From ae82591723726ae8a55b03d72da52461288ed1df Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 16:33:08 +0200 Subject: [PATCH 1/6] feat(core-flows): create or update payment collections in RMA flows --- .../http/__tests__/claims/claims.spec.ts | 57 +++++---- .../workflows/claim/confirm-claim-request.ts | 7 ++ ...eate-or-update-order-payment-collection.ts | 108 ++++++++++++++++++ .../create-order-payment-collection.ts | 73 +----------- .../exchange/confirm-exchange-request.ts | 7 ++ .../order-edit/confirm-order-edit-request.ts | 7 ++ .../return/confirm-return-request.ts | 7 ++ .../admin/payment-collections/validators.ts | 2 +- 8 files changed, 176 insertions(+), 92 deletions(-) create mode 100644 packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 0118e0bc58a44..7ea1a86fc3160 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -557,7 +557,11 @@ medusaIntegrationTestRunner({ adminHeaders ) - await api.post(`/admin/claims/${claimId2}/request`, {}, adminHeaders) + const testRes = await api.post( + `/admin/claims/${claimId2}/request`, + {}, + adminHeaders + ) claimId = baseClaim.id item = order.items[0] @@ -682,6 +686,17 @@ medusaIntegrationTestRunner({ await api.get(`/admin/orders/${order.id}`, adminHeaders) ).data.order + const paymentCollections = fulfillOrder.payment_collections + + expect(paymentCollections).toHaveLength(1) + expect(paymentCollections[0]).toEqual( + expect.objectContaining({ + status: "not_paid", + amount: 171.5, + currency_code: "usd", + }) + ) + const fulfillableItem = fulfillOrder.items.find( (item) => item.detail.fulfilled_quantity === 0 ) @@ -721,12 +736,25 @@ medusaIntegrationTestRunner({ }) it("should create a payment collection successfully and throw on multiple", async () => { - const paymentDelta = 171.5 + const orderForPayment = ( + await api.get(`/admin/orders/${order.id}`, adminHeaders) + ).data.order + + const paymentCollections = orderForPayment.payment_collections + + expect(paymentCollections).toHaveLength(1) + expect(paymentCollections[0]).toEqual( + expect.objectContaining({ + status: "not_paid", + amount: 171.5, + currency_code: "usd", + }) + ) const paymentCollection = ( await api.post( `/admin/payment-collections`, - { order_id: order.id }, + { order_id: order.id, amount: 100 }, adminHeaders ) ).data.payment_collection @@ -734,28 +762,14 @@ medusaIntegrationTestRunner({ expect(paymentCollection).toEqual( expect.objectContaining({ currency_code: "usd", - amount: paymentDelta, - payment_sessions: [], + amount: 100, + status: "not_paid", }) ) - 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.", - }) - const deleted = ( await api.delete( - `/admin/payment-collections/${paymentCollection.id}`, + `/admin/payment-collections/${paymentCollections[0].id}`, adminHeaders ) ).data @@ -973,7 +987,8 @@ medusaIntegrationTestRunner({ }, adminHeaders ) - await api.post( + + const { response } = await api.post( `/admin/claims/${baseClaim.id}/request`, {}, adminHeaders diff --git a/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts b/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts index 9557fa44858f9..4229a5afec591 100644 --- a/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts +++ b/packages/core/core-flows/src/order/workflows/claim/confirm-claim-request.ts @@ -32,6 +32,7 @@ import { throwIfIsCancelled, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection" export type ConfirmClaimRequestWorkflowInput = { claim_id: string @@ -385,6 +386,12 @@ export const confirmClaimRequestWorkflow = createWorkflow( }) }) + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }) + return new WorkflowResponse(orderPreview) } ) diff --git a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts new file mode 100644 index 0000000000000..33edd3a9f2e82 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts @@ -0,0 +1,108 @@ +import { PaymentCollectionDTO } from "@medusajs/types" +import { MedusaError, PaymentCollectionStatus } from "@medusajs/utils" +import { + createWorkflow, + transform, + when, + WorkflowData, + WorkflowResponse, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { updatePaymentCollectionStep } from "../../payment-collection" +import { createOrderPaymentCollectionWorkflow } from "./create-order-payment-collection" + +export const createOrUpdateOrderPaymentCollectionWorkflowId = + "create-or-update-order-payment-collection" +/** + * This workflow creates or updates payment collection for an order. + */ +export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow( + createOrUpdateOrderPaymentCollectionWorkflowId, + ( + 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 existingPaymentCollection = useRemoteQueryStep({ + entry_point: "payment_collection", + fields: ["id", "status"], + variables: { + filters: { + id: orderPaymentCollectionIds, + status: [PaymentCollectionStatus.NOT_PAID], + }, + }, + list: false, + }).config({ name: "payment-collection-query" }) + + const amountPending = transform({ order, input }, ({ order, input }) => { + const pendingPayment = order.summary.pending_difference + + if (input.amount && input.amount > pendingPayment) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Amount cannot be greater than ${pendingPayment}` + ) + } + + return pendingPayment + }) + + const updatedPaymentCollections = when( + { existingPaymentCollection, amountPending }, + ({ existingPaymentCollection, amountPending }) => { + return !!existingPaymentCollection?.id && amountPending > 0 + } + ).then(() => { + return updatePaymentCollectionStep({ + selector: { id: existingPaymentCollection.id }, + update: { + amount: amountPending, + }, + }) as PaymentCollectionDTO[] + }) + + const createdPaymentCollection = when( + { existingPaymentCollection, amountPending }, + ({ existingPaymentCollection, amountPending }) => { + return !!!existingPaymentCollection?.id && amountPending > 0 + } + ).then(() => { + return createOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + amount: amountPending, + }, + }) as PaymentCollectionDTO[] + }) + + const paymentCollections = transform( + { updatedPaymentCollections, createdPaymentCollection }, + ({ updatedPaymentCollections, createdPaymentCollection }) => + updatedPaymentCollections || createdPaymentCollection + ) + + return new WorkflowResponse(paymentCollections) + } +) 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 index 38fd4d8d18306..af08759fdddd9 100644 --- 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 @@ -1,35 +1,13 @@ -import { PaymentCollectionDTO } from "@medusajs/types" -import { - MathBN, - MedusaError, - Modules, - PaymentCollectionStatus, -} from "@medusajs/utils" +import { Modules } 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" /** @@ -40,7 +18,7 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow( ( input: WorkflowData<{ order_id: string - amount?: number + amount: number }> ) => { const order = useRemoteQueryStep({ @@ -51,57 +29,12 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow( 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", "status"], - variables: { - filters: { - id: orderPaymentCollectionIds, - status: [PaymentCollectionStatus.NOT_PAID], - }, - }, - list: false, - }).config({ name: "payment-collection-query" }) - - throwIfActivePaymentCollectionExists({ paymentCollection }) - const paymentCollectionData = transform( { order, input }, ({ order, input }) => { - const pendingPayment = order.summary.raw_pending_difference - - 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, 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, + amount: input.amount, region_id: order.region_id, } } diff --git a/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts b/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts index f72d74c448069..a305ac1ec2855 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/confirm-exchange-request.ts @@ -32,6 +32,7 @@ import { throwIfIsCancelled, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection" export type ConfirmExchangeRequestWorkflowInput = { exchange_id: string @@ -381,6 +382,12 @@ export const confirmExchangeRequestWorkflow = createWorkflow( }) }) + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }) + return new WorkflowResponse(orderPreview) } ) diff --git a/packages/core/core-flows/src/order/workflows/order-edit/confirm-order-edit-request.ts b/packages/core/core-flows/src/order/workflows/order-edit/confirm-order-edit-request.ts index 5b8e76f143927..68a68741d1ed5 100644 --- a/packages/core/core-flows/src/order/workflows/order-edit/confirm-order-edit-request.ts +++ b/packages/core/core-flows/src/order/workflows/order-edit/confirm-order-edit-request.ts @@ -15,6 +15,7 @@ import { throwIfIsCancelled, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection" export type ConfirmOrderEditRequestWorkflowInput = { order_id: string @@ -161,6 +162,12 @@ export const confirmOrderEditRequestWorkflow = createWorkflow( reserveInventoryStep(formatedInventoryItems) + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }) + return new WorkflowResponse(orderPreview) } ) diff --git a/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts b/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts index f10f1101299ad..7a884cdb79630 100644 --- a/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts +++ b/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts @@ -28,6 +28,7 @@ import { throwIfIsCancelled, throwIfOrderChangeIsNotActive, } from "../../utils/order-validation" +import { createOrUpdateOrderPaymentCollectionWorkflow } from "../create-or-update-order-payment-collection" export type ConfirmReturnRequestWorkflowInput = { return_id: string @@ -259,6 +260,12 @@ export const confirmReturnRequestWorkflow = createWorkflow( confirmOrderChanges({ changes: [orderChange], orderId: order.id }) ) + createOrUpdateOrderPaymentCollectionWorkflow.runAsStep({ + input: { + order_id: order.id, + }, + }) + return new WorkflowResponse(orderPreview) } ) diff --git a/packages/medusa/src/api/admin/payment-collections/validators.ts b/packages/medusa/src/api/admin/payment-collections/validators.ts index 5f3c6c6daf0e5..d9d6c9913fac7 100644 --- a/packages/medusa/src/api/admin/payment-collections/validators.ts +++ b/packages/medusa/src/api/admin/payment-collections/validators.ts @@ -12,6 +12,6 @@ export type AdminCreatePaymentCollectionType = z.infer< export const AdminCreatePaymentCollection = z .object({ order_id: z.string(), - amount: z.number().optional(), + amount: z.number(), }) .strict() From 5f5297ba3b936408d7f434f6d29ea85e9bc7a6c0 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 17:06:24 +0200 Subject: [PATCH 2/6] chore: change ui to pick payment link from unpaid payment collection --- .../copy-payment-link/copy-payment-link.tsx | 65 ++----------------- .../order-summary-section.tsx | 19 +++--- 2 files changed, 15 insertions(+), 69 deletions(-) diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/copy-payment-link/copy-payment-link.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/copy-payment-link/copy-payment-link.tsx index 68fb73d4b7ceb..681da8a07ae0e 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/copy-payment-link/copy-payment-link.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/copy-payment-link/copy-payment-link.tsx @@ -1,18 +1,15 @@ import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons" -import { AdminOrder } from "@medusajs/types" -import { Button, toast, Tooltip } from "@medusajs/ui" +import { AdminOrder, AdminPaymentCollection } from "@medusajs/types" +import { Button, Tooltip } from "@medusajs/ui" import copy from "copy-to-clipboard" import React, { useState } from "react" import { useTranslation } from "react-i18next" -import { - useCreatePaymentCollection, - useDeletePaymentCollection, -} from "../../../../../hooks/api" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" export const MEDUSA_BACKEND_URL = __STOREFRONT_URL__ ?? "http://localhost:8000" type CopyPaymentLinkProps = { + paymentCollection: AdminPaymentCollection order: AdminOrder } @@ -20,18 +17,11 @@ type CopyPaymentLinkProps = { * This component is based on the `button` element and supports all of its props */ const CopyPaymentLink = React.forwardRef( - ({ order }: CopyPaymentLinkProps, ref) => { - const [isCreating, setIsCreating] = useState(false) - const [url, setUrl] = useState("") + ({ paymentCollection, order }: CopyPaymentLinkProps, ref) => { const [done, setDone] = useState(false) const [open, setOpen] = useState(false) const [text, setText] = useState("CopyPaymentLink") const { t } = useTranslation() - const { mutateAsync: createPaymentCollection } = - useCreatePaymentCollection() - - const { mutateAsync: deletePaymentCollection } = - useDeletePaymentCollection() const copyToClipboard = async ( e: @@ -40,53 +30,11 @@ const CopyPaymentLink = React.forwardRef( ) => { e.stopPropagation() - if (!url?.length) { - const activePaymentCollection = order.payment_collections.find( - (pc) => - pc.status === "not_paid" && - pc.amount === order.summary?.pending_difference - ) - - if (!activePaymentCollection) { - setIsCreating(true) - - const paymentCollectionsToDelete = order.payment_collections.filter( - (pc) => pc.status === "not_paid" - ) - - const promises = paymentCollectionsToDelete.map((paymentCollection) => - deletePaymentCollection(paymentCollection.id) - ) - - await Promise.all(promises) - - await createPaymentCollection( - { order_id: order.id }, - { - onSuccess: (data) => { - setUrl( - `${MEDUSA_BACKEND_URL}/payment-collection/${data.payment_collection.id}` - ) - }, - onError: (err) => { - toast.error(err.message) - }, - onSettled: () => setIsCreating(false), - } - ) - } else { - setUrl( - `${MEDUSA_BACKEND_URL}/payment-collection/${activePaymentCollection.id}` - ) - } - } - setDone(true) - copy(url) + copy(`${MEDUSA_BACKEND_URL}/payment-collection/${paymentCollection.id}`) setTimeout(() => { setDone(false) - setUrl("") }, 2000) } @@ -109,7 +57,6 @@ const CopyPaymentLink = React.forwardRef( size="small" aria-label="CopyPaymentLink code snippet" onClick={copyToClipboard} - isLoading={isCreating} > {done ? ( @@ -118,7 +65,7 @@ const CopyPaymentLink = React.forwardRef( )} {t("orders.payment.paymentLink", { amount: getStylizedAmount( - order?.summary?.pending_difference, + paymentCollection.amount as number, order?.currency_code ), })} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index c616021064904..31658f7984d29 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -100,18 +100,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { return false }, [reservations]) - // TODO: We need a way to link payment collections to a change order to - // accurately differentiate order payments and order change payments - // This fix should be temporary. - const authorizedPaymentCollection = order.payment_collections.find( - (pc) => - pc.status === "authorized" && - pc.amount === order.summary?.pending_difference + const unpaidPaymentCollection = order.payment_collections.find( + (pc) => pc.status === "not_paid" ) const showPayment = - typeof authorizedPaymentCollection === "undefined" && - (order?.summary?.pending_difference || 0) > 0 + unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0 const showRefund = (order?.summary?.pending_difference || 0) < 0 return ( @@ -152,7 +146,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { )} - {showPayment && } + {showPayment && ( + + )} )} From 7cbe480f20d6c059b25c6b5b129df1cda38ff33a Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 18:08:21 +0200 Subject: [PATCH 3/6] Apply suggestions from code review Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> --- integration-tests/http/__tests__/claims/claims.spec.ts | 2 +- .../create-or-update-order-payment-collection.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 7ea1a86fc3160..5e7d79f655932 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -735,7 +735,7 @@ medusaIntegrationTestRunner({ ) }) - it("should create a payment collection successfully and throw on multiple", async () => { + it("should create a payment collection successfully", async () => { const orderForPayment = ( await api.get(`/admin/orders/${order.id}`, adminHeaders) ).data.order diff --git a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts index 33edd3a9f2e82..0cfb76da1f815 100644 --- a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts +++ b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts @@ -57,9 +57,9 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow( }).config({ name: "payment-collection-query" }) const amountPending = transform({ order, input }, ({ order, input }) => { - const pendingPayment = order.summary.pending_difference + const pendingPayment = order.summary.raw_pending_difference ?? order.summary.pending_difference - if (input.amount && input.amount > pendingPayment) { + if (MathBN.gt(input.amount ?? 0, pendingPayment)) { throw new MedusaError( MedusaError.Types.NOT_ALLOWED, `Amount cannot be greater than ${pendingPayment}` @@ -72,7 +72,7 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow( const updatedPaymentCollections = when( { existingPaymentCollection, amountPending }, ({ existingPaymentCollection, amountPending }) => { - return !!existingPaymentCollection?.id && amountPending > 0 + return !!existingPaymentCollection?.id && MathBN.gt(amountPending, 0) } ).then(() => { return updatePaymentCollectionStep({ @@ -86,7 +86,7 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow( const createdPaymentCollection = when( { existingPaymentCollection, amountPending }, ({ existingPaymentCollection, amountPending }) => { - return !!!existingPaymentCollection?.id && amountPending > 0 + return !!!existingPaymentCollection?.id && MathBN.gt(amountPending, 0) } ).then(() => { return createOrderPaymentCollectionWorkflow.runAsStep({ From 71eb52a38c6c90589706aaa93ca3a3a27e3ad3df Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 18:09:43 +0200 Subject: [PATCH 4/6] chore: fix mathbn --- .../workflows/create-or-update-order-payment-collection.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts index 0cfb76da1f815..c12a3b1dfc450 100644 --- a/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts +++ b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts @@ -1,5 +1,5 @@ import { PaymentCollectionDTO } from "@medusajs/types" -import { MedusaError, PaymentCollectionStatus } from "@medusajs/utils" +import { MathBN, MedusaError, PaymentCollectionStatus } from "@medusajs/utils" import { createWorkflow, transform, @@ -57,7 +57,8 @@ export const createOrUpdateOrderPaymentCollectionWorkflow = createWorkflow( }).config({ name: "payment-collection-query" }) const amountPending = transform({ order, input }, ({ order, input }) => { - const pendingPayment = order.summary.raw_pending_difference ?? order.summary.pending_difference + const pendingPayment = + order.summary.raw_pending_difference ?? order.summary.pending_difference if (MathBN.gt(input.amount ?? 0, pendingPayment)) { throw new MedusaError( From 9ff69d918dab7cf86e055766a64f167148cc3438 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 18:29:26 +0200 Subject: [PATCH 5/6] feat(dashboard,core-flows,js-sdk,types): ability to mark payment as paid --- .../http/__tests__/claims/claims.spec.ts | 34 ++++++-- .../src/hooks/api/payment-collections.tsx | 26 +++++++ .../dashboard/src/i18n/translations/en.json | 3 + .../order-summary-section.tsx | 60 ++++++++++++++ .../core-flows/src/order/workflows/index.ts | 1 + .../mark-payment-collection-as-paid.ts | 78 +++++++++++++++++++ .../js-sdk/src/admin/payment-collection.ts | 17 ++++ .../types/src/http/payment/admin/payloads.ts | 4 + .../[id]/mark-as-paid/route.ts | 31 ++++++++ .../admin/payment-collections/middlewares.ts | 12 +++ .../admin/payment-collections/query-config.ts | 4 + .../admin/payment-collections/validators.ts | 9 +++ 12 files changed, 273 insertions(+), 6 deletions(-) create mode 100644 packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts create mode 100644 packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 5e7d79f655932..c67bae48ae3ac 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -735,7 +735,8 @@ medusaIntegrationTestRunner({ ) }) - it("should create a payment collection successfully", async () => { + it("should create a payment collection successfully & mark as paid", async () => { + const paymentDelta = 171.5 const orderForPayment = ( await api.get(`/admin/orders/${order.id}`, adminHeaders) ).data.order @@ -746,12 +747,12 @@ medusaIntegrationTestRunner({ expect(paymentCollections[0]).toEqual( expect.objectContaining({ status: "not_paid", - amount: 171.5, + amount: paymentDelta, currency_code: "usd", }) ) - const paymentCollection = ( + const createdPaymentCollection = ( await api.post( `/admin/payment-collections`, { order_id: order.id, amount: 100 }, @@ -759,7 +760,7 @@ medusaIntegrationTestRunner({ ) ).data.payment_collection - expect(paymentCollection).toEqual( + expect(createdPaymentCollection).toEqual( expect.objectContaining({ currency_code: "usd", amount: 100, @@ -769,16 +770,37 @@ medusaIntegrationTestRunner({ const deleted = ( await api.delete( - `/admin/payment-collections/${paymentCollections[0].id}`, + `/admin/payment-collections/${createdPaymentCollection.id}`, adminHeaders ) ).data expect(deleted).toEqual({ - id: expect.any(String), + id: createdPaymentCollection.id, object: "payment-collection", deleted: true, }) + + const finalPaymentCollection = ( + await api.post( + `/admin/payment-collections/${paymentCollections[0].id}/mark-as-paid`, + { order_id: order.id }, + adminHeaders + ) + ).data.payment_collection + + console.log("finalPaymentCollection -- ", finalPaymentCollection) + + expect(finalPaymentCollection).toEqual( + expect.objectContaining({ + currency_code: "usd", + amount: paymentDelta, + status: "authorized", + authorized_amount: paymentDelta, + captured_amount: paymentDelta, + refunded_amount: 0, + }) + ) }) }) diff --git a/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx b/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx index 8164cda952c8e..557ff4d457fa9 100644 --- a/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/payment-collections.tsx @@ -35,6 +35,32 @@ export const useCreatePaymentCollection = ( }) } +export const useMarkPaymentCollectionAsPaid = ( + paymentCollectionId: string, + options?: UseMutationOptions< + HttpTypes.AdminPaymentCollectionResponse, + Error, + HttpTypes.AdminMarkPaymentCollectionAsPaid + > +) => { + return useMutation({ + mutationFn: (payload) => + sdk.admin.paymentCollection.markAsPaid(paymentCollectionId, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.all, + }) + + queryClient.invalidateQueries({ + queryKey: paymentCollectionQueryKeys.all, + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useDeletePaymentCollection = ( options?: Omit< UseMutationOptions< diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 4c890b03181eb..d3b6133df3401 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -814,6 +814,7 @@ "totalPaidByCustomer": "Total paid by customer", "capture": "Capture payment", "refund": "Refund", + "markAsPaid": "Mark as paid", "statusLabel": "Payment status", "statusTitle": "Payment Status", "status": { @@ -830,6 +831,8 @@ }, "capturePayment": "Payment of {{amount}} will be captured.", "capturePaymentSuccess": "Payment of {{amount}} successfully captured", + "markAsPaidPayment": "Payment of {{amount}} will be marked as paid.", + "markAsPaidPaymentSuccess": "Payment of {{amount}} successfully marked as paid", "createRefund": "Create Refund", "refundPaymentSuccess": "Refund of amount {{amount}} successful", "createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}", diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 31658f7984d29..c932fc11e9cd5 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -27,18 +27,23 @@ import { Heading, StatusBadge, Text, + toast, Tooltip, + usePrompt, } from "@medusajs/ui" +import { AdminPaymentCollection } from "../../../../../../../../core/types/dist/http/payment/admin/entities" import { ActionMenu } from "../../../../../components/common/action-menu" import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu.tsx" import { Thumbnail } from "../../../../../components/common/thumbnail" import { useClaims } from "../../../../../hooks/api/claims.tsx" import { useExchanges } from "../../../../../hooks/api/exchanges.tsx" import { useOrderPreview } from "../../../../../hooks/api/orders.tsx" +import { useMarkPaymentCollectionAsPaid } from "../../../../../hooks/api/payment-collections.tsx" import { useReservationItems } from "../../../../../hooks/api/reservations" import { useReturns } from "../../../../../hooks/api/returns" import { useDate } from "../../../../../hooks/use-date" +import { formatCurrency } from "../../../../../lib/format-currency.ts" import { getLocaleAmount, getStylizedAmount, @@ -54,6 +59,7 @@ type OrderSummarySectionProps = { export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { const { t } = useTranslation() const navigate = useNavigate() + const prompt = usePrompt() const { reservations } = useReservationItems( { @@ -104,10 +110,54 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { (pc) => pc.status === "not_paid" ) + const { mutateAsync: markAsPaid } = useMarkPaymentCollectionAsPaid( + unpaidPaymentCollection?.id! + ) + const showPayment = unpaidPaymentCollection && (order?.summary?.pending_difference || 0) > 0 const showRefund = (order?.summary?.pending_difference || 0) < 0 + const handleMarkAsPaid = async ( + paymentCollection: AdminPaymentCollection + ) => { + const res = await prompt({ + title: t("orders.payment.markAsPaid"), + description: t("orders.payment.markAsPaidPayment", { + amount: formatCurrency( + paymentCollection.amount as number, + order.currency_code + ), + }), + confirmText: t("actions.confirm"), + cancelText: t("actions.cancel"), + variant: "confirmation", + }) + + if (!res) { + return + } + + await markAsPaid( + { order_id: order.id }, + { + onSuccess: () => { + toast.success( + t("orders.payment.markAsPaidPaymentSuccess", { + amount: formatCurrency( + paymentCollection.amount as number, + order.currency_code + ), + }) + ) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + return (
@@ -152,6 +202,16 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { order={order} /> )} + + {showPayment && ( + + )} )} diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 92273db68e1b6..f6e6294c9c1e0 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -41,6 +41,7 @@ export * from "./exchange/update-exchange-add-item" export * from "./exchange/update-exchange-shipping-method" export * from "./get-order-detail" export * from "./get-orders-list" +export * from "./mark-payment-collection-as-paid" export * from "./order-edit/begin-order-edit" export * from "./order-edit/cancel-begin-order-edit" export * from "./order-edit/confirm-order-edit-request" diff --git a/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts b/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts new file mode 100644 index 0000000000000..fea5e7ef05161 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts @@ -0,0 +1,78 @@ +import { PaymentCollectionDTO } from "@medusajs/types" +import { MedusaError } from "@medusajs/utils" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, +} from "@medusajs/workflows-sdk" +import { useRemoteQueryStep } from "../../common" +import { createPaymentSessionsWorkflow } from "../../definition" +import { + authorizePaymentSessionStep, + capturePaymentWorkflow, +} from "../../payment" + +/** + * This step validates that the payment collection is not_paid + */ +export const throwUnlessPaymentCollectionNotPaid = createStep( + "validate-existing-payment-collection", + ({ paymentCollection }: { paymentCollection: PaymentCollectionDTO }) => { + if (paymentCollection.status !== "not_paid") { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Can only mark 'not_paid' payment collection as paid` + ) + } + } +) + +const systemPaymentProviderId = "pp_system_default" +export const markPaymentCollectionAsPaidId = "mark-payment-collection-as-paid" +/** + * This workflow marks a payment collection for an order as paid. + */ +export const markPaymentCollectionAsPaid = createWorkflow( + markPaymentCollectionAsPaidId, + ( + input: WorkflowData<{ + payment_collection_id: string + order_id: string + }> + ) => { + const paymentCollection = useRemoteQueryStep({ + entry_point: "payment_collection", + fields: ["id", "status", "amount"], + variables: { id: input.payment_collection_id }, + throw_if_key_not_found: true, + list: false, + }) + + throwUnlessPaymentCollectionNotPaid({ paymentCollection }) + + const paymentSession = createPaymentSessionsWorkflow.runAsStep({ + input: { + payment_collection_id: paymentCollection.id, + provider_id: systemPaymentProviderId, + data: {}, + context: {}, + }, + }) + + const payment = authorizePaymentSessionStep({ + id: paymentSession.id, + context: { order_id: input.order_id }, + }) + + capturePaymentWorkflow.runAsStep({ + input: { + payment_id: payment.id, + captured_by: "req.auth_context.actor_id", + amount: paymentCollection.amount, + }, + }) + + return new WorkflowResponse(payment) + } +) diff --git a/packages/core/js-sdk/src/admin/payment-collection.ts b/packages/core/js-sdk/src/admin/payment-collection.ts index ca8dacc0cfb3c..12551002a7d39 100644 --- a/packages/core/js-sdk/src/admin/payment-collection.ts +++ b/packages/core/js-sdk/src/admin/payment-collection.ts @@ -60,4 +60,21 @@ export class PaymentCollection { } ) } + + async markAsPaid( + id: string, + body: HttpTypes.AdminMarkPaymentCollectionAsPaid, + query?: SelectParams, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/payment-collections/${id}/mark-as-paid`, + { + method: "POST", + headers, + body, + query, + } + ) + } } diff --git a/packages/core/types/src/http/payment/admin/payloads.ts b/packages/core/types/src/http/payment/admin/payloads.ts index 3396965b1a0ef..b407662567bc7 100644 --- a/packages/core/types/src/http/payment/admin/payloads.ts +++ b/packages/core/types/src/http/payment/admin/payloads.ts @@ -17,3 +17,7 @@ export interface AdminCreatePaymentCollection { order_id: string amount?: number } + +export interface AdminMarkPaymentCollectionAsPaid { + order_id: string +} diff --git a/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts b/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts new file mode 100644 index 0000000000000..6037c60efcc71 --- /dev/null +++ b/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts @@ -0,0 +1,31 @@ +import { markPaymentCollectionAsPaid } from "@medusajs/core-flows" +import { HttpTypes } from "@medusajs/types" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../../types/routing" +import { refetchEntity } from "../../../../utils/refetch-entity" +import { AdminMarkPaymentCollectionPaidType } from "../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { id } = req.params + + await markPaymentCollectionAsPaid(req.scope).run({ + input: { + ...req.body, + payment_collection_id: id, + }, + }) + + const paymentCollection = await refetchEntity( + "payment_collection", + id, + req.scope, + req.remoteQueryConfig.fields + ) + + res.status(200).json({ payment_collection: paymentCollection }) +} diff --git a/packages/medusa/src/api/admin/payment-collections/middlewares.ts b/packages/medusa/src/api/admin/payment-collections/middlewares.ts index 4d8400dad6573..9ab5f6519908d 100644 --- a/packages/medusa/src/api/admin/payment-collections/middlewares.ts +++ b/packages/medusa/src/api/admin/payment-collections/middlewares.ts @@ -5,6 +5,7 @@ import * as queryConfig from "./query-config" import { AdminCreatePaymentCollection, AdminGetPaymentCollectionParams, + AdminMarkPaymentCollectionPaid, } from "./validators" export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [ @@ -19,6 +20,17 @@ export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/payment-collections/:id/mark-as-paid", + middlewares: [ + validateAndTransformBody(AdminMarkPaymentCollectionPaid), + validateAndTransformQuery( + AdminGetPaymentCollectionParams, + queryConfig.retrievePaymentCollectionTransformQueryConfig + ), + ], + }, { method: ["DELETE"], matcher: "/admin/payment-collections/:id", diff --git a/packages/medusa/src/api/admin/payment-collections/query-config.ts b/packages/medusa/src/api/admin/payment-collections/query-config.ts index 5dc3aa9d48392..00372fff775e5 100644 --- a/packages/medusa/src/api/admin/payment-collections/query-config.ts +++ b/packages/medusa/src/api/admin/payment-collections/query-config.ts @@ -3,7 +3,11 @@ export const defaultPaymentCollectionFields = [ "currency_code", "amount", "status", + "authorized_amount", + "captured_amount", + "refunded_amount", "*payment_sessions", + "*payments", ] export const retrievePaymentCollectionTransformQueryConfig = { diff --git a/packages/medusa/src/api/admin/payment-collections/validators.ts b/packages/medusa/src/api/admin/payment-collections/validators.ts index d9d6c9913fac7..e3b424f979056 100644 --- a/packages/medusa/src/api/admin/payment-collections/validators.ts +++ b/packages/medusa/src/api/admin/payment-collections/validators.ts @@ -15,3 +15,12 @@ export const AdminCreatePaymentCollection = z amount: z.number(), }) .strict() + +export type AdminMarkPaymentCollectionPaidType = z.infer< + typeof AdminMarkPaymentCollectionPaid +> +export const AdminMarkPaymentCollectionPaid = z + .object({ + order_id: z.string(), + }) + .strict() From 4ba7988ac1d2f0fd36d0aba8d4564338c0e49e11 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 18:40:24 +0200 Subject: [PATCH 6/6] chore: add captured bt --- .../src/order/workflows/mark-payment-collection-as-paid.ts | 3 ++- .../api/admin/payment-collections/[id]/mark-as-paid/route.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts b/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts index fea5e7ef05161..d02029183adf5 100644 --- a/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts +++ b/packages/core/core-flows/src/order/workflows/mark-payment-collection-as-paid.ts @@ -39,6 +39,7 @@ export const markPaymentCollectionAsPaid = createWorkflow( input: WorkflowData<{ payment_collection_id: string order_id: string + captured_by?: string }> ) => { const paymentCollection = useRemoteQueryStep({ @@ -68,7 +69,7 @@ export const markPaymentCollectionAsPaid = createWorkflow( capturePaymentWorkflow.runAsStep({ input: { payment_id: payment.id, - captured_by: "req.auth_context.actor_id", + captured_by: input.captured_by, amount: paymentCollection.amount, }, }) diff --git a/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts b/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts index 6037c60efcc71..46767f49b7f93 100644 --- a/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts +++ b/packages/medusa/src/api/admin/payment-collections/[id]/mark-as-paid/route.ts @@ -17,6 +17,7 @@ export const POST = async ( input: { ...req.body, payment_collection_id: id, + captured_by: req.auth_context.actor_id, }, })