diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 3c0657e63c..d1c37acc89 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -100,7 +100,7 @@ medusaIntegrationTestRunner({ prices: [ { currency_code: "usd", - amount: 123456.1234657890123456789, + amount: 50.25, }, ], }, @@ -333,7 +333,7 @@ medusaIntegrationTestRunner({ prices: [ { currency_code: "usd", - amount: 1000, + amount: 15, }, ], rules: [ @@ -359,7 +359,7 @@ medusaIntegrationTestRunner({ prices: [ { currency_code: "usd", - amount: 1000, + amount: 20, }, ], rules: [ @@ -672,6 +672,40 @@ medusaIntegrationTestRunner({ 0 ) }) + + it("should create a payment collection successfully and throw on multiple", async () => { + const paymentDelta = 110.5 + + const paymentCollection = ( + await api.post( + `/admin/payment-collections`, + { order_id: order.id }, + adminHeaders + ) + ).data.payment_collection + + expect(paymentCollection).toEqual( + expect.objectContaining({ + currency_code: "usd", + amount: paymentDelta, + payment_sessions: [], + }) + ) + + const { response } = await api + .post( + `/admin/payment-collections`, + { order_id: order.id }, + adminHeaders + ) + .catch((e) => e) + + expect(response.data).toEqual({ + type: "not_allowed", + message: + "Active payment collections were found. Complete existing ones or delete them before proceeding.", + }) + }) }) describe("with only outbound items", () => { diff --git a/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts b/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts new file mode 100644 index 0000000000..b8e055efeb --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/create-order-payment-collection.ts @@ -0,0 +1,135 @@ +import { PaymentCollectionDTO } from "@medusajs/types" +import { + MathBN, + MedusaError, + Modules, + PaymentCollectionStatus, +} from "@medusajs/utils" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, + transform, +} from "@medusajs/workflows-sdk" +import { createRemoteLinkStep, useRemoteQueryStep } from "../../common" +import { createPaymentCollectionsStep } from "../../definition" + +/** + * This step validates that the order doesn't have an active payment collection. + */ +export const throwIfActivePaymentCollectionExists = createStep( + "validate-existing-payment-collection", + ({ paymentCollection }: { paymentCollection: PaymentCollectionDTO }) => { + if (paymentCollection) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Active payment collections were found. Complete existing ones or delete them before proceeding.` + ) + } + } +) + +export const createOrderPaymentCollectionWorkflowId = + "create-order-payment-collection" +/** + * This workflow creates a payment collection for an order. + */ +export const createOrderPaymentCollectionWorkflow = createWorkflow( + createOrderPaymentCollectionWorkflowId, + ( + input: WorkflowData<{ + order_id: string + amount?: number + }> + ) => { + const order = useRemoteQueryStep({ + entry_point: "order", + fields: ["id", "summary", "currency_code", "region_id"], + variables: { id: input.order_id }, + throw_if_key_not_found: true, + list: false, + }) + + const orderPaymentCollections = useRemoteQueryStep({ + entry_point: "order_payment_collection", + fields: ["payment_collection_id"], + variables: { order_id: order.id }, + }).config({ name: "order-payment-collection-query" }) + + const orderPaymentCollectionIds = transform( + { orderPaymentCollections }, + ({ orderPaymentCollections }) => + orderPaymentCollections.map((opc) => opc.payment_collection_id) + ) + + const paymentCollection = useRemoteQueryStep({ + entry_point: "payment_collection", + fields: ["id"], + variables: { + id: orderPaymentCollectionIds, + status: [ + PaymentCollectionStatus.NOT_PAID, + PaymentCollectionStatus.AWAITING, + ], + }, + list: false, + }).config({ name: "payment-collection-query" }) + + throwIfActivePaymentCollectionExists({ paymentCollection }) + + const paymentCollectionData = transform( + { order, input }, + ({ order, input }) => { + const pendingPayment = MathBN.sub( + order.summary.raw_current_order_total, + order.summary.raw_original_order_total + ) + + if (MathBN.lte(pendingPayment, 0)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot create a payment collection for amount less than 0` + ) + } + + if (input.amount && MathBN.gt(input.amount, pendingPayment)) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot create a payment collection for amount greater than ${pendingPayment}` + ) + } + + return { + currency_code: order.currency_code, + amount: input.amount ?? pendingPayment, + region_id: order.region_id, + } + } + ) + + const createdPaymentCollections = createPaymentCollectionsStep([ + paymentCollectionData, + ]) + + const orderPaymentCollectionLink = transform( + { order, createdPaymentCollections }, + ({ order, createdPaymentCollections }) => { + return [ + { + [Modules.ORDER]: { order_id: order.id }, + [Modules.PAYMENT]: { + payment_collection_id: createdPaymentCollections[0].id, + }, + }, + ] + } + ) + + createRemoteLinkStep(orderPaymentCollectionLink).config({ + name: "order-payment-collection-link", + }) + + return new WorkflowResponse(createdPaymentCollections) + } +) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 43192d7bfb..85f67f1592 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -21,6 +21,7 @@ export * from "./complete-orders" export * from "./create-fulfillment" export * from "./create-order-change" export * from "./create-order-change-actions" +export * from "./create-order-payment-collection" export * from "./create-orders" export * from "./create-shipment" export * from "./decline-order-change" diff --git a/packages/core/types/src/http/payment/admin/responses.ts b/packages/core/types/src/http/payment/admin/responses.ts index 241c6add2f..2999daf591 100644 --- a/packages/core/types/src/http/payment/admin/responses.ts +++ b/packages/core/types/src/http/payment/admin/responses.ts @@ -1,5 +1,15 @@ import { PaginatedResponse } from "../../common" -import { AdminPayment, AdminPaymentProvider, AdminRefund, AdminRefundReason } from "./entities" +import { + AdminPayment, + AdminPaymentCollection, + AdminPaymentProvider, + AdminRefund, + AdminRefundReason, +} from "./entities" + +export interface AdminPaymentCollectionResponse { + payment_collection: AdminPaymentCollection +} export interface AdminPaymentResponse { payment: AdminPayment @@ -27,4 +37,4 @@ export type RefundReasonsResponse = PaginatedResponse<{ export type AdminPaymentProviderListResponse = PaginatedResponse<{ payment_providers: AdminPaymentProvider[] -}> \ No newline at end of file +}> diff --git a/packages/medusa/src/api/admin/payment-collections/middlewares.ts b/packages/medusa/src/api/admin/payment-collections/middlewares.ts new file mode 100644 index 0000000000..b318dad45a --- /dev/null +++ b/packages/medusa/src/api/admin/payment-collections/middlewares.ts @@ -0,0 +1,22 @@ +import { MiddlewareRoute } from "@medusajs/framework" +import { validateAndTransformBody } from "../../utils/validate-body" +import { validateAndTransformQuery } from "../../utils/validate-query" +import * as queryConfig from "./query-config" +import { + AdminCreatePaymentCollection, + AdminGetPaymentCollectionParams, +} from "./validators" + +export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [ + { + method: ["POST"], + matcher: "/admin/payment-collections", + middlewares: [ + validateAndTransformBody(AdminCreatePaymentCollection), + validateAndTransformQuery( + AdminGetPaymentCollectionParams, + queryConfig.retrievePaymentCollectionTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api/admin/payment-collections/query-config.ts b/packages/medusa/src/api/admin/payment-collections/query-config.ts new file mode 100644 index 0000000000..5dc3aa9d48 --- /dev/null +++ b/packages/medusa/src/api/admin/payment-collections/query-config.ts @@ -0,0 +1,12 @@ +export const defaultPaymentCollectionFields = [ + "id", + "currency_code", + "amount", + "status", + "*payment_sessions", +] + +export const retrievePaymentCollectionTransformQueryConfig = { + defaults: defaultPaymentCollectionFields, + isList: false, +} diff --git a/packages/medusa/src/api/admin/payment-collections/route.ts b/packages/medusa/src/api/admin/payment-collections/route.ts new file mode 100644 index 0000000000..09b4b132d6 --- /dev/null +++ b/packages/medusa/src/api/admin/payment-collections/route.ts @@ -0,0 +1,26 @@ +import { createOrderPaymentCollectionWorkflow } from "@medusajs/core-flows" +import { HttpTypes } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" +import { refetchEntity } from "../../utils/refetch-entity" +import { AdminCreatePaymentCollectionType } from "./validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { result } = await createOrderPaymentCollectionWorkflow(req.scope).run({ + input: req.body, + }) + + const paymentCollection = await refetchEntity( + "payment_collection", + result[0].id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ payment_collection: paymentCollection }) +} diff --git a/packages/medusa/src/api/admin/payment-collections/validators.ts b/packages/medusa/src/api/admin/payment-collections/validators.ts new file mode 100644 index 0000000000..5f3c6c6daf --- /dev/null +++ b/packages/medusa/src/api/admin/payment-collections/validators.ts @@ -0,0 +1,17 @@ +import { z } from "zod" +import { createSelectParams } from "../../utils/validators" + +export type AdminGetPaymentCollectionParamsType = z.infer< + typeof AdminGetPaymentCollectionParams +> +export const AdminGetPaymentCollectionParams = createSelectParams() + +export type AdminCreatePaymentCollectionType = z.infer< + typeof AdminCreatePaymentCollection +> +export const AdminCreatePaymentCollection = z + .object({ + order_id: z.string(), + amount: z.number().optional(), + }) + .strict() diff --git a/packages/medusa/src/api/middlewares.ts b/packages/medusa/src/api/middlewares.ts index e06b04e4f9..579cf184d2 100644 --- a/packages/medusa/src/api/middlewares.ts +++ b/packages/medusa/src/api/middlewares.ts @@ -16,6 +16,7 @@ import { adminInviteRoutesMiddlewares } from "./admin/invites/middlewares" import { adminNotificationRoutesMiddlewares } from "./admin/notifications/middlewares" import { adminOrderEditRoutesMiddlewares } from "./admin/order-edits/middlewares" import { adminOrderRoutesMiddlewares } from "./admin/orders/middlewares" +import { adminPaymentCollectionsMiddlewares } from "./admin/payment-collections/middlewares" import { adminPaymentRoutesMiddlewares } from "./admin/payments/middlewares" import { adminPriceListsRoutesMiddlewares } from "./admin/price-lists/middlewares" import { adminPricePreferencesRoutesMiddlewares } from "./admin/price-preferences/middlewares" @@ -114,4 +115,5 @@ export default defineMiddlewares([ ...adminExchangeRoutesMiddlewares, ...adminProductVariantRoutesMiddlewares, ...adminOrderEditRoutesMiddlewares, + ...adminPaymentCollectionsMiddlewares, ])