diff --git a/integration-tests/http/__tests__/payment/admin/payment.spec.ts b/integration-tests/http/__tests__/payment/admin/payment.spec.ts index 9de3d36fb0a47..f672f6ec6ab97 100644 --- a/integration-tests/http/__tests__/payment/admin/payment.spec.ts +++ b/integration-tests/http/__tests__/payment/admin/payment.spec.ts @@ -127,14 +127,17 @@ medusaIntegrationTestRunner({ adminHeaders ) - // refund + 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, - // BREAKING: We should probably introduce reason and notes in V2 too - // reason: "return", - // note: "Do not like it", + refund_reason_id: refundReason.id, + note: "Do not like it", }, adminHeaders ) @@ -155,6 +158,11 @@ medusaIntegrationTestRunner({ 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, diff --git a/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts b/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts new file mode 100644 index 0000000000000..809fd141367af --- /dev/null +++ b/integration-tests/http/__tests__/refund-reason/refund-reason.spec.ts @@ -0,0 +1,155 @@ +import { medusaIntegrationTestRunner } from "medusa-test-utils" +import { + adminHeaders, + createAdminUser, +} from "../../../helpers/create-admin-user" + +jest.setTimeout(30000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, api, getContainer }) => { + let refundReason1 + let refundReason2 + + beforeEach(async () => { + const appContainer = getContainer() + await createAdminUser(dbConnection, adminHeaders, appContainer) + + refundReason1 = ( + await api.post( + "/admin/refund-reasons", + { label: "reason 1 - too big" }, + adminHeaders + ) + ).data.refund_reason + + refundReason2 = ( + await api.post( + "/admin/refund-reasons", + { label: "reason 2 - too small" }, + adminHeaders + ) + ).data.refund_reason + }) + + describe("GET /admin/refund-reasons", () => { + it("should list refund reasons and query count", async () => { + const response = await api + .get("/admin/refund-reasons", adminHeaders) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(2) + expect(response.data.refund_reasons).toEqual([ + expect.objectContaining({ + label: "reason 1 - too big", + }), + expect.objectContaining({ + label: "reason 2 - too small", + }), + ]) + }) + + it("should list refund-reasons with specific query", async () => { + const response = await api.get( + "/admin/refund-reasons?q=1", + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.refund_reasons).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + label: "reason 1 - too big", + }), + ]) + ) + }) + }) + + describe("POST /admin/refund-reasons", () => { + it("should create a refund reason", async () => { + const response = await api.post( + "/admin/refund-reasons", + { + label: "reason test", + description: "test description", + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.refund_reason).toEqual( + expect.objectContaining({ + label: "reason test", + description: "test description", + }) + ) + }) + }) + + describe("POST /admin/refund-reasons/:id", () => { + it("should correctly update refund reason", async () => { + const response = await api.post( + `/admin/refund-reasons/${refundReason1.id}`, + { + label: "reason test", + description: "test description", + }, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.refund_reason).toEqual( + expect.objectContaining({ + label: "reason test", + description: "test description", + }) + ) + }) + }) + + describe("GET /admin/refund-reasons/:id", () => { + it("should fetch a refund reason", async () => { + const response = await api.get( + `/admin/refund-reasons/${refundReason1.id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.refund_reason).toEqual( + expect.objectContaining({ + id: refundReason1.id, + }) + ) + }) + }) + + describe("DELETE /admin/refund-reasons/:id", () => { + it("should remove refund reasons", async () => { + const deleteResponse = await api.delete( + `/admin/refund-reasons/${refundReason1.id}`, + adminHeaders + ) + + expect(deleteResponse.data).toEqual({ + id: refundReason1.id, + object: "refund_reason", + deleted: true, + }) + + await api + .get(`/admin/refund-reasons/${refundReason1.id}`, adminHeaders) + .catch((error) => { + expect(error.response.data.type).toEqual("not_found") + expect(error.response.data.message).toEqual( + `Refund reason with id: ${refundReason1.id.id} not found` + ) + }) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/payment-collection/steps/create-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/steps/create-refund-reasons.ts new file mode 100644 index 0000000000000..2e38cce2cb2e8 --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/steps/create-refund-reasons.ts @@ -0,0 +1,31 @@ +import { CreateRefundReasonDTO, IPaymentModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const createRefundReasonStepId = "create-refund-reason" +export const createRefundReasonStep = createStep( + createRefundReasonStepId, + async (data: CreateRefundReasonDTO[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + const refundReasons = await service.createRefundReasons(data) + + return new StepResponse( + refundReasons, + refundReasons.map((rr) => rr.id) + ) + }, + async (ids, { container }) => { + if (!ids?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + await service.deleteRefundReasons(ids) + } +) diff --git a/packages/core/core-flows/src/payment-collection/steps/delete-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/steps/delete-refund-reasons.ts new file mode 100644 index 0000000000000..304629affe4d0 --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/steps/delete-refund-reasons.ts @@ -0,0 +1,28 @@ +import { IPaymentModuleService } from "@medusajs/types" +import { ModuleRegistrationName } from "@medusajs/utils" +import { createStep, StepResponse } from "@medusajs/workflows-sdk" + +export const deleteRefundReasonsStepId = "delete-refund-reasons" +export const deleteRefundReasonsStep = createStep( + deleteRefundReasonsStepId, + async (ids: string[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + await service.softDeleteRefundReasons(ids) + + return new StepResponse(void 0, ids) + }, + async (prevCustomerIds, { container }) => { + if (!prevCustomerIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + await service.restoreRefundReasons(prevCustomerIds) + } +) diff --git a/packages/core/core-flows/src/payment-collection/steps/index.ts b/packages/core/core-flows/src/payment-collection/steps/index.ts index 08a606113971a..8e1be26418eda 100644 --- a/packages/core/core-flows/src/payment-collection/steps/index.ts +++ b/packages/core/core-flows/src/payment-collection/steps/index.ts @@ -1,4 +1,7 @@ export * from "./create-payment-session" +export * from "./create-refund-reasons" export * from "./delete-payment-sessions" +export * from "./delete-refund-reasons" export * from "./update-payment-collection" +export * from "./update-refund-reasons" export * from "./validate-deleted-payment-sessions" diff --git a/packages/core/core-flows/src/payment-collection/steps/update-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/steps/update-refund-reasons.ts new file mode 100644 index 0000000000000..be3ff953e7ff0 --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/steps/update-refund-reasons.ts @@ -0,0 +1,43 @@ +import { IPaymentModuleService, UpdateRefundReasonDTO } from "@medusajs/types" +import { + ModuleRegistrationName, + getSelectsAndRelationsFromObjectArray, + promiseAll, +} from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +export const updateRefundReasonStepId = "update-refund-reasons" +export const updateRefundReasonsStep = createStep( + updateRefundReasonStepId, + async (data: UpdateRefundReasonDTO[], { container }) => { + const ids = data.map((d) => d.id) + const { selects, relations } = getSelectsAndRelationsFromObjectArray(data) + const service = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + const prevRefundReasons = await service.listRefundReasons( + { id: ids }, + { select: selects, relations } + ) + + const reasons = await service.updateRefundReasons(data) + + return new StepResponse(reasons, prevRefundReasons) + }, + async (previousData, { container }) => { + if (!previousData) { + return + } + + const service = container.resolve( + ModuleRegistrationName.PAYMENT + ) + + await promiseAll( + previousData.map((refundReason) => + service.updateRefundReasons(refundReason) + ) + ) + } +) diff --git a/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts new file mode 100644 index 0000000000000..cdc0b8347225d --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/workflows/create-refund-reasons.ts @@ -0,0 +1,17 @@ +import { CreateRefundReasonDTO, RefundReasonDTO } from "@medusajs/types" +import { + WorkflowData, + WorkflowResponse, + createWorkflow, +} from "@medusajs/workflows-sdk" +import { createRefundReasonStep } from "../steps/create-refund-reasons" + +export const createRefundReasonsWorkflowId = "create-refund-reasons-workflow" +export const createRefundReasonsWorkflow = createWorkflow( + createRefundReasonsWorkflowId, + ( + input: WorkflowData<{ data: CreateRefundReasonDTO[] }> + ): WorkflowResponse => { + return new WorkflowResponse(createRefundReasonStep(input.data)) + } +) diff --git a/packages/core/core-flows/src/payment-collection/workflows/delete-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/workflows/delete-refund-reasons.ts new file mode 100644 index 0000000000000..59d5d410acd9b --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/workflows/delete-refund-reasons.ts @@ -0,0 +1,14 @@ +import { + WorkflowData, + WorkflowResponse, + createWorkflow, +} from "@medusajs/workflows-sdk" +import { deleteRefundReasonsStep } from "../steps" + +export const deleteRefundReasonsWorkflowId = "delete-refund-reasons-workflow" +export const deleteRefundReasonsWorkflow = createWorkflow( + deleteRefundReasonsWorkflowId, + (input: WorkflowData<{ ids: string[] }>): WorkflowResponse => { + return new WorkflowResponse(deleteRefundReasonsStep(input.ids)) + } +) diff --git a/packages/core/core-flows/src/payment-collection/workflows/index.ts b/packages/core/core-flows/src/payment-collection/workflows/index.ts index 2dd6e79700df2..ad02d0f72d825 100644 --- a/packages/core/core-flows/src/payment-collection/workflows/index.ts +++ b/packages/core/core-flows/src/payment-collection/workflows/index.ts @@ -1 +1,3 @@ export * from "./create-payment-session" +export * from "./create-refund-reasons" +export * from "./update-refund-reasons" diff --git a/packages/core/core-flows/src/payment-collection/workflows/update-refund-reasons.ts b/packages/core/core-flows/src/payment-collection/workflows/update-refund-reasons.ts new file mode 100644 index 0000000000000..3df098a5cde7c --- /dev/null +++ b/packages/core/core-flows/src/payment-collection/workflows/update-refund-reasons.ts @@ -0,0 +1,17 @@ +import { RefundReasonDTO, UpdateRefundReasonDTO } from "@medusajs/types" +import { + WorkflowData, + WorkflowResponse, + createWorkflow, +} from "@medusajs/workflows-sdk" +import { updateRefundReasonsStep } from "../steps" + +export const updateRefundReasonsWorkflowId = "update-refund-reasons" +export const updateRefundReasonsWorkflow = createWorkflow( + updateRefundReasonsWorkflowId, + ( + input: WorkflowData + ): WorkflowResponse => { + return new WorkflowResponse(updateRefundReasonsStep(input)) + } +) diff --git a/packages/core/types/src/http/payment/admin.ts b/packages/core/types/src/http/payment/admin.ts index a7fa66741187b..e0876cbc35002 100644 --- a/packages/core/types/src/http/payment/admin.ts +++ b/packages/core/types/src/http/payment/admin.ts @@ -1,3 +1,4 @@ +import { BaseFilterable } from "../../dal" import { BasePayment, BasePaymentCollection, @@ -7,6 +8,7 @@ import { BasePaymentProviderFilters, BasePaymentSession, BasePaymentSessionFilters, + RefundReason, } from "./common" export interface AdminPaymentProvider extends BasePaymentProvider { @@ -42,3 +44,23 @@ export interface AdminPaymentsResponse { } export interface AdminPaymentFilters extends BasePaymentFilters {} + +// Refund reason + +export interface AdminRefundReason extends RefundReason {} +export interface RefundReasonFilters extends BaseFilterable { + id?: string | string[] +} + +export interface RefundReasonResponse { + refund_reason: AdminRefundReason +} + +export interface RefundReasonsResponse { + refund_reasons: AdminRefundReason[] +} + +export interface AdminCreateRefundReason { + label: string + description?: string +} diff --git a/packages/core/types/src/http/payment/common.ts b/packages/core/types/src/http/payment/common.ts index e5172bad9bde8..94eb49b8b8baf 100644 --- a/packages/core/types/src/http/payment/common.ts +++ b/packages/core/types/src/http/payment/common.ts @@ -262,6 +262,21 @@ export interface BaseRefund { */ amount: BigNumberValue + /** + * The id of the refund_reason that is associated with the refund + */ + refund_reason_id?: string | null + + /** + * The id of the refund_reason that is associated with the refund + */ + refund_reason?: RefundReason | null + + /** + * A field to add some additional information about the refund + */ + note?: string | null + /** * The creation date of the refund. */ @@ -338,6 +353,33 @@ export interface BasePaymentSession { payment?: BasePayment } +export interface RefundReason { + /** + * The ID of the refund reason + */ + id: string + /** + * The label of the refund reason + */ + label: string + /** + * The description of the refund reason + */ + description?: string | null + /** + * The metadata of the refund reason + */ + metadata: Record | null + /** + * When the refund reason was created + */ + created_at: Date | string + /** + * When the refund reason was updated + */ + updated_at: Date | string +} + /** * The filters to apply on the retrieved payment collection. */ diff --git a/packages/core/types/src/payment/common.ts b/packages/core/types/src/payment/common.ts index 2dbc248c36862..f0eaa7d786c79 100644 --- a/packages/core/types/src/payment/common.ts +++ b/packages/core/types/src/payment/common.ts @@ -477,6 +477,21 @@ export interface RefundDTO { */ amount: BigNumberValue + /** + * The id of the refund_reason that is associated with the refund + */ + refund_reason_id?: string | null + + /** + * The id of the refund_reason that is associated with the refund + */ + refund_reason?: RefundReasonDTO | null + + /** + * A field to add some additional information about the refund + */ + note?: string | null + /** * The creation date of the refund. */ @@ -583,3 +598,38 @@ export interface FilterablePaymentProviderProps */ is_enabled?: boolean } + +export interface FilterableRefundReasonProps + extends BaseFilterable { + /** + * The IDs to filter the refund reasons by. + */ + id?: string | string[] +} + +export interface RefundReasonDTO { + /** + * The ID of the refund reason + */ + id: string + /** + * The label of the refund reason + */ + label: string + /** + * The description of the refund reason + */ + description?: string | null + /** + * The metadata of the refund reason + */ + metadata: Record | null + /** + * When the refund reason was created + */ + created_at: Date | string + /** + * When the refund reason was updated + */ + updated_at: Date | string +} diff --git a/packages/core/types/src/payment/mutations.ts b/packages/core/types/src/payment/mutations.ts index bfc0e7a1e257c..74fe1e799e3d9 100644 --- a/packages/core/types/src/payment/mutations.ts +++ b/packages/core/types/src/payment/mutations.ts @@ -212,6 +212,16 @@ export interface CreateRefundDTO { */ payment_id: string + /** + * The associated refund reason's ID. + */ + refund_reason_id?: string | null + + /** + * A text field that adds some information about the refund + */ + note?: string + /** * Who refunded the payment. For example, * a user's ID. @@ -323,3 +333,37 @@ export interface ProviderWebhookPayload { headers: Record } } + +export interface CreateRefundReasonDTO { + /** + * The label of the refund reason + */ + label: string + /** + * The description of the refund reason + */ + description?: string | null + /** + * The metadata of the refund reason + */ + metadata?: Record | null +} + +export interface UpdateRefundReasonDTO { + /** + * The id of the refund reason + */ + id: string + /** + * The label of the refund reason + */ + label?: string + /** + * The description of the refund reason + */ + description?: string | null + /** + * The metadata of the refund reason + */ + metadata?: Record | null +} diff --git a/packages/core/types/src/payment/service.ts b/packages/core/types/src/payment/service.ts index a3413e7bab577..c3c25fc29ef9b 100644 --- a/packages/core/types/src/payment/service.ts +++ b/packages/core/types/src/payment/service.ts @@ -1,4 +1,5 @@ import { FindConfig } from "../common" +import { RestoreReturn, SoftDeleteReturn } from "../dal" import { IModuleService } from "../modules-sdk" import { Context } from "../shared-context" import { @@ -9,21 +10,25 @@ import { FilterablePaymentProviderProps, FilterablePaymentSessionProps, FilterableRefundProps, + FilterableRefundReasonProps, PaymentCollectionDTO, PaymentDTO, PaymentProviderDTO, PaymentSessionDTO, RefundDTO, + RefundReasonDTO, } from "./common" import { CreateCaptureDTO, CreatePaymentCollectionDTO, CreatePaymentSessionDTO, CreateRefundDTO, + CreateRefundReasonDTO, PaymentCollectionUpdatableFields, ProviderWebhookPayload, UpdatePaymentDTO, UpdatePaymentSessionDTO, + UpdateRefundReasonDTO, UpsertPaymentCollectionDTO, } from "./mutations" @@ -817,6 +822,199 @@ export interface IPaymentModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method creates refund reasons. + * + * @param {CreateRefundReasonDTO[]} data - The refund reasons to create. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created refund reasons. + * + * @example + * const refundReasons = + * await paymentModuleService.createRefundReasons([ + * { + * label: "Too big", + * }, + * { + * label: "Too big", + * }, + * ]) + */ + createRefundReasons( + data: CreateRefundReasonDTO[], + sharedContext?: Context + ): Promise + + /** + * This method creates a refund reason. + * + * @param {CreateRefundReasonDTO} data - The refund reason to create. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The created refund reason. + * + * @example + * const refundReason = + * await paymentModuleService.createRefundReasons({ + * label: "Too big", + * }) + */ + createRefundReasons( + data: CreateRefundReasonDTO, + sharedContext?: Context + ): Promise + + /** + * This method deletes a refund reason by its ID. + * + * @param {string[]} refundReasonId - The refund reason's ID. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the refund reason is deleted successfully. + * + * @example + * await paymentModuleService.deleteRefundReasons([ + * "refr_123", + * "refr_321", + * ]) + */ + deleteRefundReasons( + refundReasonId: string[], + sharedContext?: Context + ): Promise + + /** + * This method deletes a refund reason by its ID. + * + * @param {string} refundReasonId - The refund reason's ID. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} Resolves when the refund reason is deleted successfully. + * + * @example + * await paymentModuleService.deleteRefundReasons( + * "refr_123" + * ) + */ + deleteRefundReasons( + refundReasonId: string, + sharedContext?: Context + ): Promise + + /** + * This method soft deletes refund reasons by their IDs. + * + * @param {string[]} refundReasonId - The IDs of refund reasons. + * @param {SoftDeleteReturn} config - An object that is used to specify an entity's related entities that should be soft-deleted when the main entity is soft-deleted. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise>} An object that includes the IDs of related records that were also soft deleted. + * If there are no related records, the promise resolves to `void`. + * + * @example + * await paymentModule.softDeleteRefundReasons(["cus_123"]) + */ + softDeleteRefundReasons( + refundReasonId: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method restores soft deleted refund reason by their IDs. + * + * @param {string[]} refundReasonId - The IDs of refund reasons. + * @param {RestoreReturn} config - Configurations determining which relations to restore along with each of the refund reason. You can pass to its `returnLinkableKeys` + * property any of the refund reason's relation attribute names. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise>} An object that includes the IDs of related records that were restored. + * If there are no related records restored, the promise resolves to `void`. + * + * @example + * await paymentModule.restoreRefundReasons(["cus_123"]) + */ + restoreRefundReasons( + refundReasonId: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> + + /** + * This method updates an existing refund reason. + * + * @param {UpdateRefundReasonDTO} data - The attributes to update in the refund reason. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated refund reason. + * + * @example + * const refundReason = + * await paymentModuleService.updateRefundReasons( + * [{ + * id: "refr_test1", + * amount: 3000, + * }] + * ) + */ + updateRefundReasons( + data: UpdateRefundReasonDTO[], + sharedContext?: Context + ): Promise + + updateRefundReasons( + data: UpdateRefundReasonDTO, + sharedContext?: Context + ): Promise + + /** + * This method retrieves a paginated list of refund reasons based on optional filters and configuration. + * + * @param {FilterableRefundReasonProps} filters - The filters to apply on the retrieved refund reason. + * @param {FindConfig} config - The configurations determining how the refund reason is retrieved. Its properties, such as `select` or `relations`, accept the + * attributes or relations associated with a refund reason. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The list of refund reasons. + * + * @example + * To retrieve a list of refund reasons using their IDs: + * + * ```ts + * const refundReasons = + * await paymentModuleService.listRefundReasons({ + * id: ["refr_123", "refr_321"], + * }) + * ``` + * + * To specify relations that should be retrieved within the refund : + * + * ```ts + * const refundReasons = + * await paymentModuleService.listRefundReasons( + * { + * id: ["refr_123", "refr_321"], + * }, + * {} + * ) + * ``` + * + * By default, only the first `15` records are retrieved. You can control pagination by specifying the `skip` and `take` properties of the `config` parameter: + * + * ```ts + * const refundReasons = + * await paymentModuleService.listRefundReasons( + * { + * id: ["refr_123", "refr_321"], + * }, + * { + * take: 20, + * skip: 2, + * } + * ) + * ``` + * + * + */ + listRefundReasons( + filters?: FilterableRefundReasonProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + /* ********** HOOKS ********** */ /** diff --git a/packages/medusa/src/api/admin/payments/[id]/refund/route.ts b/packages/medusa/src/api/admin/payments/[id]/refund/route.ts index 8dfa60d83e665..cdba9da3c143c 100644 --- a/packages/medusa/src/api/admin/payments/[id]/refund/route.ts +++ b/packages/medusa/src/api/admin/payments/[id]/refund/route.ts @@ -15,7 +15,7 @@ export const POST = async ( input: { payment_id: id, created_by: req.auth_context.actor_id, - amount: req.validatedBody.amount, + ...req.validatedBody, }, }) diff --git a/packages/medusa/src/api/admin/payments/query-config.ts b/packages/medusa/src/api/admin/payments/query-config.ts index 1a66f4a9a08e0..36fbe40f7393d 100644 --- a/packages/medusa/src/api/admin/payments/query-config.ts +++ b/packages/medusa/src/api/admin/payments/query-config.ts @@ -9,7 +9,9 @@ export const defaultAdminPaymentFields = [ "captures.amount", "refunds.id", "refunds.amount", + "refunds.note", "refunds.payment_id", + "refunds.refund_reason.label", ] export const listTransformQueryConfig = { diff --git a/packages/medusa/src/api/admin/payments/validators.ts b/packages/medusa/src/api/admin/payments/validators.ts index 7def18ce3124e..fead7ea939de6 100644 --- a/packages/medusa/src/api/admin/payments/validators.ts +++ b/packages/medusa/src/api/admin/payments/validators.ts @@ -55,5 +55,17 @@ export type AdminCreatePaymentRefundType = z.infer< export const AdminCreatePaymentRefund = z .object({ amount: z.number().optional(), + refund_reason_id: z.string().optional(), + note: z.string().optional(), + }) + .strict() + +export type AdminCreatePaymentRefundReasonType = z.infer< + typeof AdminCreatePaymentRefundReason +> +export const AdminCreatePaymentRefundReason = z + .object({ + label: z.string(), + description: z.string().optional(), }) .strict() diff --git a/packages/medusa/src/api/admin/refund-reasons/[id]/route.ts b/packages/medusa/src/api/admin/refund-reasons/[id]/route.ts new file mode 100644 index 0000000000000..ac9c75f2e7f51 --- /dev/null +++ b/packages/medusa/src/api/admin/refund-reasons/[id]/route.ts @@ -0,0 +1,66 @@ +import { + deleteReturnReasonsWorkflow, + updateRefundReasonsWorkflow, +} from "@medusajs/core-flows" +import { RefundReasonResponse } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { refetchEntity } from "../../../utils/refetch-entity" +import { AdminUpdatePaymentRefundReasonType } from "../validators" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const refund_reason = await refetchEntity( + "refund_reason", + req.params.id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.json({ refund_reason }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + await updateRefundReasonsWorkflow(req.scope).run({ + input: [ + { + ...req.validatedBody, + id, + }, + ], + }) + + const refund_reason = await refetchEntity( + "refund_reason", + req.params.id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.json({ refund_reason }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + const input = { ids: [id] } + + await deleteReturnReasonsWorkflow(req.scope).run({ input }) + + res.json({ + id, + object: "refund_reason", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/admin/refund-reasons/middlewares.ts b/packages/medusa/src/api/admin/refund-reasons/middlewares.ts new file mode 100644 index 0000000000000..ebc49b9243ac6 --- /dev/null +++ b/packages/medusa/src/api/admin/refund-reasons/middlewares.ts @@ -0,0 +1,64 @@ +import { MiddlewareRoute } from "@medusajs/framework" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" +import * as queryConfig from "./query-config" +import { + AdminCreatePaymentRefundReason, + AdminGetRefundReasonsParams, + AdminUpdatePaymentRefundReason, +} from "./validators" + +export const adminRefundReasonsRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/admin/refund-reasons", + middlewares: [ + validateAndTransformQuery( + AdminGetRefundReasonsParams, + queryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/refund-reasons", + middlewares: [ + validateAndTransformBody(AdminCreatePaymentRefundReason), + validateAndTransformQuery( + AdminGetRefundReasonsParams, + queryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/refund-reasons/:id", + middlewares: [ + validateAndTransformBody(AdminUpdatePaymentRefundReason), + validateAndTransformQuery( + AdminGetRefundReasonsParams, + queryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/refund-reasons/:id", + middlewares: [ + validateAndTransformQuery( + AdminGetRefundReasonsParams, + queryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["DELETE"], + matcher: "/admin/refund-reasons/:id", + middlewares: [ + validateAndTransformQuery( + AdminGetRefundReasonsParams, + queryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api/admin/refund-reasons/query-config.ts b/packages/medusa/src/api/admin/refund-reasons/query-config.ts new file mode 100644 index 0000000000000..927a3c9f6a8ba --- /dev/null +++ b/packages/medusa/src/api/admin/refund-reasons/query-config.ts @@ -0,0 +1,23 @@ +export const defaultAdminRefundReasonFields = [ + "id", + "label", + "description", + "created_at", + "updated_at", + "deleted_at", +] + +export const defaultAdminRetrieveRefundReasonFields = [ + ...defaultAdminRefundReasonFields, +] + +export const retrieveTransformQueryConfig = { + defaultFields: defaultAdminRetrieveRefundReasonFields, + isList: false, +} + +export const listTransformQueryConfig = { + defaults: defaultAdminRefundReasonFields, + defaultLimit: 20, + isList: true, +} diff --git a/packages/medusa/src/api/admin/refund-reasons/route.ts b/packages/medusa/src/api/admin/refund-reasons/route.ts new file mode 100644 index 0000000000000..575cbc36df844 --- /dev/null +++ b/packages/medusa/src/api/admin/refund-reasons/route.ts @@ -0,0 +1,49 @@ +import { createRefundReasonsWorkflow } from "@medusajs/core-flows" +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { + PaginatedResponse, + RefundReasonResponse, + RefundReasonsResponse, +} from "@medusajs/types" +import { refetchEntities, refetchEntity } from "../../utils/refetch-entity" +import { AdminCreatePaymentRefundReasonType } from "./validators" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse> +) => { + const { rows: refund_reasons, metadata } = await refetchEntities( + "refund_reasons", + req.filterableFields, + req.scope, + req.remoteQueryConfig.fields, + req.remoteQueryConfig.pagination + ) + + res.json({ + refund_reasons, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { + result: [refundReason], + } = await createRefundReasonsWorkflow(req.scope).run({ + input: { data: [req.validatedBody] }, + }) + + const refund_reason = await refetchEntity( + "refund_reason", + refundReason.id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ refund_reason }) +} diff --git a/packages/medusa/src/api/admin/refund-reasons/validators.ts b/packages/medusa/src/api/admin/refund-reasons/validators.ts new file mode 100644 index 0000000000000..1d978ce436c17 --- /dev/null +++ b/packages/medusa/src/api/admin/refund-reasons/validators.ts @@ -0,0 +1,41 @@ +import { z } from "zod" +import { createFindParams, createOperatorMap } from "../../utils/validators" + +export type AdminCreatePaymentRefundReasonType = z.infer< + typeof AdminCreatePaymentRefundReason +> +export const AdminCreatePaymentRefundReason = z + .object({ + label: z.string(), + description: z.string().nullish(), + }) + .strict() + +export type AdminUpdatePaymentRefundReasonType = z.infer< + typeof AdminUpdatePaymentRefundReason +> +export const AdminUpdatePaymentRefundReason = z + .object({ + label: z.string().optional(), + description: z.string().nullish(), + }) + .strict() + +/** + * Parameters used to filter and configure the pagination of the retrieved refund reason. + */ +export const AdminGetRefundReasonsParams = createFindParams({ + limit: 15, + offset: 0, +}).merge( + z.object({ + id: z.union([z.string(), z.array(z.string())]).optional(), + q: z.string().optional(), + created_at: createOperatorMap().optional(), + updated_at: createOperatorMap().optional(), + deleted_at: createOperatorMap().optional(), + }) +) +export type AdminGetRefundReasonsParamsType = z.infer< + typeof AdminGetRefundReasonsParams +> diff --git a/packages/medusa/src/api/middlewares.ts b/packages/medusa/src/api/middlewares.ts index 9e639d468f611..bcf74e5196d86 100644 --- a/packages/medusa/src/api/middlewares.ts +++ b/packages/medusa/src/api/middlewares.ts @@ -23,6 +23,7 @@ import { adminProductTagRoutesMiddlewares } from "./admin/product-tags/middlewar import { adminProductTypeRoutesMiddlewares } from "./admin/product-types/middlewares" import { adminProductRoutesMiddlewares } from "./admin/products/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" +import { adminRefundReasonsRoutesMiddlewares } from "./admin/refund-reasons/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" import { adminReservationRoutesMiddlewares } from "./admin/reservations/middlewares" import { adminReturnReasonRoutesMiddlewares } from "./admin/return-reasons/middlewares" @@ -107,5 +108,6 @@ export default defineMiddlewares([ ...storeReturnReasonRoutesMiddlewares, ...adminReturnReasonRoutesMiddlewares, ...adminClaimRoutesMiddlewares, + ...adminRefundReasonsRoutesMiddlewares, ...adminExchangeRoutesMiddlewares, ]) diff --git a/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts b/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts index 7d810ba2c76b8..4e0989a136900 100644 --- a/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts +++ b/packages/modules/payment/integration-tests/__tests__/services/payment-module/index.spec.ts @@ -23,6 +23,7 @@ moduleIntegrationTestRunner({ "payment", "paymentCollection", "paymentProvider", + "refundReason", ]) Object.keys(linkable).forEach((key) => { @@ -54,6 +55,14 @@ moduleIntegrationTestRunner({ field: "paymentProvider", }, }, + refundReason: { + id: { + linkable: "refund_reason_id", + primaryKey: "id", + serviceName: "payment", + field: "refundReason", + }, + }, }) }) diff --git a/packages/modules/payment/src/joiner-config.ts b/packages/modules/payment/src/joiner-config.ts index b8660e30cf8eb..d6153bc4ae109 100644 --- a/packages/modules/payment/src/joiner-config.ts +++ b/packages/modules/payment/src/joiner-config.ts @@ -4,13 +4,21 @@ import { PaymentCollection, PaymentProvider, PaymentSession, + RefundReason, } from "@models" export const joinerConfig = defineJoinerConfig(Modules.PAYMENT, { - models: [Payment, PaymentCollection, PaymentProvider, PaymentSession], + models: [ + Payment, + PaymentCollection, + PaymentProvider, + PaymentSession, + RefundReason, + ], linkableKeys: { payment_id: Payment.name, payment_collection_id: PaymentCollection.name, payment_provider_id: PaymentProvider.name, + refund_reason_id: RefundReason.name, }, }) diff --git a/packages/modules/payment/src/migrations/.snapshot-medusa-payment.json b/packages/modules/payment/src/migrations/.snapshot-medusa-payment.json new file mode 100644 index 0000000000000..47226f14e8a68 --- /dev/null +++ b/packages/modules/payment/src/migrations/.snapshot-medusa-payment.json @@ -0,0 +1,1271 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "currency_code": { + "name": "currency_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "amount": { + "name": "amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "raw_amount": { + "name": "raw_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "authorized_amount": { + "name": "authorized_amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "raw_authorized_amount": { + "name": "raw_authorized_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "captured_amount": { + "name": "captured_amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "raw_captured_amount": { + "name": "raw_captured_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "refunded_amount": { + "name": "refunded_amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "raw_refunded_amount": { + "name": "raw_refunded_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "region_id": { + "name": "region_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "status": { + "name": "status", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "'not_paid'", + "enumItems": [ + "not_paid", + "awaiting", + "authorized", + "partially_authorized", + "canceled" + ], + "mappedType": "enum" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + } + }, + "name": "payment_collection", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "region_id" + ], + "composite": false, + "keyName": "IDX_payment_collection_region_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_payment_collection_deleted_at", + "primary": false, + "unique": false + }, + { + "keyName": "payment_collection_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "data": { + "name": "data", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "type_detail": { + "name": "type_detail", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "description_detail": { + "name": "description_detail", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + } + }, + "name": "payment_method_token", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_payment_method_token_deleted_at", + "primary": false, + "unique": false + }, + { + "keyName": "payment_method_token_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + } + }, + "name": "payment_provider", + "schema": "public", + "indexes": [ + { + "keyName": "payment_provider_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "payment_collection_id": { + "name": "payment_collection_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "payment_provider_id": { + "name": "payment_provider_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "payment_collection_payment_providers", + "schema": "public", + "indexes": [ + { + "keyName": "payment_collection_payment_providers_pkey", + "columnNames": [ + "payment_collection_id", + "payment_provider_id" + ], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "payment_collection_payment_providers_payment_coll_aa276_foreign": { + "constraintName": "payment_collection_payment_providers_payment_coll_aa276_foreign", + "columnNames": [ + "payment_collection_id" + ], + "localTableName": "public.payment_collection_payment_providers", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment_collection", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "payment_collection_payment_providers_payment_provider_id_foreign": { + "constraintName": "payment_collection_payment_providers_payment_provider_id_foreign", + "columnNames": [ + "payment_provider_id" + ], + "localTableName": "public.payment_collection_payment_providers", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment_provider", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "currency_code": { + "name": "currency_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "amount": { + "name": "amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "raw_amount": { + "name": "raw_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "data": { + "name": "data", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "context": { + "name": "context", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "status": { + "name": "status", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "'pending'", + "enumItems": [ + "authorized", + "captured", + "pending", + "requires_more", + "error", + "canceled" + ], + "mappedType": "enum" + }, + "authorized_at": { + "name": "authorized_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "payment_collection_id": { + "name": "payment_collection_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "payment_session", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "payment_collection_id" + ], + "composite": false, + "keyName": "IDX_payment_session_payment_collection_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_payment_session_deleted_at", + "primary": false, + "unique": false + }, + { + "keyName": "payment_session_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "payment_session_payment_collection_id_foreign": { + "constraintName": "payment_session_payment_collection_id_foreign", + "columnNames": [ + "payment_collection_id" + ], + "localTableName": "public.payment_session", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment_collection", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "amount": { + "name": "amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "raw_amount": { + "name": "raw_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "currency_code": { + "name": "currency_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "cart_id": { + "name": "cart_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "order_id": { + "name": "order_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "data": { + "name": "data", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "captured_at": { + "name": "captured_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "payment_collection_id": { + "name": "payment_collection_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "payment_session_id": { + "name": "payment_session_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "payment", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_payment_deleted_at", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "payment_collection_id" + ], + "composite": false, + "keyName": "IDX_payment_payment_collection_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "payment_session_id" + ], + "composite": false, + "keyName": "IDX_payment_payment_session_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "payment_session_id" + ], + "composite": false, + "keyName": "payment_payment_session_id_unique", + "primary": false, + "unique": true + }, + { + "keyName": "IDX_payment_provider_id", + "columnNames": [ + "provider_id" + ], + "composite": false, + "primary": false, + "unique": false, + "expression": "CREATE INDEX IF NOT EXISTS \"IDX_payment_provider_id\" ON \"payment\" (provider_id)" + }, + { + "keyName": "payment_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "payment_payment_collection_id_foreign": { + "constraintName": "payment_payment_collection_id_foreign", + "columnNames": [ + "payment_collection_id" + ], + "localTableName": "public.payment", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment_collection", + "updateRule": "cascade" + }, + "payment_payment_session_id_foreign": { + "constraintName": "payment_payment_session_id_foreign", + "columnNames": [ + "payment_session_id" + ], + "localTableName": "public.payment", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment_session", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "amount": { + "name": "amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "raw_amount": { + "name": "raw_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "payment_id": { + "name": "payment_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "created_by": { + "name": "created_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + } + }, + "name": "capture", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "payment_id" + ], + "composite": false, + "keyName": "IDX_capture_payment_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_capture_deleted_at", + "primary": false, + "unique": false + }, + { + "keyName": "capture_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "capture_payment_id_foreign": { + "constraintName": "capture_payment_id_foreign", + "columnNames": [ + "payment_id" + ], + "localTableName": "public.capture", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "label": { + "name": "label", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "refund_reason", + "schema": "public", + "indexes": [ + { + "keyName": "refund_reason_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "amount": { + "name": "amount", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "raw_amount": { + "name": "raw_amount", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "json" + }, + "payment_id": { + "name": "payment_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "refund_reason_id": { + "name": "refund_reason_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "note": { + "name": "note", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "created_by": { + "name": "created_by", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + } + }, + "name": "refund", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "payment_id" + ], + "composite": false, + "keyName": "IDX_refund_payment_id", + "primary": false, + "unique": false + }, + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_refund_deleted_at", + "primary": false, + "unique": false + }, + { + "keyName": "refund_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "refund_payment_id_foreign": { + "constraintName": "refund_payment_id_foreign", + "columnNames": [ + "payment_id" + ], + "localTableName": "public.refund", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.payment", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "refund_refund_reason_id_foreign": { + "constraintName": "refund_refund_reason_id_foreign", + "columnNames": [ + "refund_reason_id" + ], + "localTableName": "public.refund", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.refund_reason", + "deleteRule": "set null", + "updateRule": "cascade" + } + } + } + ] +} diff --git a/packages/modules/payment/src/migrations/Migration20240806072619.ts b/packages/modules/payment/src/migrations/Migration20240806072619.ts new file mode 100644 index 0000000000000..6288e3428ae47 --- /dev/null +++ b/packages/modules/payment/src/migrations/Migration20240806072619.ts @@ -0,0 +1,81 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240806072619 extends Migration { + async up(): Promise { + this.addSql( + 'create table if not exists "refund_reason" ("id" text not null, "label" text not null, "description" text null, "metadata" jsonb null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "refund_reason_pkey" primary key ("id"));' + ) + + this.addSql( + 'alter table if exists "payment_session" drop constraint if exists "payment_session_status_check";' + ) + + this.addSql( + 'alter table if exists "payment_session" drop constraint if exists "payment_session_payment_collection_id_foreign";' + ) + + this.addSql( + 'alter table if exists "payment_session" alter column "status" type text using ("status"::text);' + ) + this.addSql( + "alter table if exists \"payment_session\" add constraint \"payment_session_status_check\" check (\"status\" in ('authorized', 'captured', 'pending', 'requires_more', 'error', 'canceled'));" + ) + this.addSql( + 'create index if not exists "IDX_payment_session_deleted_at" on "payment_session" ("deleted_at");' + ) + + this.addSql('drop index if exists "IDX_capture_deleted_at";') + this.addSql('drop index if exists "IDX_refund_deleted_at";') + this.addSql( + 'create index if not exists "IDX_payment_payment_session_id" on "payment" ("payment_session_id");' + ) + this.addSql( + 'alter table if exists "payment" add constraint "payment_payment_session_id_unique" unique ("payment_session_id");' + ) + + this.addSql( + 'create index if not exists "IDX_capture_deleted_at" on "capture" ("deleted_at");' + ) + + this.addSql( + 'alter table if exists "refund" add column if not exists "refund_reason_id" text null, add column if not exists "note" text null;' + ) + this.addSql( + 'create index if not exists "IDX_refund_deleted_at" on "refund" ("deleted_at");' + ) + } + + async down(): Promise { + this.addSql('drop table if exists "refund_reason" cascade;') + + this.addSql( + 'alter table if exists "payment_session" drop constraint if exists "payment_session_status_check";' + ) + + this.addSql('drop index if exists "IDX_capture_deleted_at";') + + this.addSql('drop index if exists "IDX_payment_payment_session_id";') + this.addSql( + 'alter table if exists "payment" drop constraint if exists "payment_payment_session_id_unique";' + ) + this.addSql( + 'create index if not exists "IDX_capture_deleted_at" on "payment" ("deleted_at");' + ) + this.addSql( + 'create index if not exists "IDX_refund_deleted_at" on "payment" ("deleted_at");' + ) + + this.addSql( + 'alter table if exists "payment_session" alter column "status" type text using ("status"::text);' + ) + this.addSql( + "alter table if exists \"payment_session\" add constraint \"payment_session_status_check\" check (\"status\" in ('authorized', 'pending', 'requires_more', 'error', 'canceled'));" + ) + this.addSql('drop index if exists "IDX_payment_session_deleted_at";') + this.addSql('drop index if exists "IDX_refund_deleted_at";') + this.addSql( + 'alter table if exists "refund" drop column if exists "refund_reason_id";' + ) + this.addSql('alter table if exists "refund" drop column if exists "note";') + } +} diff --git a/packages/modules/payment/src/models/capture.ts b/packages/modules/payment/src/models/capture.ts index 28f3f2b8b89d9..f99c6410ce0fb 100644 --- a/packages/modules/payment/src/models/capture.ts +++ b/packages/modules/payment/src/models/capture.ts @@ -37,6 +37,9 @@ export default class Capture { }) payment!: Payment + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + @Property({ onCreate: () => new Date(), columnType: "timestamptz", diff --git a/packages/modules/payment/src/models/index.ts b/packages/modules/payment/src/models/index.ts index 6588a97b7604a..cc1ebaed7943c 100644 --- a/packages/modules/payment/src/models/index.ts +++ b/packages/modules/payment/src/models/index.ts @@ -5,3 +5,4 @@ export { default as PaymentMethodToken } from "./payment-method-token" export { default as PaymentProvider } from "./payment-provider" export { default as PaymentSession } from "./payment-session" export { default as Refund } from "./refund" +export { default as RefundReason } from "./refund-reason" diff --git a/packages/modules/payment/src/models/payment-method-token.ts b/packages/modules/payment/src/models/payment-method-token.ts index 6d965a37c5704..dbad27a306c78 100644 --- a/packages/modules/payment/src/models/payment-method-token.ts +++ b/packages/modules/payment/src/models/payment-method-token.ts @@ -45,7 +45,7 @@ export default class PaymentMethodToken { @Property({ columnType: "timestamptz", nullable: true, - index: "IDX_payment_metod_token_deleted_at", + index: "IDX_payment_method_token_deleted_at", }) deleted_at: Date | null = null diff --git a/packages/modules/payment/src/models/payment-session.ts b/packages/modules/payment/src/models/payment-session.ts index 52ceafc6ba221..c6afe35411aee 100644 --- a/packages/modules/payment/src/models/payment-session.ts +++ b/packages/modules/payment/src/models/payment-session.ts @@ -79,6 +79,9 @@ export default class PaymentSession { }) payment?: Rel | null + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + @Property({ onCreate: () => new Date(), columnType: "timestamptz", diff --git a/packages/modules/payment/src/models/payment.ts b/packages/modules/payment/src/models/payment.ts index afc8cd35e7904..4e6fdaf821768 100644 --- a/packages/modules/payment/src/models/payment.ts +++ b/packages/modules/payment/src/models/payment.ts @@ -4,6 +4,7 @@ import { DALUtils, MikroOrmBigNumberProperty, Searchable, + createPsqlIndexStatementHelper, generateEntityId, } from "@medusajs/utils" import { @@ -28,7 +29,13 @@ import Refund from "./refund" type OptionalPaymentProps = DAL.ModelDateColumns -@Entity({ tableName: "payment" }) +const tableName = "payment" +const ProviderIdIndex = createPsqlIndexStatementHelper({ + tableName, + columns: "provider_id", +}) + +@Entity({ tableName }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) export default class Payment { [OptionalProps]?: OptionalPaymentProps @@ -46,6 +53,7 @@ export default class Payment { currency_code: string @Property({ columnType: "text" }) + @ProviderIdIndex.MikroORMIndex() provider_id: string @Searchable() diff --git a/packages/modules/payment/src/models/refund-reason.ts b/packages/modules/payment/src/models/refund-reason.ts new file mode 100644 index 0000000000000..c42aaeb5d5c0a --- /dev/null +++ b/packages/modules/payment/src/models/refund-reason.ts @@ -0,0 +1,54 @@ +import { DALUtils, generateEntityId, Searchable } from "@medusajs/utils" +import { + BeforeCreate, + Entity, + Filter, + OnInit, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +@Entity({ tableName: "refund_reason" }) +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) +export default class RefundReason { + @PrimaryKey({ columnType: "text" }) + id: string + + @Searchable() + @Property({ columnType: "text" }) + label: string + + @Property({ columnType: "text", nullable: true }) + description: string | null = null + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "refr") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "refr") + } +} diff --git a/packages/modules/payment/src/models/refund.ts b/packages/modules/payment/src/models/refund.ts index f325fea17c6e3..7114892350424 100644 --- a/packages/modules/payment/src/models/refund.ts +++ b/packages/modules/payment/src/models/refund.ts @@ -1,4 +1,4 @@ -import { BigNumberRawValue } from "@medusajs/types" +import { BigNumberRawValue, DAL } from "@medusajs/types" import { BigNumber, MikroOrmBigNumberProperty, @@ -9,14 +9,24 @@ import { Entity, ManyToOne, OnInit, + OptionalProps, PrimaryKey, Property, Rel, } from "@mikro-orm/core" import Payment from "./payment" +import RefundReason from "./refund-reason" + +type OptionalProps = + | "note" + | "refund_reason_id" + | "refund_reason" + | DAL.ModelDateColumns @Entity({ tableName: "refund" }) export default class Refund { + [OptionalProps]?: OptionalProps + @PrimaryKey({ columnType: "text" }) id: string @@ -36,6 +46,20 @@ export default class Refund { @Property({ columnType: "text", nullable: true }) payment_id: string + @ManyToOne(() => RefundReason, { + columnType: "text", + mapToPk: true, + fieldName: "refund_reason_id", + nullable: true, + }) + refund_reason_id: string | null = null + + @ManyToOne(() => RefundReason, { persist: false, nullable: true }) + refund_reason: Rel | null = null + + @Property({ columnType: "text", nullable: true }) + note: string | null = null + @Property({ onCreate: () => new Date(), columnType: "timestamptz", @@ -66,10 +90,12 @@ export default class Refund { @BeforeCreate() onCreate() { this.id = generateEntityId(this.id, "ref") + this.refund_reason_id ??= this.refund_reason?.id || null } @OnInit() onInit() { this.id = generateEntityId(this.id, "ref") + this.refund_reason_id ??= this.refund_reason?.id || null } } diff --git a/packages/modules/payment/src/services/payment-module.ts b/packages/modules/payment/src/services/payment-module.ts index e66cf91d3bf2a..35ef3ef68b60f 100644 --- a/packages/modules/payment/src/services/payment-module.ts +++ b/packages/modules/payment/src/services/payment-module.ts @@ -22,6 +22,7 @@ import { PaymentSessionDTO, ProviderWebhookPayload, RefundDTO, + RefundReasonDTO, UpdatePaymentCollectionDTO, UpdatePaymentDTO, UpdatePaymentSessionDTO, @@ -47,6 +48,7 @@ import { PaymentCollection, PaymentSession, Refund, + RefundReason, } from "@models" import { joinerConfig } from "../joiner-config" import PaymentProviderService from "./payment-provider" @@ -67,6 +69,7 @@ const generateMethodForModels = { Payment, Capture, Refund, + RefundReason, } export default class PaymentModuleService @@ -76,6 +79,7 @@ export default class PaymentModuleService Payment: { dto: PaymentDTO } Capture: { dto: CaptureDTO } Refund: { dto: RefundDTO } + RefundReason: { dto: RefundReasonDTO } }>(generateMethodForModels) implements IPaymentModuleService { @@ -784,6 +788,8 @@ export default class PaymentModuleService payment: data.payment_id, amount: data.amount, created_by: data.created_by, + note: data.note, + refund_reason_id: data.refund_reason_id, }, sharedContext )