From 430d9a38c466dcac20fe54279ea3336e010a8806 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Tue, 20 Aug 2024 18:40:58 +0200 Subject: [PATCH] feat(core-flows): create or update payment collections in RMA flows (#8676) * feat(core-flows): create or update payment collections in RMA flows * chore: change ui to pick payment link from unpaid payment collection * Apply suggestions from code review Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> * chore: fix mathbn --------- Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> --- .../http/__tests__/claims/claims.spec.ts | 59 ++++++---- .../copy-payment-link/copy-payment-link.tsx | 65 +---------- .../order-summary-section.tsx | 19 ++- .../workflows/claim/confirm-claim-request.ts | 7 ++ ...eate-or-update-order-payment-collection.ts | 109 ++++++++++++++++++ .../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 +- 10 files changed, 193 insertions(+), 162 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 0118e0bc58..5e7d79f655 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 ) @@ -720,13 +735,26 @@ medusaIntegrationTestRunner({ ) }) - it("should create a payment collection successfully and throw on multiple", async () => { - const paymentDelta = 171.5 + it("should create a payment collection successfully", async () => { + 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/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 68fb73d4b7..681da8a07a 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 c616021064..31658f7984 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 && ( + + )} )} 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 9557fa4485..4229a5afec 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 0000000000..c12a3b1dfc --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/create-or-update-order-payment-collection.ts @@ -0,0 +1,109 @@ +import { PaymentCollectionDTO } from "@medusajs/types" +import { MathBN, 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.raw_pending_difference ?? order.summary.pending_difference + + if (MathBN.gt(input.amount ?? 0, 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 && MathBN.gt(amountPending, 0) + } + ).then(() => { + return updatePaymentCollectionStep({ + selector: { id: existingPaymentCollection.id }, + update: { + amount: amountPending, + }, + }) as PaymentCollectionDTO[] + }) + + const createdPaymentCollection = when( + { existingPaymentCollection, amountPending }, + ({ existingPaymentCollection, amountPending }) => { + return !!!existingPaymentCollection?.id && MathBN.gt(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 38fd4d8d18..af08759fdd 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 f72d74c448..a305ac1ec2 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 5b8e76f143..68a68741d1 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 f10f110129..7a884cdb79 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 5f3c6c6daf..d9d6c9913f 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()