From 4e06064a9adb9ed616895a73c51aac460610658b Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 2 Sep 2024 16:15:46 +0200 Subject: [PATCH 1/3] feat(core-flows,types): Refunds can only be performed when order is imbalanced --- .../http/__tests__/fixtures/order.ts | 1 + .../__tests__/payment/admin/payment.spec.ts | 496 +++++++++--------- .../src/payment/workflows/refund-payment.ts | 82 ++- packages/core/types/src/order/common.ts | 10 + 4 files changed, 321 insertions(+), 268 deletions(-) diff --git a/integration-tests/http/__tests__/fixtures/order.ts b/integration-tests/http/__tests__/fixtures/order.ts index 4ed8bdaf708e8..bc857f8491522 100644 --- a/integration-tests/http/__tests__/fixtures/order.ts +++ b/integration-tests/http/__tests__/fixtures/order.ts @@ -149,6 +149,7 @@ export async function createOrderSeeder({ api }) { await api.post(`/store/carts`, { currency_code: "usd", email: "tony@stark-industries.com", + region_id: region.id, shipping_address: { address_1: "test address 1", address_2: "test address 2", diff --git a/integration-tests/http/__tests__/payment/admin/payment.spec.ts b/integration-tests/http/__tests__/payment/admin/payment.spec.ts index 13f473336e24e..39bb096640352 100644 --- a/integration-tests/http/__tests__/payment/admin/payment.spec.ts +++ b/integration-tests/http/__tests__/payment/admin/payment.spec.ts @@ -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 + + 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 () => { container = getContainer() - paymentModule = container.resolve(ModuleRegistrationName.PAYMENT) await createAdminUser(dbConnection, adminHeaders, container) + order = await createOrderSeeder({ api }) - region = ( - await api.post( - "/admin/regions", - { name: "United States", currency_code: "usd", countries: ["us"] }, + await api.post( + `/admin/orders/${order.id}/fulfillments`, + { items: [{ id: order.items[0].id, quantity: 1 }] }, + adminHeaders + ) + }) + + 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 ) - ).data.region - product = ( + 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: [], + 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/products", - getProductFixture({ - title: "test", - status: "published", - variants: [ - { - title: "Test variant", - manage_inventory: false, - prices: [ - { - amount: 1000, - currency_code: "usd", - rules: { region_id: region.id }, - }, - ], - }, + `/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 ) - ).data.product - cart = ( - await api.post("/store/carts", { - region_id: region.id, - items: [{ variant_id: product.variants[0].id, quantity: 1 }], - }) - ).data.cart + const refundReason = ( + await api.post( + `/admin/refund-reasons`, + { label: "test" }, + adminHeaders + ) + ).data.refund_reason - const collection = ( await api.post( - "/store/payment-collections", + `/admin/payments/${payment.id}/refund`, { - cart_id: cart.id, + amount: 25, + refund_reason_id: refundReason.id, + note: "Do not like it", }, adminHeaders ) - ).data.payment_collection - paymentCollection = ( await api.post( - `/store/payment-collections/${collection.id}/payment-sessions`, - { provider_id: "pp_system_default" }, + `/admin/payments/${payment.id}/refund`, + { + amount: 25, + refund_reason_id: refundReason.id, + note: "Do not like it", + }, 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 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", + }), + ], + }) + ) + }) - const payments = ( - await api.get( - `/admin/payments?payment_session_id=${lastSession.id}`, + 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 ) - ).data.payments - payment = payments[0] - }) - it("should capture an authorized payment", async () => { - const response = await api.post( - `/admin/payments/${payment.id}/capture`, - undefined, - adminHeaders - ) + await api.post( + `/admin/payments/${payment.id}/refund`, + { amount: 25 }, + 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) - }) + const e = await api + .post( + `/admin/payments/${payment.id}/refund`, + { amount: 1000 }, + adminHeaders + ) + .catch((e) => e) - it("should refund a captured payment", async () => { - await api.post( - `/admin/payments/${payment.id}/capture`, - undefined, - adminHeaders - ) + expect(e.response.data.message).toEqual( + "amount to refund cannot be greater than pending difference - 75" + ) + }) - 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: 500, - refund_reason_id: refundReason.id, - note: "Do not like it", - }, - adminHeaders - ) + it("should not update payment collection of other orders", async () => { + await setupTaxStructure(container.resolve(ModuleRegistrationName.TAX)) + await seedStorefrontDefaults(container, "dkk") - // 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", - }), - }), - ], - amount: 1000, - }) - ) - }) + let order1 = await createOrderSeeder({ api }) - it("should issue multiple refunds", async () => { - await api.post( - `/admin/payments/${payment.id}/capture`, - undefined, - adminHeaders - ) + expect(order1).toEqual( + expect.objectContaining({ + id: expect.any(String), + payment_status: "authorized", + }) + ) - const refundReason = ( - await api.post(`/admin/refund-reasons`, { label: "test" }, adminHeaders) - ).data.refund_reason + const order1Payment = order1.payment_collections[0].payments[0] - 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/${order1Payment.id}/capture?fields=*payment_collection`, + { amount: order1Payment.amount }, + adminHeaders + ) - await api.post( - `/admin/payments/${payment.id}/refund`, - { - amount: 250, - refund_reason_id: refundReason.id, - note: "Do not like it", - }, - adminHeaders - ) + order1 = (await api.get(`/admin/orders/${order1.id}`, adminHeaders)) + .data.order - 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", - }), - ], - }) - ) + 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 throw if refund exceeds captured total", 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 - ) - 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" ) }) }, diff --git a/packages/core/core-flows/src/payment/workflows/refund-payment.ts b/packages/core/core-flows/src/payment/workflows/refund-payment.ts index c51a0856b8aa1..9f5b5c0500e25 100644 --- a/packages/core/core-flows/src/payment/workflows/refund-payment.ts +++ b/packages/core/core-flows/src/payment/workflows/refund-payment.ts @@ -1,8 +1,9 @@ -import { BigNumberInput } from "@medusajs/types" -import { MathBN, PaymentEvents } from "@medusajs/utils" +import { BigNumberInput, OrderDTO, PaymentDTO } from "@medusajs/types" +import { MathBN, MedusaError, PaymentEvents } from "@medusajs/utils" import { WorkflowData, WorkflowResponse, + createStep, createWorkflow, transform, when, @@ -11,6 +12,41 @@ import { emitEventStep, useRemoteQueryStep } from "../../common" import { addOrderTransactionStep } from "../../order/steps/add-order-transaction" import { refundPaymentStep } from "../steps/refund-payment" +/** + * This step validates that the refund is valid for the order + */ +export const validateRefundStep = createStep( + "begin-claim-order-validation", + 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, + `amount to refund cannot be greater than pending difference - ${amountPending}` + ) + } + } +) + export const refundPaymentWorkflowId = "refund-payment-workflow" /** * This workflow refunds a payment. @@ -24,28 +60,52 @@ export const refundPaymentWorkflow = createWorkflow( 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", fields: ["order.id"], variables: { payment_collection_id: payment.payment_collection_id }, list: false, - }) + throw_if_key_not_found: true, + }).config({ name: "order-payment-collection" }) + + const order = useRemoteQueryStep({ + 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({ orderPayment }, ({ orderPayment }) => { - return !!orderPayment?.order?.id + when({ orderPaymentCollection }, ({ orderPaymentCollection }) => { + return !!orderPaymentCollection?.order?.id }).then(() => { const orderTransactionData = transform( - { input, payment, orderPayment }, - ({ input, payment, orderPayment }) => { + { input, payment, orderPaymentCollection }, + ({ input, payment, orderPaymentCollection }) => { return { - order_id: orderPayment.order.id, + order_id: orderPaymentCollection.order.id, amount: MathBN.mult( input.amount ?? payment.raw_amount ?? payment.amount, -1 ), - currency_code: payment.currency_code, + currency_code: payment.currency_code ?? order.currency_code, reference_id: payment.id, reference: "refund", } diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 336633cd80736..f6575c3672480 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -104,6 +104,16 @@ export type OrderSummaryDTO = { * The refunded total of the order summary. */ refunded_total: BigNumberValue + + /** + * The pending difference of the order. + */ + pending_difference: BigNumberValue + + /** + * The raw pending difference of the order. + */ + raw_pending_difference: BigNumberRawValue } /** From b310fab493bdcfd7e6721ab0e0b3654de9a5584e Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 2 Sep 2024 22:26:27 +0200 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../core/core-flows/src/payment/workflows/refund-payment.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/core-flows/src/payment/workflows/refund-payment.ts b/packages/core/core-flows/src/payment/workflows/refund-payment.ts index 9f5b5c0500e25..d94c07707e138 100644 --- a/packages/core/core-flows/src/payment/workflows/refund-payment.ts +++ b/packages/core/core-flows/src/payment/workflows/refund-payment.ts @@ -16,7 +16,7 @@ import { refundPaymentStep } from "../steps/refund-payment" * This step validates that the refund is valid for the order */ export const validateRefundStep = createStep( - "begin-claim-order-validation", + "validate-refund-step", async function ({ order, payment, @@ -41,7 +41,7 @@ export const validateRefundStep = createStep( if (MathBN.gt(amountToRefund, amountPending)) { throw new MedusaError( MedusaError.Types.INVALID_DATA, - `amount to refund cannot be greater than pending difference - ${amountPending}` + `Cannot refund more than pending difference - ${amountPending}` ) } } From 886a00654b43b1d94e0c90fce6ab57d38683dad8 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 2 Sep 2024 23:04:26 +0200 Subject: [PATCH 3/3] chore: fix tests --- .../__tests__/payment/admin/payment.spec.ts | 2 +- .../__tests__/region/admin/region.spec.ts | 35 ++- .../modules/__tests__/order/order.spec.ts | 9 +- .../__tests__/payment/payments.spec.ts | 287 ------------------ .../src/api/admin/orders/query-config.ts | 1 + 5 files changed, 41 insertions(+), 293 deletions(-) delete mode 100644 integration-tests/modules/__tests__/payment/payments.spec.ts diff --git a/integration-tests/http/__tests__/payment/admin/payment.spec.ts b/integration-tests/http/__tests__/payment/admin/payment.spec.ts index 39bb096640352..f52a5a78a07f5 100644 --- a/integration-tests/http/__tests__/payment/admin/payment.spec.ts +++ b/integration-tests/http/__tests__/payment/admin/payment.spec.ts @@ -225,7 +225,7 @@ medusaIntegrationTestRunner({ .catch((e) => e) expect(e.response.data.message).toEqual( - "amount to refund cannot be greater than pending difference - 75" + "Cannot refund more than pending difference - 75" ) }) diff --git a/integration-tests/http/__tests__/region/admin/region.spec.ts b/integration-tests/http/__tests__/region/admin/region.spec.ts index 2dc63c72c4193..16fc258df97c1 100644 --- a/integration-tests/http/__tests__/region/admin/region.spec.ts +++ b/integration-tests/http/__tests__/region/admin/region.spec.ts @@ -1,3 +1,4 @@ +import { ContainerRegistrationKeys, Modules } from "@medusajs/utils" import { medusaIntegrationTestRunner } from "medusa-test-utils" import { adminHeaders, @@ -10,9 +11,10 @@ medusaIntegrationTestRunner({ testSuite: ({ dbConnection, getContainer, api }) => { let region1 let region2 + let container beforeEach(async () => { - const container = getContainer() + container = getContainer() await createAdminUser(dbConnection, adminHeaders, container) 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", () => { diff --git a/integration-tests/modules/__tests__/order/order.spec.ts b/integration-tests/modules/__tests__/order/order.spec.ts index 20c250120a89c..33950f648dda6 100644 --- a/integration-tests/modules/__tests__/order/order.spec.ts +++ b/integration-tests/modules/__tests__/order/order.spec.ts @@ -1,13 +1,13 @@ import { ICartModuleService, IFulfillmentModuleService, - IInventoryServiceNext, + IInventoryService, IOrderModuleService, IPaymentModuleService, IPricingModuleService, IProductModuleService, IRegionModuleService, - IStockLocationServiceNext, + IStockLocationService, } from "@medusajs/types" import { ContainerRegistrationKeys, @@ -32,8 +32,8 @@ medusaIntegrationTestRunner({ let productModule: IProductModuleService let paymentModule: IPaymentModuleService let pricingModule: IPricingModuleService - let inventoryModule: IInventoryServiceNext - let stockLocationModule: IStockLocationServiceNext + let inventoryModule: IInventoryService + let stockLocationModule: IStockLocationService let fulfillmentModule: IFulfillmentModuleService let orderModule: IOrderModuleService let remoteLink, remoteQuery @@ -138,6 +138,7 @@ medusaIntegrationTestRunner({ display_id: 1, payment_collections: [], payment_status: "not_paid", + region_id: "test_region_id", fulfillments: [], fulfillment_status: "not_fulfilled", summary: expect.objectContaining({ diff --git a/integration-tests/modules/__tests__/payment/payments.spec.ts b/integration-tests/modules/__tests__/payment/payments.spec.ts deleted file mode 100644 index 0c10fd318bd11..0000000000000 --- a/integration-tests/modules/__tests__/payment/payments.spec.ts +++ /dev/null @@ -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, - }) - ) - }) - }) - }, -}) diff --git a/packages/medusa/src/api/admin/orders/query-config.ts b/packages/medusa/src/api/admin/orders/query-config.ts index fe29de3cd6f9a..d4d7f9c8799f2 100644 --- a/packages/medusa/src/api/admin/orders/query-config.ts +++ b/packages/medusa/src/api/admin/orders/query-config.ts @@ -12,6 +12,7 @@ export const defaultAdminOrderFields = [ export const defaultAdminRetrieveOrderFields = [ "id", "display_id", + "region_id", "status", "version", "summary",