feat: Capture payment (#6601)
* feat: Capture payment * add amount to workflow input
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
|
||||
import { IRegionModuleService } from "@medusajs/types"
|
||||
import { capturePaymentWorkflow } from "@medusajs/core-flows"
|
||||
import {
|
||||
LinkModuleUtils,
|
||||
ModuleRegistrationName,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IPaymentModuleService, IRegionModuleService } from "@medusajs/types"
|
||||
import { medusaIntegrationTestRunner } from "medusa-test-utils/dist"
|
||||
|
||||
jest.setTimeout(50000)
|
||||
@@ -12,14 +17,17 @@ medusaIntegrationTestRunner({
|
||||
describe("Payments", () => {
|
||||
let appContainer
|
||||
let regionService: IRegionModuleService
|
||||
let paymentService: IPaymentModuleService
|
||||
let remoteLink
|
||||
|
||||
beforeAll(async () => {
|
||||
appContainer = getContainer()
|
||||
regionService = appContainer.resolve(ModuleRegistrationName.REGION)
|
||||
remoteLink = appContainer.resolve("remoteLink")
|
||||
paymentService = appContainer.resolve(ModuleRegistrationName.PAYMENT)
|
||||
remoteLink = appContainer.resolve(LinkModuleUtils.REMOTE_LINK)
|
||||
})
|
||||
|
||||
// TODO: Test should move to `integration-tests/api`
|
||||
it("should list payment providers", async () => {
|
||||
const region = await regionService.create({
|
||||
name: "Test Region",
|
||||
@@ -55,6 +63,119 @@ medusaIntegrationTestRunner({
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
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 capture a payment with custom amount", 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,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
export * from "./api-key"
|
||||
export * from "./customer"
|
||||
export * from "./customer-group"
|
||||
export * from "./definition"
|
||||
export * from "./definitions"
|
||||
export * as Handlers from "./handlers"
|
||||
export * from "./invite"
|
||||
export * from "./payment"
|
||||
export * from "./product"
|
||||
export * from "./promotion"
|
||||
export * from "./region"
|
||||
export * from "./user"
|
||||
export * from "./tax"
|
||||
export * from "./api-key"
|
||||
export * from "./store"
|
||||
export * from "./product"
|
||||
export * from "./tax"
|
||||
export * from "./user"
|
||||
|
||||
2
packages/core-flows/src/payment/index.ts
Normal file
2
packages/core-flows/src/payment/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./steps"
|
||||
export * from "./workflows"
|
||||
23
packages/core-flows/src/payment/steps/capture-payment.ts
Normal file
23
packages/core-flows/src/payment/steps/capture-payment.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IPaymentModuleService } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
type StepInput = {
|
||||
payment_id: string
|
||||
captured_by?: string
|
||||
amount?: number
|
||||
}
|
||||
|
||||
export const capturePaymentStepId = "capture-payment-step"
|
||||
export const capturePaymentStep = createStep(
|
||||
capturePaymentStepId,
|
||||
async (input: StepInput, { container }) => {
|
||||
const paymentModule = container.resolve<IPaymentModuleService>(
|
||||
ModuleRegistrationName.PAYMENT
|
||||
)
|
||||
|
||||
const payment = await paymentModule.capturePayment(input)
|
||||
|
||||
return new StepResponse(payment)
|
||||
}
|
||||
)
|
||||
1
packages/core-flows/src/payment/steps/index.ts
Normal file
1
packages/core-flows/src/payment/steps/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./capture-payment"
|
||||
18
packages/core-flows/src/payment/workflows/capture-payment.ts
Normal file
18
packages/core-flows/src/payment/workflows/capture-payment.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { PaymentDTO } from "@medusajs/types"
|
||||
import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk"
|
||||
import { capturePaymentStep } from "../steps/capture-payment"
|
||||
|
||||
export const capturePaymentWorkflowId = "capture-payment-workflow"
|
||||
export const capturePaymentWorkflow = createWorkflow(
|
||||
capturePaymentWorkflowId,
|
||||
(
|
||||
input: WorkflowData<{
|
||||
payment_id: string
|
||||
captured_by?: string
|
||||
amount?: number
|
||||
}>
|
||||
): WorkflowData<PaymentDTO> => {
|
||||
const payment = capturePaymentStep(input)
|
||||
return payment
|
||||
}
|
||||
)
|
||||
1
packages/core-flows/src/payment/workflows/index.ts
Normal file
1
packages/core-flows/src/payment/workflows/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./capture-payment"
|
||||
@@ -0,0 +1,42 @@
|
||||
import { capturePaymentWorkflow } from "@medusajs/core-flows"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../../types/routing"
|
||||
import { defaultAdminPaymentFields } from "../../query-config"
|
||||
|
||||
export const POST = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const { id } = req.params
|
||||
|
||||
const { errors } = await capturePaymentWorkflow(req.scope).run({
|
||||
input: {
|
||||
payment_id: id,
|
||||
captured_by: req.auth?.actor_id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: Modules.PAYMENT,
|
||||
variables: { id },
|
||||
fields: defaultAdminPaymentFields,
|
||||
})
|
||||
|
||||
const [payment] = await remoteQuery(query)
|
||||
|
||||
res.status(200).json({ payment })
|
||||
}
|
||||
28
packages/medusa/src/api-v2/admin/payments/[id]/route.ts
Normal file
28
packages/medusa/src/api-v2/admin/payments/[id]/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../../types/routing"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const { id } = req.params
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: Modules.PAYMENT,
|
||||
variables: { id },
|
||||
fields: req.retrieveConfig.select as string[],
|
||||
})
|
||||
|
||||
const [payment] = await remoteQuery(query)
|
||||
|
||||
res.status(200).json({ payment })
|
||||
}
|
||||
44
packages/medusa/src/api-v2/admin/payments/middlewares.ts
Normal file
44
packages/medusa/src/api-v2/admin/payments/middlewares.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { transformQuery } from "../../../api/middlewares"
|
||||
import { MiddlewareRoute } from "../../../types/middlewares"
|
||||
import { authenticate } from "../../../utils/authenticate-middleware"
|
||||
import * as queryConfig from "./query-config"
|
||||
import { AdminGetPaymentsParams } from "./validators"
|
||||
|
||||
export const adminPaymentRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
{
|
||||
method: "ALL",
|
||||
matcher: "/admin/payments",
|
||||
middlewares: [authenticate("admin", ["session", "bearer"])],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/payments",
|
||||
middlewares: [
|
||||
transformQuery(
|
||||
AdminGetPaymentsParams,
|
||||
queryConfig.listTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["GET"],
|
||||
matcher: "/admin/payments/:id",
|
||||
middlewares: [
|
||||
transformQuery(
|
||||
AdminGetPaymentsParams,
|
||||
queryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/admin/payments/:id/capture",
|
||||
middlewares: [],
|
||||
},
|
||||
// TODO: Add in follow-up PR
|
||||
// {
|
||||
// method: ["POST"],
|
||||
// matcher: "/admin/payments/:id/refund",
|
||||
// middlewares: [],
|
||||
// },
|
||||
]
|
||||
25
packages/medusa/src/api-v2/admin/payments/query-config.ts
Normal file
25
packages/medusa/src/api-v2/admin/payments/query-config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const defaultAdminPaymentFields = [
|
||||
"id",
|
||||
"currency_code",
|
||||
"amount",
|
||||
"payment_collection_id",
|
||||
"payment_session_id",
|
||||
]
|
||||
|
||||
export const defaultAdminPaymentRelations = ["captures", "refunds"]
|
||||
|
||||
export const allowedAdminPaymentRelations = ["captures", "refunds"]
|
||||
|
||||
export const listTransformQueryConfig = {
|
||||
defaultFields: defaultAdminPaymentFields,
|
||||
defaultRelations: defaultAdminPaymentRelations,
|
||||
allowedRelations: allowedAdminPaymentRelations,
|
||||
isList: true,
|
||||
}
|
||||
|
||||
export const retrieveTransformQueryConfig = {
|
||||
defaultFields: defaultAdminPaymentFields,
|
||||
defaultRelations: defaultAdminPaymentRelations,
|
||||
allowedRelations: allowedAdminPaymentRelations,
|
||||
isList: false,
|
||||
}
|
||||
36
packages/medusa/src/api-v2/admin/payments/route.ts
Normal file
36
packages/medusa/src/api-v2/admin/payments/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
remoteQueryObjectFromString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
AuthenticatedMedusaRequest,
|
||||
MedusaResponse,
|
||||
} from "../../../types/routing"
|
||||
|
||||
export const GET = async (
|
||||
req: AuthenticatedMedusaRequest,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY)
|
||||
|
||||
const query = remoteQueryObjectFromString({
|
||||
entryPoint: Modules.PAYMENT,
|
||||
variables: {
|
||||
filters: req.filterableFields,
|
||||
order: req.listConfig.order,
|
||||
skip: req.listConfig.skip,
|
||||
take: req.listConfig.take,
|
||||
},
|
||||
fields: req.listConfig.select as string[],
|
||||
})
|
||||
|
||||
const { rows: payments, metadata } = await remoteQuery(query)
|
||||
|
||||
res.status(200).json({
|
||||
payments,
|
||||
count: metadata.count,
|
||||
offset: metadata.skip,
|
||||
limit: metadata.take,
|
||||
})
|
||||
}
|
||||
46
packages/medusa/src/api-v2/admin/payments/validators.ts
Normal file
46
packages/medusa/src/api-v2/admin/payments/validators.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Type } from "class-transformer"
|
||||
import { IsOptional, ValidateNested } from "class-validator"
|
||||
import {
|
||||
DateComparisonOperator,
|
||||
FindParams,
|
||||
extendedFindParamsMixin,
|
||||
} from "../../../types/common"
|
||||
import { IsType } from "../../../utils"
|
||||
|
||||
export class AdminGetPaymentsPaymentParams extends FindParams {}
|
||||
|
||||
export class AdminGetPaymentsParams extends extendedFindParamsMixin({
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
}) {
|
||||
/**
|
||||
* IDs to filter users by.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsType([String, [String]])
|
||||
id?: string | string[]
|
||||
|
||||
/**
|
||||
* Date filters to apply on the users' `update_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
updated_at?: DateComparisonOperator
|
||||
|
||||
/**
|
||||
* Date filters to apply on the customer users' `created_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
created_at?: DateComparisonOperator
|
||||
|
||||
/**
|
||||
* Date filters to apply on the users' `deleted_at` date.
|
||||
*/
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DateComparisonOperator)
|
||||
deleted_at?: DateComparisonOperator
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { adminCurrencyRoutesMiddlewares } from "./admin/currencies/middlewares"
|
||||
import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares"
|
||||
import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares"
|
||||
import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares"
|
||||
import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares"
|
||||
import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares"
|
||||
import { adminProductRoutesMiddlewares } from "./admin/products/middlewares"
|
||||
import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares"
|
||||
@@ -44,6 +45,7 @@ export const config: MiddlewaresConfig = {
|
||||
...adminCurrencyRoutesMiddlewares,
|
||||
...storeCurrencyRoutesMiddlewares,
|
||||
...adminProductRoutesMiddlewares,
|
||||
...adminPaymentRoutesMiddlewares,
|
||||
...adminPriceListsRoutesMiddlewares,
|
||||
],
|
||||
}
|
||||
|
||||
@@ -444,7 +444,6 @@ moduleIntegrationTestRunner({
|
||||
data: {},
|
||||
cart_id: null,
|
||||
order_id: null,
|
||||
order_edit_id: null,
|
||||
customer_id: null,
|
||||
deleted_at: null,
|
||||
captured_at: null,
|
||||
@@ -498,7 +497,7 @@ moduleIntegrationTestRunner({
|
||||
})
|
||||
|
||||
describe("capture", () => {
|
||||
it("should capture a payment successfully", async () => {
|
||||
it("should capture a payment successfully and update captured_at", async () => {
|
||||
const capturedPayment = await service.capturePayment({
|
||||
amount: 100,
|
||||
payment_id: "pay-id-1",
|
||||
@@ -515,61 +514,104 @@ moduleIntegrationTestRunner({
|
||||
amount: 100,
|
||||
}),
|
||||
],
|
||||
|
||||
// TODO: uncomment when totals calculations are implemented
|
||||
// captured_amount: 100,
|
||||
// captured_at: expect.any(Date),
|
||||
captured_at: expect.any(Date),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: uncomment when totals are implemented
|
||||
it("should split a payment in two captures a payment successfully", async () => {
|
||||
await service.capturePayment({
|
||||
amount: 50,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
|
||||
// it("should fail to capture amount greater than authorized", async () => {
|
||||
// const error = await service
|
||||
// .capturePayment({
|
||||
// amount: 200,
|
||||
// payment_id: "pay-id-1",
|
||||
// })
|
||||
// .catch((e) => e)
|
||||
//
|
||||
// expect(error.message).toEqual(
|
||||
// "Total captured amount for payment: pay-id-1 exceeds authorised amount."
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// it("should fail to capture already captured payment", async () => {
|
||||
// await service.capturePayment({
|
||||
// amount: 100,
|
||||
// payment_id: "pay-id-1",
|
||||
// })
|
||||
//
|
||||
// const error = await service
|
||||
// .capturePayment({
|
||||
// amount: 100,
|
||||
// payment_id: "pay-id-1",
|
||||
// })
|
||||
// .catch((e) => e)
|
||||
//
|
||||
// expect(error.message).toEqual(
|
||||
// "The payment: pay-id-1 is already fully captured."
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// it("should fail to capture a canceled payment", async () => {
|
||||
// await service.cancelPayment("pay-id-1")
|
||||
//
|
||||
// const error = await service
|
||||
// .capturePayment({
|
||||
// amount: 100,
|
||||
// payment_id: "pay-id-1",
|
||||
// })
|
||||
// .catch((e) => e)
|
||||
//
|
||||
// expect(error.message).toEqual(
|
||||
// "The payment: pay-id-1 has been canceled."
|
||||
// )
|
||||
// })
|
||||
const capturedPayment = await service.capturePayment({
|
||||
amount: 50,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
|
||||
expect(capturedPayment).toEqual(
|
||||
expect.objectContaining({
|
||||
id: "pay-id-1",
|
||||
amount: 100,
|
||||
|
||||
captures: [
|
||||
expect.objectContaining({
|
||||
created_by: null,
|
||||
amount: 50,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
created_by: null,
|
||||
amount: 50,
|
||||
}),
|
||||
],
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to capture amount greater than authorized", async () => {
|
||||
const error = await service
|
||||
.capturePayment({
|
||||
amount: 200,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"You cannot capture more than the authorized amount substracted by what is already captured."
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to capture amount greater than what is already captured", async () => {
|
||||
await service.capturePayment({
|
||||
amount: 99,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
|
||||
const error = await service
|
||||
.capturePayment({
|
||||
amount: 2,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"You cannot capture more than the authorized amount substracted by what is already captured."
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to capture already captured payment", async () => {
|
||||
await service.capturePayment({
|
||||
amount: 100,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
|
||||
const error = await service
|
||||
.capturePayment({
|
||||
amount: 100,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"You cannot capture more than the authorized amount substracted by what is already captured."
|
||||
)
|
||||
})
|
||||
|
||||
it("should fail to capture a canceled payment", async () => {
|
||||
await service.cancelPayment("pay-id-1")
|
||||
|
||||
const error = await service
|
||||
.capturePayment({
|
||||
amount: 100,
|
||||
payment_id: "pay-id-1",
|
||||
})
|
||||
.catch((e) => e)
|
||||
|
||||
expect(error.message).toEqual(
|
||||
"The payment: pay-id-1 has been canceled."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("refund", () => {
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
"@mikro-orm/migrations": "5.9.7",
|
||||
"@mikro-orm/postgresql": "5.9.7",
|
||||
"awilix": "^8.0.0",
|
||||
"bignumber.js": "^9.1.2",
|
||||
"dotenv": "^16.1.4",
|
||||
"knex": "2.4.2"
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export class Migration20240225134525 extends Migration {
|
||||
const paymentCollectionExists = await this.execute(
|
||||
`SELECT * FROM information_schema.tables where table_name = 'payment_collection' and table_schema = 'public';`
|
||||
)
|
||||
|
||||
|
||||
if (paymentCollectionExists.length) {
|
||||
this.addSql(`
|
||||
${generatePostgresAlterColummnIfExistStatement(
|
||||
@@ -14,10 +14,11 @@ export class Migration20240225134525 extends Migration {
|
||||
["type", "created_by"],
|
||||
"DROP NOT NULL"
|
||||
)}
|
||||
|
||||
|
||||
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "completed_at" TIMESTAMPTZ NULL;
|
||||
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
||||
ALTER TABLE IF EXISTS "payment_collection" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
||||
ALTER TABLE "payment_collection" DROP CONSTRAINT "FK_payment_collection_region_id";
|
||||
|
||||
ALTER TABLE IF EXISTS "payment_provider" ADD COLUMN IF NOT EXISTS "is_enabled" BOOLEAN NOT NULL DEFAULT TRUE;
|
||||
|
||||
@@ -34,6 +35,7 @@ export class Migration20240225134525 extends Migration {
|
||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "payment_session_id" TEXT NOT NULL;
|
||||
ALTER TABLE IF EXISTS "payment" ADD COLUMN IF NOT EXISTS "customer_id" TEXT NULL;
|
||||
|
||||
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "raw_amount" JSONB NOT NULL;
|
||||
ALTER TABLE IF EXISTS "refund" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMPTZ NULL;
|
||||
@@ -101,6 +103,7 @@ export class Migration20240225134525 extends Migration {
|
||||
CREATE INDEX IF NOT EXISTS "IDX_capture_deleted_at" ON "payment" ("deleted_at") WHERE "deleted_at" IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "IDX_payment_session_payment_collection_id" ON "payment_session" ("payment_collection_id") WHERE "deleted_at" IS NULL;
|
||||
|
||||
`)
|
||||
} else {
|
||||
this.addSql(`
|
||||
@@ -170,7 +173,6 @@ export class Migration20240225134525 extends Migration {
|
||||
"provider_id" TEXT NOT NULL,
|
||||
"cart_id" TEXT NULL,
|
||||
"order_id" TEXT NULL,
|
||||
"order_edit_id" TEXT NULL,
|
||||
"customer_id" TEXT NULL,
|
||||
"data" JSONB NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
@@ -52,9 +52,6 @@ export default class Payment {
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
order_id: string | null = null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
order_edit_id: string | null = null
|
||||
|
||||
@Property({ columnType: "text", nullable: true })
|
||||
customer_id: string | null = null
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
PaymentSession,
|
||||
Refund,
|
||||
} from "@models"
|
||||
import BigNumber from "bignumber.js"
|
||||
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
|
||||
@@ -51,7 +52,13 @@ type InjectedDependencies = {
|
||||
paymentProviderService: PaymentProviderService
|
||||
}
|
||||
|
||||
const generateMethodForModels = [PaymentCollection, Payment, PaymentSession]
|
||||
const generateMethodForModels = [
|
||||
PaymentCollection,
|
||||
Payment,
|
||||
PaymentSession,
|
||||
Capture,
|
||||
Refund,
|
||||
]
|
||||
|
||||
export default class PaymentModuleService<
|
||||
TPaymentCollection extends PaymentCollection = PaymentCollection,
|
||||
@@ -409,10 +416,25 @@ export default class PaymentModuleService<
|
||||
): Promise<PaymentDTO> {
|
||||
const payment = await this.paymentService_.retrieve(
|
||||
data.payment_id,
|
||||
{ select: ["id", "data", "provider_id"] },
|
||||
{
|
||||
select: [
|
||||
"id",
|
||||
"data",
|
||||
"provider_id",
|
||||
"amount",
|
||||
"raw_amount",
|
||||
"canceled_at",
|
||||
],
|
||||
relations: ["captures.raw_amount"],
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// If no custom amount is passed, we assume the full amount needs to be captured
|
||||
if (!data.amount) {
|
||||
data.amount = payment.amount as number
|
||||
}
|
||||
|
||||
if (payment.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
@@ -420,22 +442,29 @@ export default class PaymentModuleService<
|
||||
)
|
||||
}
|
||||
|
||||
// this method needs to be idempotent
|
||||
if (payment.captured_at) {
|
||||
return this.retrievePayment(
|
||||
return await this.retrievePayment(
|
||||
data.payment_id,
|
||||
{ relations: ["captures"] },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
|
||||
// if (payment.captured_amount + input.amount > payment.amount) {
|
||||
// throw new MedusaError(
|
||||
// MedusaError.Types.INVALID_DATA,
|
||||
// `Total captured amount for payment: ${payment.id} exceeds authorized amount.`
|
||||
// )
|
||||
// }
|
||||
const capturedAmount = payment.captures.reduce((acc, next) => {
|
||||
const bn = new BigNumber(next.raw_amount.value)
|
||||
return acc.plus(bn)
|
||||
}, BigNumber(0))
|
||||
|
||||
const authorizedAmount = BigNumber(payment.raw_amount.value)
|
||||
const newCaptureAmount = BigNumber(data.amount)
|
||||
const remainingToCapture = authorizedAmount.minus(capturedAmount)
|
||||
|
||||
if (newCaptureAmount.gt(remainingToCapture)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`You cannot capture more than the authorized amount substracted by what is already captured.`
|
||||
)
|
||||
}
|
||||
|
||||
const paymentData = await this.paymentProviderService_.capturePayment({
|
||||
data: payment.data!,
|
||||
@@ -456,13 +485,13 @@ export default class PaymentModuleService<
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// TODO: revisit when https://github.com/medusajs/medusa/pull/6253 is merged
|
||||
// if (payment.captured_amount + data.amount === payment.amount) {
|
||||
// await this.paymentService_.update(
|
||||
// { id: payment.id, captured_at: new Date() },
|
||||
// sharedContext
|
||||
// )
|
||||
// }
|
||||
// When the entire authorized amount has been captured, we mark it fully capture by setting the captured_at field
|
||||
if (capturedAmount.plus(newCaptureAmount).eq(authorizedAmount)) {
|
||||
await this.paymentService_.update(
|
||||
{ id: payment.id, captured_at: new Date() },
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
return await this.retrievePayment(
|
||||
payment.id,
|
||||
@@ -495,7 +524,7 @@ export default class PaymentModuleService<
|
||||
data: payment.data!,
|
||||
provider_id: payment.provider_id,
|
||||
},
|
||||
data.amount
|
||||
data.amount as number
|
||||
)
|
||||
|
||||
await this.refundService_.create(
|
||||
|
||||
@@ -182,6 +182,26 @@ export interface FilterablePaymentSessionProps
|
||||
deleted_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
export interface FilterableCaptureProps extends BaseFilterable<CaptureDTO> {
|
||||
id?: string | string[]
|
||||
currency_code?: string | string[]
|
||||
amount?: number | OperatorMap<number>
|
||||
payment_id?: string | string[]
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
deleted_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
export interface FilterableRefundProps extends BaseFilterable<RefundDTO> {
|
||||
id?: string | string[]
|
||||
currency_code?: string | string[]
|
||||
amount?: number | OperatorMap<number>
|
||||
payment_id?: string | string[]
|
||||
created_at?: OperatorMap<string>
|
||||
updated_at?: OperatorMap<string>
|
||||
deleted_at?: OperatorMap<string>
|
||||
}
|
||||
|
||||
/* ********** PAYMENT ********** */
|
||||
export interface PaymentDTO {
|
||||
/**
|
||||
|
||||
@@ -144,7 +144,7 @@ export interface CreateCaptureDTO {
|
||||
/**
|
||||
* The amount of the capture.
|
||||
*/
|
||||
amount: number
|
||||
amount?: number
|
||||
|
||||
/**
|
||||
* The associated payment's ID.
|
||||
@@ -164,7 +164,7 @@ export interface CreateRefundDTO {
|
||||
/**
|
||||
* The amount of the refund.
|
||||
*/
|
||||
amount: number
|
||||
amount?: number
|
||||
|
||||
/**
|
||||
* The associated payment's ID.
|
||||
|
||||
@@ -2,14 +2,18 @@ import { FindConfig } from "../common"
|
||||
import { IModuleService } from "../modules-sdk"
|
||||
import { Context } from "../shared-context"
|
||||
import {
|
||||
CaptureDTO,
|
||||
FilterableCaptureProps,
|
||||
FilterablePaymentCollectionProps,
|
||||
FilterablePaymentProps,
|
||||
FilterablePaymentProviderProps,
|
||||
FilterablePaymentSessionProps,
|
||||
FilterableRefundProps,
|
||||
PaymentCollectionDTO,
|
||||
PaymentDTO,
|
||||
PaymentProviderDTO,
|
||||
PaymentSessionDTO,
|
||||
RefundDTO,
|
||||
} from "./common"
|
||||
import {
|
||||
CreateCaptureDTO,
|
||||
@@ -402,6 +406,18 @@ export interface IPaymentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): Promise<PaymentProviderDTO[]>
|
||||
|
||||
listCaptures(
|
||||
filters?: FilterableCaptureProps,
|
||||
config?: FindConfig<CaptureDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<CaptureDTO[]>
|
||||
|
||||
listRefunds(
|
||||
filters?: FilterableRefundProps,
|
||||
config?: FindConfig<RefundDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<RefundDTO[]>
|
||||
|
||||
/* ********** HOOKS ********** */
|
||||
|
||||
processEvent(data: ProviderWebhookPayload): Promise<void>
|
||||
|
||||
Reference in New Issue
Block a user