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:
1 parent
9a34e03ae1
commit
230acb700b
8 files changed
+216
-415
No files matched your search
@@ -149,6 +149,7 @@ export async function createOrderSeeder({ api }) {
|
|||||||
await api.post(`/store/carts`, {
|
await api.post(`/store/carts`, {
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
email: "tony@stark-industries.com",
|
email: "tony@stark-industries.com",
|
||||||
|
region_id: region.id,
|
||||||
shipping_address: {
|
shipping_address: {
|
||||||
address_1: "test address 1",
|
address_1: "test address 1",
|
||||||
address_2: "test address 2",
|
address_2: "test address 2",
|
||||||
|
|||||||
@@ -1,102 +1,61 @@
|
|||||||
import { IPaymentModuleService } from "@medusajs/types"
|
import { ClaimType, ModuleRegistrationName } from "@medusajs/utils"
|
||||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
|
||||||
import { adminHeaders } from "../../../../helpers/create-admin-user"
|
import { adminHeaders } from "../../../../helpers/create-admin-user"
|
||||||
import { seedStorefrontDefaults } from "../../../../helpers/seed-storefront-defaults"
|
import { seedStorefrontDefaults } from "../../../../helpers/seed-storefront-defaults"
|
||||||
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
import { setupTaxStructure } from "../../../../modules/__tests__/fixtures"
|
||||||
|
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
import { createAdminUser } from "../../../../helpers/create-admin-user"
|
||||||
import { getProductFixture } from "../../../../helpers/fixtures"
|
|
||||||
import { createOrderSeeder } from "../../fixtures/order"
|
import { createOrderSeeder } from "../../fixtures/order"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let paymentModule: IPaymentModuleService
|
|
||||||
let paymentCollection
|
|
||||||
let payment
|
|
||||||
let container
|
let container
|
||||||
let region
|
let order
|
||||||
let product
|
|
||||||
let cart
|
const createClaim = async ({ order }) => {
|
||||||
|
const claim = (
|
||||||
|
await api.post(
|
||||||
|
"/admin/claims",
|
||||||
|
{
|
||||||
|
order_id: order.id,
|
||||||
|
type: ClaimType.REPLACE,
|
||||||
|
description: "Base claim",
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data.claim
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/admin/claims/${claim.id}/inbound/items`,
|
||||||
|
{ items: [{ id: order.items[0].id, quantity: 1 }] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(`/admin/claims/${claim.id}/request`, {}, adminHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
container = getContainer()
|
container = getContainer()
|
||||||
paymentModule = container.resolve(ModuleRegistrationName.PAYMENT)
|
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
order = await createOrderSeeder({ api })
|
||||||
|
|
||||||
region = (
|
|
||||||
await api.post(
|
await api.post(
|
||||||
"/admin/regions",
|
`/admin/orders/${order.id}/fulfillments`,
|
||||||
{ name: "United States", currency_code: "usd", countries: ["us"] },
|
{ items: [{ id: order.items[0].id, quantity: 1 }] },
|
||||||
adminHeaders
|
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 = (
|
describe("with outstanding amount due to claim", () => {
|
||||||
await api.post(
|
beforeEach(async () => {
|
||||||
"/store/payment-collections",
|
await createClaim({ order })
|
||||||
{
|
|
||||||
cart_id: cart.id,
|
|
||||||
},
|
|
||||||
adminHeaders
|
|
||||||
)
|
|
||||||
).data.payment_collection
|
|
||||||
|
|
||||||
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 () => {
|
it("should capture an authorized payment", async () => {
|
||||||
|
const payment = order.payment_collections[0].payments[0]
|
||||||
|
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/payments/${payment.id}/capture`,
|
`/admin/payments/${payment.id}/capture`,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -110,17 +69,19 @@ medusaIntegrationTestRunner({
|
|||||||
captures: [
|
captures: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
amount: 1000,
|
amount: 100,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
refunds: [],
|
refunds: [],
|
||||||
amount: 1000,
|
amount: 100,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
expect(response.status).toEqual(200)
|
expect(response.status).toEqual(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should refund a captured payment", async () => {
|
it("should refund a captured payment", async () => {
|
||||||
|
const payment = order.payment_collections[0].payments[0]
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/capture`,
|
`/admin/payments/${payment.id}/capture`,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -128,14 +89,18 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const refundReason = (
|
const refundReason = (
|
||||||
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
|
await api.post(
|
||||||
|
`/admin/refund-reasons`,
|
||||||
|
{ label: "test" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.refund_reason
|
).data.refund_reason
|
||||||
|
|
||||||
// BREAKING: reason is now refund_reason_id
|
// BREAKING: reason is now refund_reason_id
|
||||||
const response = await api.post(
|
const response = await api.post(
|
||||||
`/admin/payments/${payment.id}/refund`,
|
`/admin/payments/${payment.id}/refund`,
|
||||||
{
|
{
|
||||||
amount: 500,
|
amount: 50,
|
||||||
refund_reason_id: refundReason.id,
|
refund_reason_id: refundReason.id,
|
||||||
note: "Do not like it",
|
note: "Do not like it",
|
||||||
},
|
},
|
||||||
@@ -151,13 +116,13 @@ medusaIntegrationTestRunner({
|
|||||||
captures: [
|
captures: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
amount: 1000,
|
amount: 100,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
refunds: [
|
refunds: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
amount: 500,
|
amount: 50,
|
||||||
note: "Do not like it",
|
note: "Do not like it",
|
||||||
refund_reason_id: refundReason.id,
|
refund_reason_id: refundReason.id,
|
||||||
refund_reason: expect.objectContaining({
|
refund_reason: expect.objectContaining({
|
||||||
@@ -165,12 +130,14 @@ medusaIntegrationTestRunner({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
amount: 1000,
|
amount: 100,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should issue multiple refunds", async () => {
|
it("should issue multiple refunds", async () => {
|
||||||
|
const payment = order.payment_collections[0].payments[0]
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/capture`,
|
`/admin/payments/${payment.id}/capture`,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -178,13 +145,17 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const refundReason = (
|
const refundReason = (
|
||||||
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
|
await api.post(
|
||||||
|
`/admin/refund-reasons`,
|
||||||
|
{ label: "test" },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
).data.refund_reason
|
).data.refund_reason
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/refund`,
|
`/admin/payments/${payment.id}/refund`,
|
||||||
{
|
{
|
||||||
amount: 250,
|
amount: 25,
|
||||||
refund_reason_id: refundReason.id,
|
refund_reason_id: refundReason.id,
|
||||||
note: "Do not like it",
|
note: "Do not like it",
|
||||||
},
|
},
|
||||||
@@ -194,7 +165,7 @@ medusaIntegrationTestRunner({
|
|||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/refund`,
|
`/admin/payments/${payment.id}/refund`,
|
||||||
{
|
{
|
||||||
amount: 250,
|
amount: 25,
|
||||||
refund_reason_id: refundReason.id,
|
refund_reason_id: refundReason.id,
|
||||||
note: "Do not like it",
|
note: "Do not like it",
|
||||||
},
|
},
|
||||||
@@ -209,20 +180,20 @@ medusaIntegrationTestRunner({
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: payment.id,
|
id: payment.id,
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
amount: 1000,
|
amount: 100,
|
||||||
captured_at: expect.any(String),
|
captured_at: expect.any(String),
|
||||||
captures: [
|
captures: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 1000,
|
amount: 100,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
refunds: [
|
refunds: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 250,
|
amount: 25,
|
||||||
note: "Do not like it",
|
note: "Do not like it",
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
amount: 250,
|
amount: 25,
|
||||||
note: "Do not like it",
|
note: "Do not like it",
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -231,40 +202,30 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should throw if refund exceeds captured total", async () => {
|
it("should throw if refund exceeds captured total", async () => {
|
||||||
|
const payment = order.payment_collections[0].payments[0]
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/capture`,
|
`/admin/payments/${payment.id}/capture`,
|
||||||
undefined,
|
undefined,
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
const refundReason = (
|
|
||||||
await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders)
|
|
||||||
).data.refund_reason
|
|
||||||
|
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/payments/${payment.id}/refund`,
|
`/admin/payments/${payment.id}/refund`,
|
||||||
{
|
{ amount: 25 },
|
||||||
amount: 250,
|
|
||||||
refund_reason_id: refundReason.id,
|
|
||||||
note: "Do not like it",
|
|
||||||
},
|
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
const e = await api
|
const e = await api
|
||||||
.post(
|
.post(
|
||||||
`/admin/payments/${payment.id}/refund`,
|
`/admin/payments/${payment.id}/refund`,
|
||||||
{
|
{ amount: 1000 },
|
||||||
amount: 1000,
|
|
||||||
refund_reason_id: refundReason.id,
|
|
||||||
note: "Do not like it",
|
|
||||||
},
|
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
.catch((e) => e)
|
.catch((e) => e)
|
||||||
|
|
||||||
expect(e.response.data.message).toEqual(
|
expect(e.response.data.message).toEqual(
|
||||||
"You cannot refund more than what is captured on the payment."
|
"Cannot refund more than pending difference - 75"
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -283,16 +244,14 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
const order1Payment = order1.payment_collections[0].payments[0]
|
const order1Payment = order1.payment_collections[0].payments[0]
|
||||||
|
|
||||||
const result = await api.post(
|
await api.post(
|
||||||
`/admin/payments/${order1Payment.id}/capture?fields=*payment_collection`,
|
`/admin/payments/${order1Payment.id}/capture?fields=*payment_collection`,
|
||||||
{
|
{ amount: order1Payment.amount },
|
||||||
amount: order1Payment.amount,
|
|
||||||
},
|
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders)).data
|
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders))
|
||||||
.order
|
.data.order
|
||||||
|
|
||||||
expect(order1).toEqual(
|
expect(order1).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -303,8 +262,8 @@ medusaIntegrationTestRunner({
|
|||||||
|
|
||||||
let order2 = await createOrderSeeder({ api })
|
let order2 = await createOrderSeeder({ api })
|
||||||
|
|
||||||
order2 = (await api.get(`/admin/orders/${order2.id}`, adminHeaders)).data
|
order2 = (await api.get(`/admin/orders/${order2.id}`, adminHeaders))
|
||||||
.order
|
.data.order
|
||||||
|
|
||||||
expect(order2).toEqual(
|
expect(order2).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -313,8 +272,8 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders)).data
|
order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders))
|
||||||
.order
|
.data.order
|
||||||
|
|
||||||
expect(order1).toEqual(
|
expect(order1).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
@@ -323,5 +282,28 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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 e = await api
|
||||||
|
.post(
|
||||||
|
`/admin/payments/${payment.id}/refund`,
|
||||||
|
{ amount: 10 },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(e.response.data.message).toEqual(
|
||||||
|
"Order does not have an outstanding balance to refund"
|
||||||
|
)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
@@ -10,9 +11,10 @@ medusaIntegrationTestRunner({
|
|||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
let region1
|
let region1
|
||||||
let region2
|
let region2
|
||||||
|
let container
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const container = getContainer()
|
container = getContainer()
|
||||||
await createAdminUser(dbConnection, adminHeaders, container)
|
await createAdminUser(dbConnection, adminHeaders, container)
|
||||||
|
|
||||||
region1 = (
|
region1 = (
|
||||||
@@ -102,6 +104,37 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should list payment providers", async () => {
|
||||||
|
const remoteLink = container.resolve(
|
||||||
|
ContainerRegistrationKeys.REMOTE_LINK
|
||||||
|
)
|
||||||
|
|
||||||
|
let response = await api.get(
|
||||||
|
`/store/regions/${region1.id}?fields=*payment_providers`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.region.payment_providers).toEqual([])
|
||||||
|
|
||||||
|
await remoteLink.create([
|
||||||
|
{
|
||||||
|
[Modules.REGION]: { region_id: region1.id },
|
||||||
|
[Modules.PAYMENT]: { payment_provider_id: "pp_system_default" },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
response = await api.get(
|
||||||
|
`/store/regions/${region1.id}?fields=*payment_providers`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(response.status).toEqual(200)
|
||||||
|
expect(response.data.region.payment_providers).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "pp_system_default",
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/regions", () => {
|
describe("POST /admin/regions", () => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
ICartModuleService,
|
ICartModuleService,
|
||||||
IFulfillmentModuleService,
|
IFulfillmentModuleService,
|
||||||
IInventoryServiceNext,
|
IInventoryService,
|
||||||
IOrderModuleService,
|
IOrderModuleService,
|
||||||
IPaymentModuleService,
|
IPaymentModuleService,
|
||||||
IPricingModuleService,
|
IPricingModuleService,
|
||||||
IProductModuleService,
|
IProductModuleService,
|
||||||
IRegionModuleService,
|
IRegionModuleService,
|
||||||
IStockLocationServiceNext,
|
IStockLocationService,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
@@ -32,8 +32,8 @@ medusaIntegrationTestRunner({
|
|||||||
let productModule: IProductModuleService
|
let productModule: IProductModuleService
|
||||||
let paymentModule: IPaymentModuleService
|
let paymentModule: IPaymentModuleService
|
||||||
let pricingModule: IPricingModuleService
|
let pricingModule: IPricingModuleService
|
||||||
let inventoryModule: IInventoryServiceNext
|
let inventoryModule: IInventoryService
|
||||||
let stockLocationModule: IStockLocationServiceNext
|
let stockLocationModule: IStockLocationService
|
||||||
let fulfillmentModule: IFulfillmentModuleService
|
let fulfillmentModule: IFulfillmentModuleService
|
||||||
let orderModule: IOrderModuleService
|
let orderModule: IOrderModuleService
|
||||||
let remoteLink, remoteQuery
|
let remoteLink, remoteQuery
|
||||||
@@ -138,6 +138,7 @@ medusaIntegrationTestRunner({
|
|||||||
display_id: 1,
|
display_id: 1,
|
||||||
payment_collections: [],
|
payment_collections: [],
|
||||||
payment_status: "not_paid",
|
payment_status: "not_paid",
|
||||||
|
region_id: "test_region_id",
|
||||||
fulfillments: [],
|
fulfillments: [],
|
||||||
fulfillment_status: "not_fulfilled",
|
fulfillment_status: "not_fulfilled",
|
||||||
summary: expect.objectContaining({
|
summary: expect.objectContaining({
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
import {
|
|
||||||
capturePaymentWorkflow,
|
|
||||||
refundPaymentWorkflow,
|
|
||||||
} from "@medusajs/core-flows"
|
|
||||||
import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types"
|
|
||||||
import {
|
|
||||||
ContainerRegistrationKeys,
|
|
||||||
ModuleRegistrationName,
|
|
||||||
Modules,
|
|
||||||
} from "@medusajs/utils"
|
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
|
||||||
|
|
||||||
jest.setTimeout(50000)
|
|
||||||
|
|
||||||
const env = { MEDUSA_FF_MEDUSA_V2: true }
|
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
|
||||||
env,
|
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
|
||||||
describe("Payments", () => {
|
|
||||||
let appContainer
|
|
||||||
let regionService: IRegionModuleService
|
|
||||||
let paymentService: IPaymentModuleService
|
|
||||||
let remoteLink
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
appContainer = getContainer()
|
|
||||||
regionService = appContainer.resolve(ModuleRegistrationName.REGION)
|
|
||||||
paymentService = appContainer.resolve(ModuleRegistrationName.PAYMENT)
|
|
||||||
remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK)
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Test should move to `integration-tests/api`
|
|
||||||
it("should list payment providers", async () => {
|
|
||||||
const region = await regionService.createRegions({
|
|
||||||
name: "Test Region",
|
|
||||||
currency_code: "usd",
|
|
||||||
})
|
|
||||||
|
|
||||||
let response = await api.get(
|
|
||||||
`/store/regions/${region.id}?fields=*payment_providers`
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.region.payment_providers).toEqual([])
|
|
||||||
|
|
||||||
await remoteLink.create([
|
|
||||||
{
|
|
||||||
[Modules.REGION]: {
|
|
||||||
region_id: region.id,
|
|
||||||
},
|
|
||||||
[Modules.PAYMENT]: {
|
|
||||||
payment_provider_id: "pp_system_default",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
response = await api.get(
|
|
||||||
`/store/regions/${region.id}?fields=*payment_providers`
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(response.status).toEqual(200)
|
|
||||||
expect(response.data.region.payment_providers).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "pp_system_default",
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should capture a payment", async () => {
|
|
||||||
const paymentCollection = await paymentService.createPaymentCollections(
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const paymentSession = await paymentService.createPaymentSession(
|
|
||||||
paymentCollection.id,
|
|
||||||
{
|
|
||||||
provider_id: "pp_system_default",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
data: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const payment = await paymentService.authorizePaymentSession(
|
|
||||||
paymentSession.id,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
await capturePaymentWorkflow(appContainer).run({
|
|
||||||
input: {
|
|
||||||
payment_id: payment.id,
|
|
||||||
},
|
|
||||||
throwOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [paymentResult] = await paymentService.listPayments({
|
|
||||||
id: payment.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(paymentResult).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: payment.id,
|
|
||||||
amount: 1000,
|
|
||||||
payment_collection_id: paymentCollection.id,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const [capture] = await paymentService.listCaptures({
|
|
||||||
payment_id: payment.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(capture).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
payment: expect.objectContaining({ id: payment.id }),
|
|
||||||
amount: 1000,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should partially capture a payment", async () => {
|
|
||||||
const paymentCollection = await paymentService.createPaymentCollections(
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const paymentSession = await paymentService.createPaymentSession(
|
|
||||||
paymentCollection.id,
|
|
||||||
{
|
|
||||||
provider_id: "pp_system_default",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
data: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const payment = await paymentService.authorizePaymentSession(
|
|
||||||
paymentSession.id,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
await capturePaymentWorkflow(appContainer).run({
|
|
||||||
input: {
|
|
||||||
payment_id: payment.id,
|
|
||||||
amount: 500,
|
|
||||||
},
|
|
||||||
throwOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [paymentResult] = await paymentService.listPayments({
|
|
||||||
id: payment.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(paymentResult).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: payment.id,
|
|
||||||
amount: 1000,
|
|
||||||
payment_collection_id: paymentCollection.id,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const [capture] = await paymentService.listCaptures({
|
|
||||||
payment_id: payment.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(capture).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
payment: expect.objectContaining({ id: payment.id }),
|
|
||||||
amount: 500,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should refund a payment", async () => {
|
|
||||||
const paymentCollection = await paymentService.createPaymentCollections(
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const paymentSession = await paymentService.createPaymentSession(
|
|
||||||
paymentCollection.id,
|
|
||||||
{
|
|
||||||
provider_id: "pp_system_default",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
data: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const payment = await paymentService.authorizePaymentSession(
|
|
||||||
paymentSession.id,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
await capturePaymentWorkflow(appContainer).run({
|
|
||||||
input: {
|
|
||||||
payment_id: payment.id,
|
|
||||||
},
|
|
||||||
throwOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
await refundPaymentWorkflow(appContainer).run({
|
|
||||||
input: {
|
|
||||||
payment_id: payment.id,
|
|
||||||
},
|
|
||||||
throwOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [refund] = await paymentService.listRefunds({
|
|
||||||
payment_id: payment.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(refund).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
payment: expect.objectContaining({ id: payment.id }),
|
|
||||||
amount: 1000,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("should partially refund a payment", async () => {
|
|
||||||
const paymentCollection = await paymentService.createPaymentCollections(
|
|
||||||
{
|
|
||||||
region_id: "test-region",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const paymentSession = await paymentService.createPaymentSession(
|
|
||||||
paymentCollection.id,
|
|
||||||
{
|
|
||||||
provider_id: "pp_system_default",
|
|
||||||
amount: 1000,
|
|
||||||
currency_code: "usd",
|
|
||||||
data: {},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const payment = await paymentService.authorizePaymentSession(
|
|
||||||
paymentSession.id,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
|
|
||||||
await capturePaymentWorkflow(appContainer).run({
|
|
||||||
input: {
|
|
||||||
payment_id: payment.id,
|
|
||||||
},
|
|
||||||
throwOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
await refundPaymentWorkflow(appContainer).run({
|
|
||||||
input: {
|
|
||||||
payment_id: payment.id,
|
|
||||||
amount: 500,
|
|
||||||
},
|
|
||||||
throwOnError: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const [refund] = await paymentService.listRefunds({
|
|
||||||
payment_id: payment.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(refund).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(String),
|
|
||||||
payment: expect.objectContaining({ id: payment.id }),
|
|
||||||
amount: 500,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { BigNumberInput } from "@medusajs/types"
|
import { BigNumberInput, OrderDTO, PaymentDTO } from "@medusajs/types"
|
||||||
import { MathBN, PaymentEvents } from "@medusajs/utils"
|
import { MathBN, MedusaError, PaymentEvents } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
|
createStep,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
when,
|
when,
|
||||||
@@ -11,6 +12,41 @@ import { emitEventStep, useRemoteQueryStep } from "../../common"
|
|||||||
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
import { addOrderTransactionStep } from "../../order/steps/add-order-transaction"
|
||||||
import { refundPaymentStep } from "../steps/refund-payment"
|
import { refundPaymentStep } from "../steps/refund-payment"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step validates that the refund is valid for the order
|
||||||
|
*/
|
||||||
|
export const validateRefundStep = createStep(
|
||||||
|
"validate-refund-step",
|
||||||
|
async function ({
|
||||||
|
order,
|
||||||
|
payment,
|
||||||
|
amount,
|
||||||
|
}: {
|
||||||
|
order: OrderDTO
|
||||||
|
payment: PaymentDTO
|
||||||
|
amount?: BigNumberInput
|
||||||
|
}) {
|
||||||
|
const pendingDifference = order.summary?.raw_pending_difference!
|
||||||
|
|
||||||
|
if (MathBN.gte(pendingDifference, 0)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Order does not have an outstanding balance to refund`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const amountPending = MathBN.mult(pendingDifference, -1)
|
||||||
|
const amountToRefund = amount ?? payment.raw_amount ?? payment.amount
|
||||||
|
|
||||||
|
if (MathBN.gt(amountToRefund, amountPending)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cannot refund more than pending difference - ${amountPending}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const refundPaymentWorkflowId = "refund-payment-workflow"
|
export const refundPaymentWorkflowId = "refund-payment-workflow"
|
||||||
/**
|
/**
|
||||||
* This workflow refunds a payment.
|
* This workflow refunds a payment.
|
||||||
@@ -24,28 +60,52 @@ export const refundPaymentWorkflow = createWorkflow(
|
|||||||
amount?: BigNumberInput
|
amount?: BigNumberInput
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
const payment = refundPaymentStep(input)
|
const payment = useRemoteQueryStep({
|
||||||
|
entry_point: "payment",
|
||||||
|
fields: [
|
||||||
|
"id",
|
||||||
|
"payment_collection_id",
|
||||||
|
"currency_code",
|
||||||
|
"amount",
|
||||||
|
"raw_amount",
|
||||||
|
],
|
||||||
|
variables: { id: input.payment_id },
|
||||||
|
list: false,
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
})
|
||||||
|
|
||||||
const orderPayment = useRemoteQueryStep({
|
const orderPaymentCollection = useRemoteQueryStep({
|
||||||
entry_point: "order_payment_collection",
|
entry_point: "order_payment_collection",
|
||||||
fields: ["order.id"],
|
fields: ["order.id"],
|
||||||
variables: { payment_collection_id: payment.payment_collection_id },
|
variables: { payment_collection_id: payment.payment_collection_id },
|
||||||
list: false,
|
list: false,
|
||||||
})
|
throw_if_key_not_found: true,
|
||||||
|
}).config({ name: "order-payment-collection" })
|
||||||
|
|
||||||
when({ orderPayment }, ({ orderPayment }) => {
|
const order = useRemoteQueryStep({
|
||||||
return !!orderPayment?.order?.id
|
entry_point: "order",
|
||||||
|
fields: ["id", "summary", "currency_code", "region_id"],
|
||||||
|
variables: { id: orderPaymentCollection.order.id },
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
list: false,
|
||||||
|
}).config({ name: "order" })
|
||||||
|
|
||||||
|
validateRefundStep({ order, payment, amount: input.amount })
|
||||||
|
refundPaymentStep(input)
|
||||||
|
|
||||||
|
when({ orderPaymentCollection }, ({ orderPaymentCollection }) => {
|
||||||
|
return !!orderPaymentCollection?.order?.id
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
const orderTransactionData = transform(
|
const orderTransactionData = transform(
|
||||||
{ input, payment, orderPayment },
|
{ input, payment, orderPaymentCollection },
|
||||||
({ input, payment, orderPayment }) => {
|
({ input, payment, orderPaymentCollection }) => {
|
||||||
return {
|
return {
|
||||||
order_id: orderPayment.order.id,
|
order_id: orderPaymentCollection.order.id,
|
||||||
amount: MathBN.mult(
|
amount: MathBN.mult(
|
||||||
input.amount ?? payment.raw_amount ?? payment.amount,
|
input.amount ?? payment.raw_amount ?? payment.amount,
|
||||||
-1
|
-1
|
||||||
),
|
),
|
||||||
currency_code: payment.currency_code,
|
currency_code: payment.currency_code ?? order.currency_code,
|
||||||
reference_id: payment.id,
|
reference_id: payment.id,
|
||||||
reference: "refund",
|
reference: "refund",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,16 @@ export type OrderSummaryDTO = {
|
|||||||
* The refunded total of the order summary.
|
* The refunded total of the order summary.
|
||||||
*/
|
*/
|
||||||
refunded_total: BigNumberValue
|
refunded_total: BigNumberValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pending difference of the order.
|
||||||
|
*/
|
||||||
|
pending_difference: BigNumberValue
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw pending difference of the order.
|
||||||
|
*/
|
||||||
|
raw_pending_difference: BigNumberRawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const defaultAdminOrderFields = [
|
|||||||
export const defaultAdminRetrieveOrderFields = [
|
export const defaultAdminRetrieveOrderFields = [
|
||||||
"id",
|
"id",
|
||||||
"display_id",
|
"display_id",
|
||||||
|
"region_id",
|
||||||
"status",
|
"status",
|
||||||
"version",
|
"version",
|
||||||
"summary",
|
"summary",
|
||||||
|
|||||||
Reference in new issue
Block a user