feat(core-flows,types): Refunds can only be performed when order is imbalanced (#8944)
* feat(core-flows,types): Refunds can only be performed when order is imbalanced * Apply suggestions from code review Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * chore: fix tests --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Oli Juhl
parent
9a34e03ae1
commit
230acb700b
@@ -1,326 +1,308 @@
|
||||
import { IPaymentModuleService } from "@medusajs/types"
|
||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { ClaimType, ModuleRegistrationName } from "@medusajs/utils"
|
||||
import { adminHeaders } from "../../../../helpers/create-admin-user"
|
||||
import { seedStorefrontDefaults } from "../../../../helpers/seed-storefront-defaults"
|
||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
||||
import { createOrderSeeder } from "../../fixtures/order"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let paymentModule: IPaymentModuleService
|
||||
let paymentCollection
|
||||
let payment
|
||||
let container
|
||||
let region
|
||||
let product
|
||||
let cart
|
||||
let order
|
||||
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
paymentModule = container.resolve(ModuleRegistrationName.PAYMENT)
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
|
||||
region = (
|
||||
const createClaim = async ({ order }) => {
|
||||
const claim = (
|
||||
await api.post(
|
||||
"/admin/regions",
|
||||
{ name: "United States", currency_code: "usd", countries: ["us"] },
|
||||
adminHeaders
|
||||
)
|
||||
).data.region
|
||||
|
||||
product = (
|
||||
await api.post(
|
||||
"/admin/products",
|
||||
getProductFixture({
|
||||
title: "test",
|
||||
status: "published",
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
manage_inventory: false,
|
||||
prices: [
|
||||
{
|
||||
amount: 1000,
|
||||
currency_code: "usd",
|
||||
rules: { region_id: region.id },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
adminHeaders
|
||||
)
|
||||
).data.product
|
||||
|
||||
cart = (
|
||||
await api.post("/store/carts", {
|
||||
region_id: region.id,
|
||||
items: [{ variant_id: product.variants[0].id, quantity: 1 }],
|
||||
})
|
||||
).data.cart
|
||||
|
||||
const collection = (
|
||||
await api.post(
|
||||
"/store/payment-collections",
|
||||
"/admin/claims",
|
||||
{
|
||||
cart_id: cart.id,
|
||||
order_id: order.id,
|
||||
type: ClaimType.REPLACE,
|
||||
description: "Base claim",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
).data.claim
|
||||
|
||||
paymentCollection = (
|
||||
await api.post(
|
||||
`/store/payment-collections/${collection.id}/payment-sessions`,
|
||||
{ provider_id: "pp_system_default" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.payment_collection
|
||||
|
||||
const lastSession = paymentCollection.payment_sessions[0]
|
||||
// TODO: Try to replace it with user behavior, like completing a cart.
|
||||
await paymentModule.authorizePaymentSession(lastSession.id, {})
|
||||
|
||||
const payments = (
|
||||
await api.get(
|
||||
`/admin/payments?payment_session_id=${lastSession.id}`,
|
||||
adminHeaders
|
||||
)
|
||||
).data.payments
|
||||
payment = payments[0]
|
||||
})
|
||||
|
||||
it("should capture an authorized payment", async () => {
|
||||
const response = await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.payment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: payment.id,
|
||||
captured_at: expect.any(String),
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 1000,
|
||||
}),
|
||||
],
|
||||
refunds: [],
|
||||
amount: 1000,
|
||||
})
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should refund a captured payment", async () => {
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
`/admin/claims/${claim.id}/inbound/items`,
|
||||
{ items: [{ id: order.items[0].id, quantity: 1 }] },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundReason = (
|
||||
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
|
||||
).data.refund_reason
|
||||
await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders)
|
||||
}
|
||||
|
||||
// BREAKING: reason is now refund_reason_id
|
||||
const response = await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 500,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
order = await createOrderSeeder({ api })
|
||||
|
||||
await api.post(
|
||||
`/admin/orders/${order.id}/fulfillments`,
|
||||
{ items: [{ id: order.items[0].id, quantity: 1 }] },
|
||||
adminHeaders
|
||||
)
|
||||
})
|
||||
|
||||
// BREAKING: Response was `data.refund` in V1 with payment ID, reason, and amount
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.payment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: payment.id,
|
||||
captured_at: expect.any(String),
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 1000,
|
||||
}),
|
||||
],
|
||||
refunds: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 500,
|
||||
note: "Do not like it",
|
||||
refund_reason_id: refundReason.id,
|
||||
refund_reason: expect.objectContaining({
|
||||
label: "test",
|
||||
describe("with outstanding amount due to claim", () => {
|
||||
beforeEach(async () => {
|
||||
await createClaim({ order })
|
||||
})
|
||||
|
||||
it("should capture an authorized payment", async () => {
|
||||
const payment = order.payment_collections[0].payments[0]
|
||||
|
||||
const response = await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
expect(response.data.payment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: payment.id,
|
||||
captured_at: expect.any(String),
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 100,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
amount: 1000,
|
||||
})
|
||||
)
|
||||
],
|
||||
refunds: [],
|
||||
amount: 100,
|
||||
})
|
||||
)
|
||||
expect(response.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("should refund a captured payment", async () => {
|
||||
const payment = order.payment_collections[0].payments[0]
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundReason = (
|
||||
await api.post(
|
||||
`/admin/refund-reasons`,
|
||||
{ label: "test" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.refund_reason
|
||||
|
||||
// BREAKING: reason is now refund_reason_id
|
||||
const response = await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 50,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
// BREAKING: Response was `data.refund` in V1 with payment ID, reason, and amount
|
||||
expect(response.status).toEqual(200)
|
||||
expect(response.data.payment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: payment.id,
|
||||
captured_at: expect.any(String),
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 100,
|
||||
}),
|
||||
],
|
||||
refunds: [
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
amount: 50,
|
||||
note: "Do not like it",
|
||||
refund_reason_id: refundReason.id,
|
||||
refund_reason: expect.objectContaining({
|
||||
label: "test",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
amount: 100,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should issue multiple refunds", async () => {
|
||||
const payment = order.payment_collections[0].payments[0]
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundReason = (
|
||||
await api.post(
|
||||
`/admin/refund-reasons`,
|
||||
{ label: "test" },
|
||||
adminHeaders
|
||||
)
|
||||
).data.refund_reason
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 25,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 25,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundedPayment = (
|
||||
await api.get(`/admin/payments/${payment.id}`, adminHeaders)
|
||||
).data.payment
|
||||
|
||||
expect(refundedPayment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: payment.id,
|
||||
currency_code: "usd",
|
||||
amount: 100,
|
||||
captured_at: expect.any(String),
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
amount: 100,
|
||||
}),
|
||||
],
|
||||
refunds: [
|
||||
expect.objectContaining({
|
||||
amount: 25,
|
||||
note: "Do not like it",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: 25,
|
||||
note: "Do not like it",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw if refund exceeds captured total", async () => {
|
||||
const payment = order.payment_collections[0].payments[0]
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{ amount: 25 },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const e = await api
|
||||
.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{ amount: 1000 },
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(e.response.data.message).toEqual(
|
||||
"Cannot refund more than pending difference - 75"
|
||||
)
|
||||
})
|
||||
|
||||
it("should not update payment collection of other orders", async () => {
|
||||
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
||||
await seedStorefrontDefaults(container, "dkk")
|
||||
|
||||
let order1 = await createOrderSeeder({ api })
|
||||
|
||||
expect(order1).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
payment_status: "authorized",
|
||||
})
|
||||
)
|
||||
|
||||
const order1Payment = order1.payment_collections[0].payments[0]
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${order1Payment.id}/capture?fields=*payment_collection`,
|
||||
{ amount: order1Payment.amount },
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders))
|
||||
.data.order
|
||||
|
||||
expect(order1).toEqual(
|
||||
expect.objectContaining({
|
||||
id: order1.id,
|
||||
payment_status: "captured",
|
||||
})
|
||||
)
|
||||
|
||||
let order2 = await createOrderSeeder({ api })
|
||||
|
||||
order2 = (await api.get(`/admin/orders/${order2.id}`, adminHeaders))
|
||||
.data.order
|
||||
|
||||
expect(order2).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
payment_status: "authorized",
|
||||
})
|
||||
)
|
||||
|
||||
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders))
|
||||
.data.order
|
||||
|
||||
expect(order1).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
payment_status: "captured",
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it("should issue multiple refunds", async () => {
|
||||
it("should throw if outstanding amount is not present", async () => {
|
||||
const payment = order.payment_collections[0].payments[0]
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundReason = (
|
||||
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
|
||||
).data.refund_reason
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 250,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 250,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundedPayment = (
|
||||
await api.get(`/admin/payments/${payment.id}`, adminHeaders)
|
||||
).data.payment
|
||||
|
||||
expect(refundedPayment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: payment.id,
|
||||
currency_code: "usd",
|
||||
amount: 1000,
|
||||
captured_at: expect.any(String),
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
amount: 1000,
|
||||
}),
|
||||
],
|
||||
refunds: [
|
||||
expect.objectContaining({
|
||||
amount: 250,
|
||||
note: "Do not like it",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
amount: 250,
|
||||
note: "Do not like it",
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw if refund exceeds captured total", async () => {
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/capture`,
|
||||
undefined,
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const refundReason = (
|
||||
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
|
||||
).data.refund_reason
|
||||
|
||||
await api.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 250,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
const e = await api
|
||||
.post(
|
||||
`/admin/payments/${payment.id}/refund`,
|
||||
{
|
||||
amount: 1000,
|
||||
refund_reason_id: refundReason.id,
|
||||
note: "Do not like it",
|
||||
},
|
||||
{ amount: 10 },
|
||||
adminHeaders
|
||||
)
|
||||
.catch((e) => e)
|
||||
|
||||
expect(e.response.data.message).toEqual(
|
||||
"You cannot refund more than what is captured on the payment."
|
||||
)
|
||||
})
|
||||
|
||||
it("should not update payment collection of other orders", async () => {
|
||||
await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX))
|
||||
await seedStorefrontDefaults(container, "dkk")
|
||||
|
||||
let order1 = await createOrderSeeder({ api })
|
||||
|
||||
expect(order1).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
payment_status: "authorized",
|
||||
})
|
||||
)
|
||||
|
||||
const order1Payment = order1.payment_collections[0].payments[0]
|
||||
|
||||
const result = await api.post(
|
||||
`/admin/payments/${order1Payment.id}/capture?fields=*payment_collection`,
|
||||
{
|
||||
amount: order1Payment.amount,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders)).data
|
||||
.order
|
||||
|
||||
expect(order1).toEqual(
|
||||
expect.objectContaining({
|
||||
id: order1.id,
|
||||
payment_status: "captured",
|
||||
})
|
||||
)
|
||||
|
||||
let order2 = await createOrderSeeder({ api })
|
||||
|
||||
order2 = (await api.get(`/admin/orders/${order2.id}`, adminHeaders)).data
|
||||
.order
|
||||
|
||||
expect(order2).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
payment_status: "authorized",
|
||||
})
|
||||
)
|
||||
|
||||
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders)).data
|
||||
.order
|
||||
|
||||
expect(order1).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.any(String),
|
||||
payment_status: "captured",
|
||||
})
|
||||
"Order does not have an outstanding balance to refund"
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user