feat(dashboard,core-flows,js-sdk,types,link-modules,payment): ability to copy payment link (#8630)
what: - enables a button to create a payment link when a payment delta is present - api to delete order payment collection - adds a pending amount to payment collections Note: Not the happiest with the decision on when to create a payment collection and when not to. The code should programatically create or delete payment collections currently to generate the right collection for the payment delta. Adding a more specific flow to create and manage a payment collection will help reduce this burden from the code path and onto CX/merchant. Another issue I found is that the payment collection status doesn't get updated when payment is complete as it still gets stuck to "authorized" state https://github.com/user-attachments/assets/037a10f9-3621-43c2-94ba-1ada4b0a041b
This commit is contained in:
@@ -721,7 +721,7 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should create a payment collection successfully and throw on multiple", async () => {
|
it("should create a payment collection successfully and throw on multiple", async () => {
|
||||||
const paymentDelta = 110.5
|
const paymentDelta = 171.5
|
||||||
|
|
||||||
const paymentCollection = (
|
const paymentCollection = (
|
||||||
await api.post(
|
await api.post(
|
||||||
@@ -752,6 +752,19 @@ medusaIntegrationTestRunner({
|
|||||||
message:
|
message:
|
||||||
"Active payment collections were found. Complete existing ones or delete them before proceeding.",
|
"Active payment collections were found. Complete existing ones or delete them before proceeding.",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deleted = (
|
||||||
|
await api.delete(
|
||||||
|
`/admin/payment-collections/${paymentCollection.id}`,
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
).data
|
||||||
|
|
||||||
|
expect(deleted).toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
object: "payment-collection",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export * from "./inventory"
|
|||||||
export * from "./invites"
|
export * from "./invites"
|
||||||
export * from "./notification"
|
export * from "./notification"
|
||||||
export * from "./orders"
|
export * from "./orders"
|
||||||
|
export * from "./payment-collections"
|
||||||
export * from "./payments"
|
export * from "./payments"
|
||||||
export * from "./price-lists"
|
export * from "./price-lists"
|
||||||
export * from "./product-types"
|
export * from "./product-types"
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ export const useCreateOrderFulfillment = (
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.details(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersQueryKeys.preview(orderId),
|
||||||
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
@@ -107,6 +112,11 @@ export const useCancelOrderFulfillment = (
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.details(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersQueryKeys.preview(orderId),
|
||||||
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
@@ -129,6 +139,11 @@ export const useCreateOrderShipment = (
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.details(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersQueryKeys.preview(orderId),
|
||||||
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
@@ -136,18 +151,15 @@ export const useCreateOrderShipment = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useCancelOrder = (
|
export const useCancelOrder = (
|
||||||
orderId: string,
|
|
||||||
options?: UseMutationOptions<any, Error, any>
|
options?: UseMutationOptions<any, Error, any>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: () => sdk.admin.order.cancel(orderId),
|
mutationFn: (id) => sdk.admin.order.cancel(id),
|
||||||
onSuccess: (data: any, variables: any, context: any) => {
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.all,
|
||||||
})
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ordersQueryKeys.lists(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
},
|
},
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { useMutation, UseMutationOptions } from "@tanstack/react-query"
|
||||||
|
import { sdk } from "../../lib/client"
|
||||||
|
import { queryClient } from "../../lib/query-client"
|
||||||
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { ordersQueryKeys } from "./orders"
|
||||||
|
|
||||||
|
const PAYMENT_COLLECTION_QUERY_KEY = "payment-collection" as const
|
||||||
|
export const paymentCollectionQueryKeys = queryKeysFactory(
|
||||||
|
PAYMENT_COLLECTION_QUERY_KEY
|
||||||
|
)
|
||||||
|
|
||||||
|
export const useCreatePaymentCollection = (
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminPaymentCollectionResponse,
|
||||||
|
Error,
|
||||||
|
HttpTypes.AdminCreatePaymentCollection
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) => sdk.admin.paymentCollection.create(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<
|
||||||
|
HttpTypes.AdminDeletePaymentCollectionResponse,
|
||||||
|
FetchError,
|
||||||
|
string
|
||||||
|
>,
|
||||||
|
"mutationFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: string) => sdk.admin.paymentCollection.delete(id),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ordersQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: paymentCollectionQueryKeys.all,
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -70,11 +70,7 @@ export const useCapturePayment = (
|
|||||||
mutationFn: (payload) => sdk.admin.payment.capture(paymentId, payload),
|
mutationFn: (payload) => sdk.admin.payment.capture(paymentId, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.all,
|
||||||
})
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ordersQueryKeys.lists(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
@@ -95,11 +91,7 @@ export const useRefundPayment = (
|
|||||||
mutationFn: (payload) => sdk.admin.payment.refund(paymentId, payload),
|
mutationFn: (payload) => sdk.admin.payment.refund(paymentId, payload),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ordersQueryKeys.details(),
|
queryKey: ordersQueryKeys.all,
|
||||||
})
|
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ordersQueryKeys.lists(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
options?.onSuccess?.(data, variables, context)
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { sdk } from "../../lib/client"
|
|||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
|
||||||
const REFUND_REASON_QUERY_KEY = "refund-reason" as const
|
const REFUND_REASON_QUERY_KEY = "refund-reason" as const
|
||||||
export const paymentQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)
|
export const refundReasonQueryKeys = queryKeysFactory(REFUND_REASON_QUERY_KEY)
|
||||||
|
|
||||||
export const useRefundReasons = (
|
export const useRefundReasons = (
|
||||||
query?: HttpTypes.RefundReasonFilters,
|
query?: HttpTypes.RefundReasonFilters,
|
||||||
|
|||||||
@@ -830,7 +830,9 @@
|
|||||||
"capturePaymentSuccess": "Payment of {{amount}} successfully captured",
|
"capturePaymentSuccess": "Payment of {{amount}} successfully captured",
|
||||||
"createRefund": "Create Refund",
|
"createRefund": "Create Refund",
|
||||||
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
"refundPaymentSuccess": "Refund of amount {{amount}} successful",
|
||||||
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}"
|
"createRefundWrongQuantity": "Quantity should be a number between 1 and {{number}}",
|
||||||
|
"refundAmount": "Refund {{ amount }}",
|
||||||
|
"paymentLink": "Copy payment link for {{ amount }}"
|
||||||
},
|
},
|
||||||
|
|
||||||
"edits": {
|
"edits": {
|
||||||
|
|||||||
@@ -10,3 +10,12 @@ export const getTotalCaptured = (
|
|||||||
(paymentCollection.refunded_amount as number))
|
(paymentCollection.refunded_amount as number))
|
||||||
return acc
|
return acc
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
|
export const getTotalPending = (paymentCollections: AdminPaymentCollection[]) =>
|
||||||
|
paymentCollections.reduce((acc, paymentCollection) => {
|
||||||
|
acc +=
|
||||||
|
(paymentCollection.amount as number) -
|
||||||
|
(paymentCollection.captured_amount as number)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, 0)
|
||||||
|
|||||||
+12
-1
@@ -7,6 +7,7 @@ import { AdminOrder } from "@medusajs/types"
|
|||||||
import { Alert, Button, Select, Switch, toast } from "@medusajs/ui"
|
import { Alert, Button, Select, Switch, toast } from "@medusajs/ui"
|
||||||
import { useForm, useWatch } from "react-hook-form"
|
import { useForm, useWatch } from "react-hook-form"
|
||||||
|
|
||||||
|
import { OrderLineItemDTO } from "@medusajs/types"
|
||||||
import { Form } from "../../../../../components/common/form"
|
import { Form } from "../../../../../components/common/form"
|
||||||
import {
|
import {
|
||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
@@ -118,8 +119,18 @@ export function OrderCreateFulfillmentForm({
|
|||||||
|
|
||||||
if (itemsToFulfill?.length) {
|
if (itemsToFulfill?.length) {
|
||||||
setFulfillableItems(itemsToFulfill)
|
setFulfillableItems(itemsToFulfill)
|
||||||
|
|
||||||
|
const quantityMap = fulfillableItems.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc[item.id] = getFulfillableQuantity(item as OrderLineItemDTO)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
)
|
||||||
|
|
||||||
|
form.setValue("quantity", quantityMap)
|
||||||
}
|
}
|
||||||
}, [order.items])
|
}, [order.items?.length])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
|
|||||||
+3
-3
@@ -11,8 +11,8 @@ import {
|
|||||||
RouteFocusModal,
|
RouteFocusModal,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/modals"
|
} from "../../../../../components/modals"
|
||||||
import { CreateShipmentSchema } from "./constants"
|
|
||||||
import { useCreateOrderShipment } from "../../../../../hooks/api"
|
import { useCreateOrderShipment } from "../../../../../hooks/api"
|
||||||
|
import { CreateShipmentSchema } from "./constants"
|
||||||
|
|
||||||
type OrderCreateFulfillmentFormProps = {
|
type OrderCreateFulfillmentFormProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
@@ -27,7 +27,7 @@ export function OrderCreateShipmentForm({
|
|||||||
const { handleSuccess } = useRouteModal()
|
const { handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
const { mutateAsync: createShipment, isPending: isMutating } =
|
const { mutateAsync: createShipment, isPending: isMutating } =
|
||||||
useCreateOrderShipment(order.id, fulfillment.id)
|
useCreateOrderShipment(order.id, fulfillment?.id)
|
||||||
|
|
||||||
const form = useForm<zod.infer<typeof CreateShipmentSchema>>({
|
const form = useForm<zod.infer<typeof CreateShipmentSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -44,7 +44,7 @@ export function OrderCreateShipmentForm({
|
|||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
await createShipment(
|
await createShipment(
|
||||||
{
|
{
|
||||||
items: fulfillment.items.map((i) => ({
|
items: fulfillment?.items?.map((i) => ({
|
||||||
id: i.line_item_id,
|
id: i.line_item_id,
|
||||||
quantity: i.quantity,
|
quantity: i.quantity,
|
||||||
})),
|
})),
|
||||||
|
|||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import { CheckCircleSolid, SquareTwoStack } from "@medusajs/icons"
|
||||||
|
import { AdminOrder } from "@medusajs/types"
|
||||||
|
import { Button, toast, 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 = {
|
||||||
|
order: AdminOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component is based on the `button` element and supports all of its props
|
||||||
|
*/
|
||||||
|
const CopyPaymentLink = React.forwardRef<any, CopyPaymentLinkProps>(
|
||||||
|
({ order }: CopyPaymentLinkProps, ref) => {
|
||||||
|
const [isCreating, setIsCreating] = useState(false)
|
||||||
|
const [url, setUrl] = useState("")
|
||||||
|
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:
|
||||||
|
| React.MouseEvent<HTMLElement, MouseEvent>
|
||||||
|
| React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||||
|
) => {
|
||||||
|
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)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setDone(false)
|
||||||
|
setUrl("")
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (done) {
|
||||||
|
setText("Copied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setText("CopyPaymentLink")
|
||||||
|
}, 500)
|
||||||
|
}, [done])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={text} open={done || open} onOpenChange={setOpen}>
|
||||||
|
<Button
|
||||||
|
ref={ref}
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
aria-label="CopyPaymentLink code snippet"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
isLoading={isCreating}
|
||||||
|
>
|
||||||
|
{done ? (
|
||||||
|
<CheckCircleSolid className="inline" />
|
||||||
|
) : (
|
||||||
|
<SquareTwoStack className="inline" />
|
||||||
|
)}
|
||||||
|
{t("orders.payment.paymentLink", {
|
||||||
|
amount: getStylizedAmount(
|
||||||
|
order?.summary?.pending_difference,
|
||||||
|
order?.currency_code
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
CopyPaymentLink.displayName = "CopyPaymentLink"
|
||||||
|
|
||||||
|
export { CopyPaymentLink }
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./copy-payment-link"
|
||||||
+7
-2
@@ -1,5 +1,10 @@
|
|||||||
import { Buildings, XCircle } from "@medusajs/icons"
|
import { Buildings, XCircle } from "@medusajs/icons"
|
||||||
import { AdminOrder, FulfillmentDTO, OrderLineItemDTO } from "@medusajs/types"
|
import {
|
||||||
|
AdminOrder,
|
||||||
|
AdminOrderFulfillment,
|
||||||
|
HttpTypes,
|
||||||
|
OrderLineItemDTO,
|
||||||
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Container,
|
||||||
@@ -152,7 +157,7 @@ const Fulfillment = ({
|
|||||||
order,
|
order,
|
||||||
index,
|
index,
|
||||||
}: {
|
}: {
|
||||||
fulfillment: FulfillmentDTO
|
fulfillment: AdminOrderFulfillment
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
index: number
|
index: number
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
+3
-3
@@ -11,11 +11,11 @@ import {
|
|||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useCancelOrder } from "../../../../../hooks/api/orders"
|
||||||
import {
|
import {
|
||||||
getOrderFulfillmentStatus,
|
getOrderFulfillmentStatus,
|
||||||
getOrderPaymentStatus,
|
getOrderPaymentStatus,
|
||||||
} from "../../../../../lib/order-helpers"
|
} from "../../../../../lib/order-helpers"
|
||||||
import { useCancelOrder } from "../../../../../hooks/api/orders"
|
|
||||||
|
|
||||||
type OrderGeneralSectionProps = {
|
type OrderGeneralSectionProps = {
|
||||||
order: Order
|
order: Order
|
||||||
@@ -25,7 +25,7 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const prompt = usePrompt()
|
const prompt = usePrompt()
|
||||||
|
|
||||||
const { mutateAsync } = useCancelOrder(order.id)
|
const { mutateAsync: cancelOrder } = useCancelOrder()
|
||||||
|
|
||||||
const handleCancel = async () => {
|
const handleCancel = async () => {
|
||||||
const res = await prompt({
|
const res = await prompt({
|
||||||
@@ -41,7 +41,7 @@ export const OrderGeneralSection = ({ order }: OrderGeneralSectionProps) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await mutateAsync()
|
await cancelOrder(order.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+27
-8
@@ -25,7 +25,7 @@ import {
|
|||||||
getStylizedAmount,
|
getStylizedAmount,
|
||||||
} from "../../../../../lib/money-amount-helpers"
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
import { getOrderPaymentStatus } from "../../../../../lib/order-helpers"
|
import { getOrderPaymentStatus } from "../../../../../lib/order-helpers"
|
||||||
import { getTotalCaptured } from "../../../../../lib/payment"
|
import { getTotalCaptured, getTotalPending } from "../../../../../lib/payment"
|
||||||
|
|
||||||
type OrderPaymentSectionProps = {
|
type OrderPaymentSectionProps = {
|
||||||
order: HttpTypes.AdminOrder
|
order: HttpTypes.AdminOrder
|
||||||
@@ -321,15 +321,34 @@ const Total = ({
|
|||||||
currencyCode: string
|
currencyCode: string
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const totalPending = getTotalPending(paymentCollections)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div>
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
{t("orders.payment.totalPaidByCustomer")}
|
<Text size="small" weight="plus" leading="compact">
|
||||||
</Text>
|
{t("orders.payment.totalPaidByCustomer")}
|
||||||
<Text size="small" weight="plus" leading="compact">
|
</Text>
|
||||||
{getStylizedAmount(getTotalCaptured(paymentCollections), currencyCode)}
|
|
||||||
</Text>
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{getStylizedAmount(
|
||||||
|
getTotalCaptured(paymentCollections),
|
||||||
|
currencyCode
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPending > 0 && (
|
||||||
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
Total pending
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{getStylizedAmount(totalPending, currencyCode)}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-34
@@ -45,6 +45,7 @@ import {
|
|||||||
} from "../../../../../lib/money-amount-helpers"
|
} from "../../../../../lib/money-amount-helpers"
|
||||||
import { getTotalCaptured } from "../../../../../lib/payment.ts"
|
import { getTotalCaptured } from "../../../../../lib/payment.ts"
|
||||||
import { getReturnableQuantity } from "../../../../../lib/rma.ts"
|
import { getReturnableQuantity } from "../../../../../lib/rma.ts"
|
||||||
|
import { CopyPaymentLink } from "../copy-payment-link/copy-payment-link.tsx"
|
||||||
|
|
||||||
type OrderSummarySectionProps = {
|
type OrderSummarySectionProps = {
|
||||||
order: AdminOrder
|
order: AdminOrder
|
||||||
@@ -54,9 +55,12 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { reservations } = useReservationItems({
|
const { reservations } = useReservationItems(
|
||||||
line_item_id: order.items.map((i) => i.id),
|
{
|
||||||
})
|
line_item_id: order?.items?.map((i) => i.id),
|
||||||
|
},
|
||||||
|
{ enabled: Array.isArray(order?.items) }
|
||||||
|
)
|
||||||
|
|
||||||
const { order: orderPreview } = useOrderPreview(order.id!)
|
const { order: orderPreview } = useOrderPreview(order.id!)
|
||||||
|
|
||||||
@@ -96,6 +100,20 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
|||||||
return false
|
return false
|
||||||
}, [reservations])
|
}, [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 showPayment =
|
||||||
|
typeof authorizedPaymentCollection === "undefined" &&
|
||||||
|
(order?.summary?.pending_difference || 0) > 0
|
||||||
|
const showRefund = (order?.summary?.pending_difference || 0) < 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="divide-y divide-dashed p-0">
|
<Container className="divide-y divide-dashed p-0">
|
||||||
<Header order={order} orderPreview={orderPreview} />
|
<Header order={order} orderPreview={orderPreview} />
|
||||||
@@ -103,38 +121,40 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
|
|||||||
<CostBreakdown order={order} />
|
<CostBreakdown order={order} />
|
||||||
<Total order={order} />
|
<Total order={order} />
|
||||||
|
|
||||||
{showAllocateButton ||
|
{(showAllocateButton || showReturns || showPayment) && (
|
||||||
(showReturns && (
|
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4 gap-x-2">
|
||||||
<div className="bg-ui-bg-subtle flex items-center justify-end rounded-b-xl px-4 py-4">
|
{showReturns && (
|
||||||
{showReturns && (
|
<ButtonMenu
|
||||||
<ButtonMenu
|
groups={[
|
||||||
groups={[
|
{
|
||||||
{
|
actions: returns.map((r) => ({
|
||||||
actions: returns.map((r) => ({
|
label: t("orders.returns.receive.receive", {
|
||||||
label: t("orders.returns.receive.receive", {
|
label: `#${r.id.slice(-7)}`,
|
||||||
label: `#${r.id.slice(-7)}`,
|
}),
|
||||||
}),
|
icon: <ArrowLongRight />,
|
||||||
icon: <ArrowLongRight />,
|
to: `/orders/${order.id}/returns/${r.id}/receive`,
|
||||||
to: `/orders/${order.id}/returns/${r.id}/receive`,
|
})),
|
||||||
})),
|
},
|
||||||
},
|
]}
|
||||||
]}
|
>
|
||||||
>
|
<Button variant="secondary" size="small">
|
||||||
<Button variant="secondary" size="small">
|
{t("orders.returns.receive.action")}
|
||||||
{t("orders.returns.receive.action")}
|
|
||||||
</Button>
|
|
||||||
</ButtonMenu>
|
|
||||||
)}
|
|
||||||
{showAllocateButton && (
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate(`./allocate-items`)}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
{t("orders.allocateItems.action")}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</ButtonMenu>
|
||||||
</div>
|
)}
|
||||||
))}
|
|
||||||
|
{showAllocateButton && (
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(`./allocate-items`)}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
{t("orders.allocateItems.action")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showPayment && <CopyPaymentLink order={order} />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-9
@@ -1,18 +1,16 @@
|
|||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { AdminOrder, AdminReturn } from "@medusajs/types"
|
|
||||||
import { Alert, Button, Input, Switch, Text, toast } from "@medusajs/ui"
|
|
||||||
import React, { useEffect, useMemo } from "react"
|
|
||||||
import { useForm } from "react-hook-form"
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { ArrrowRight } from "@medusajs/icons"
|
import { ArrrowRight } from "@medusajs/icons"
|
||||||
|
import { AdminOrder, AdminReturn } from "@medusajs/types"
|
||||||
|
import { Alert, Button, Input, Switch, Text, toast } from "@medusajs/ui"
|
||||||
|
import { useEffect, useMemo } from "react"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
|
|
||||||
|
import { Form } from "../../../../../components/common/form"
|
||||||
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
import { Thumbnail } from "../../../../../components/common/thumbnail"
|
||||||
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
import { RouteDrawer, useRouteModal } from "../../../../../components/modals"
|
||||||
import { useStockLocation } from "../../../../../hooks/api"
|
import { useStockLocation } from "../../../../../hooks/api"
|
||||||
import { ReceiveReturnSchema } from "./constants"
|
|
||||||
import { Form } from "../../../../../components/common/form"
|
|
||||||
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
|
||||||
import {
|
import {
|
||||||
useAddReceiveItems,
|
useAddReceiveItems,
|
||||||
useCancelReceiveReturn,
|
useCancelReceiveReturn,
|
||||||
@@ -20,6 +18,8 @@ import {
|
|||||||
useRemoveReceiveItems,
|
useRemoveReceiveItems,
|
||||||
useUpdateReceiveItem,
|
useUpdateReceiveItem,
|
||||||
} from "../../../../../hooks/api/returns"
|
} from "../../../../../hooks/api/returns"
|
||||||
|
import { getStylizedAmount } from "../../../../../lib/money-amount-helpers"
|
||||||
|
import { ReceiveReturnSchema } from "./constants"
|
||||||
import WrittenOffQuantity from "./written-off-quantity"
|
import WrittenOffQuantity from "./written-off-quantity"
|
||||||
|
|
||||||
type OrderAllocateItemsFormProps = {
|
type OrderAllocateItemsFormProps = {
|
||||||
@@ -318,7 +318,7 @@ export function OrderReceiveReturnForm({
|
|||||||
</span>
|
</span>
|
||||||
<span className="txt-small font-medium">
|
<span className="txt-small font-medium">
|
||||||
{getStylizedAmount(
|
{getStylizedAmount(
|
||||||
preview.summary.difference_sum || 0,
|
preview.summary.pending_difference || 0,
|
||||||
order.currency_code
|
order.currency_code
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_MEDUSA_ADMIN_BACKEND_URL: string
|
readonly VITE_MEDUSA_ADMIN_BACKEND_URL: string
|
||||||
|
readonly VITE_MEDUSA_STOREFRONT_URL: string
|
||||||
readonly VITE_MEDUSA_V2: "true" | "false"
|
readonly VITE_MEDUSA_V2: "true" | "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,4 +11,5 @@ interface ImportMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
declare const __BACKEND_URL__: string | undefined
|
declare const __BACKEND_URL__: string | undefined
|
||||||
|
declare const __STOREFRONT_URL__: string | undefined
|
||||||
declare const __BASE__: string
|
declare const __BASE__: string
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export default defineConfig(({ mode }) => {
|
|||||||
|
|
||||||
const BASE = env.VITE_MEDUSA_BASE || "/"
|
const BASE = env.VITE_MEDUSA_BASE || "/"
|
||||||
const BACKEND_URL = env.VITE_MEDUSA_BACKEND_URL || "http://localhost:9000"
|
const BACKEND_URL = env.VITE_MEDUSA_BACKEND_URL || "http://localhost:9000"
|
||||||
|
const STOREFRONT_URL =
|
||||||
|
env.VITE_MEDUSA_STOREFRONT_URL || "http://localhost:8000"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add this to your .env file to specify the project to load admin extensions from.
|
* Add this to your .env file to specify the project to load admin extensions from.
|
||||||
@@ -25,6 +27,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
define: {
|
define: {
|
||||||
__BASE__: JSON.stringify(BASE),
|
__BASE__: JSON.stringify(BASE),
|
||||||
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
|
__BACKEND_URL__: JSON.stringify(BACKEND_URL),
|
||||||
|
__STOREFRONT_URL__: JSON.stringify(STOREFRONT_URL),
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
open: true,
|
open: true,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export interface DeleteEntitiesStepType {
|
||||||
|
moduleRegistrationName: string
|
||||||
|
invokeMethod: string
|
||||||
|
compensateMethod: string
|
||||||
|
entityIdentifier?: string
|
||||||
|
data: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteEntitiesStepId = "delete-entities-step"
|
||||||
|
/**
|
||||||
|
* This step deletes one or more entities.
|
||||||
|
*/
|
||||||
|
export const deleteEntitiesStep = createStep(
|
||||||
|
deleteEntitiesStepId,
|
||||||
|
async (input: DeleteEntitiesStepType, { container }) => {
|
||||||
|
const {
|
||||||
|
moduleRegistrationName,
|
||||||
|
invokeMethod,
|
||||||
|
compensateMethod,
|
||||||
|
data = [],
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const module = container.resolve<any>(moduleRegistrationName)
|
||||||
|
data.length ? await module[invokeMethod](data) : []
|
||||||
|
|
||||||
|
return new StepResponse(void 0, {
|
||||||
|
entityIdentifiers: input.data,
|
||||||
|
moduleRegistrationName,
|
||||||
|
compensateMethod,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async (compensateInput, { container }) => {
|
||||||
|
const {
|
||||||
|
entityIdentifiers = [],
|
||||||
|
moduleRegistrationName,
|
||||||
|
compensateMethod,
|
||||||
|
} = compensateInput!
|
||||||
|
|
||||||
|
if (!entityIdentifiers?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = container.resolve<any>(moduleRegistrationName)
|
||||||
|
|
||||||
|
await module[compensateMethod](entityIdentifiers)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -65,13 +65,12 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
|
|
||||||
const paymentCollection = useRemoteQueryStep({
|
const paymentCollection = useRemoteQueryStep({
|
||||||
entry_point: "payment_collection",
|
entry_point: "payment_collection",
|
||||||
fields: ["id"],
|
fields: ["id", "status"],
|
||||||
variables: {
|
variables: {
|
||||||
id: orderPaymentCollectionIds,
|
filters: {
|
||||||
status: [
|
id: orderPaymentCollectionIds,
|
||||||
PaymentCollectionStatus.NOT_PAID,
|
status: [PaymentCollectionStatus.NOT_PAID],
|
||||||
PaymentCollectionStatus.AWAITING,
|
},
|
||||||
],
|
|
||||||
},
|
},
|
||||||
list: false,
|
list: false,
|
||||||
}).config({ name: "payment-collection-query" })
|
}).config({ name: "payment-collection-query" })
|
||||||
@@ -81,10 +80,7 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
const paymentCollectionData = transform(
|
const paymentCollectionData = transform(
|
||||||
{ order, input },
|
{ order, input },
|
||||||
({ order, input }) => {
|
({ order, input }) => {
|
||||||
const pendingPayment = MathBN.sub(
|
const pendingPayment = order.summary.raw_pending_difference
|
||||||
order.summary.raw_current_order_total,
|
|
||||||
order.summary.raw_original_order_total
|
|
||||||
)
|
|
||||||
|
|
||||||
if (MathBN.lte(pendingPayment, 0)) {
|
if (MathBN.lte(pendingPayment, 0)) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
@@ -93,7 +89,10 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input.amount && MathBN.gt(input.amount, pendingPayment)) {
|
if (
|
||||||
|
input.amount &&
|
||||||
|
MathBN.gt(input.amount ?? pendingPayment, pendingPayment)
|
||||||
|
) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
`Cannot create a payment collection for amount greater than ${pendingPayment}`
|
`Cannot create a payment collection for amount greater than ${pendingPayment}`
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { PaymentCollectionDTO } from "@medusajs/types"
|
||||||
|
import { MedusaError, Modules, PaymentCollectionStatus } from "@medusajs/utils"
|
||||||
|
import {
|
||||||
|
WorkflowData,
|
||||||
|
createStep,
|
||||||
|
createWorkflow,
|
||||||
|
} from "@medusajs/workflows-sdk"
|
||||||
|
import { removeRemoteLinkStep, useRemoteQueryStep } from "../../common"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This step validates that the order doesn't have an active payment collection.
|
||||||
|
*/
|
||||||
|
export const throwUnlessStatusIsNotPaid = createStep(
|
||||||
|
"validate-payment-collection",
|
||||||
|
({ paymentCollection }: { paymentCollection: PaymentCollectionDTO }) => {
|
||||||
|
if (paymentCollection.status !== PaymentCollectionStatus.NOT_PAID) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Can only delete payment collections where status is not_paid`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const deleteOrderPaymentCollectionsId =
|
||||||
|
"delete-order-payment-collectionworkflow"
|
||||||
|
/**
|
||||||
|
* This workflow deletes one or more invites.
|
||||||
|
*/
|
||||||
|
export const deleteOrderPaymentCollections = createWorkflow(
|
||||||
|
deleteOrderPaymentCollectionsId,
|
||||||
|
(input: WorkflowData<{ id: string }>): WorkflowData<void> => {
|
||||||
|
const paymentCollection = useRemoteQueryStep({
|
||||||
|
entry_point: "payment_collection",
|
||||||
|
fields: ["id", "status"],
|
||||||
|
variables: { id: input.id },
|
||||||
|
throw_if_key_not_found: true,
|
||||||
|
list: false,
|
||||||
|
}).config({ name: "payment-collection-query" })
|
||||||
|
|
||||||
|
throwUnlessStatusIsNotPaid({ paymentCollection })
|
||||||
|
|
||||||
|
removeRemoteLinkStep({
|
||||||
|
[Modules.PAYMENT]: { payment_collection_id: input.id },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -27,6 +27,7 @@ export * from "./create-shipment"
|
|||||||
export * from "./decline-order-change"
|
export * from "./decline-order-change"
|
||||||
export * from "./delete-order-change"
|
export * from "./delete-order-change"
|
||||||
export * from "./delete-order-change-actions"
|
export * from "./delete-order-change-actions"
|
||||||
|
export * from "./delete-order-payment-collection"
|
||||||
export * from "./exchange/begin-order-exchange"
|
export * from "./exchange/begin-order-exchange"
|
||||||
export * from "./exchange/cancel-begin-order-exchange"
|
export * from "./exchange/cancel-begin-order-exchange"
|
||||||
export * from "./exchange/cancel-exchange"
|
export * from "./exchange/cancel-exchange"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Invite } from "./invite"
|
|||||||
import { Notification } from "./notification"
|
import { Notification } from "./notification"
|
||||||
import { Order } from "./order"
|
import { Order } from "./order"
|
||||||
import { Payment } from "./payment"
|
import { Payment } from "./payment"
|
||||||
|
import { PaymentCollection } from "./payment-collection"
|
||||||
import { PriceList } from "./price-list"
|
import { PriceList } from "./price-list"
|
||||||
import { PricePreference } from "./price-preference"
|
import { PricePreference } from "./price-preference"
|
||||||
import { Product } from "./product"
|
import { Product } from "./product"
|
||||||
@@ -67,6 +68,7 @@ export class Admin {
|
|||||||
public payment: Payment
|
public payment: Payment
|
||||||
public productVariant: ProductVariant
|
public productVariant: ProductVariant
|
||||||
public refundReason: RefundReason
|
public refundReason: RefundReason
|
||||||
|
public paymentCollection: PaymentCollection
|
||||||
|
|
||||||
constructor(client: Client) {
|
constructor(client: Client) {
|
||||||
this.invite = new Invite(client)
|
this.invite = new Invite(client)
|
||||||
@@ -102,5 +104,6 @@ export class Admin {
|
|||||||
this.productVariant = new ProductVariant(client)
|
this.productVariant = new ProductVariant(client)
|
||||||
this.refundReason = new RefundReason(client)
|
this.refundReason = new RefundReason(client)
|
||||||
this.exchange = new Exchange(client)
|
this.exchange = new Exchange(client)
|
||||||
|
this.paymentCollection = new PaymentCollection(client)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { HttpTypes, SelectParams } from "@medusajs/types"
|
||||||
|
import { Client } from "../client"
|
||||||
|
import { ClientHeaders } from "../types"
|
||||||
|
|
||||||
|
export class PaymentCollection {
|
||||||
|
private client: Client
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(
|
||||||
|
query?: HttpTypes.AdminPaymentCollectionFilters,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminPaymentCollectionsResponse>(
|
||||||
|
`/admin/payment-collections`,
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieve(
|
||||||
|
id: string,
|
||||||
|
query?: HttpTypes.AdminPaymentCollectionFilters,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminPaymentCollectionResponse>(
|
||||||
|
`/admin/payment-collections/${id}`,
|
||||||
|
{
|
||||||
|
query,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
body: HttpTypes.AdminCreatePaymentCollection,
|
||||||
|
query?: SelectParams,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminPaymentCollectionResponse>(
|
||||||
|
`/admin/payment-collections`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
query,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string, headers?: ClientHeaders) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminDeletePaymentCollectionResponse>(
|
||||||
|
`/admin/payment-collections/${id}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,14 +4,18 @@ import {
|
|||||||
BaseOrderAddress,
|
BaseOrderAddress,
|
||||||
BaseOrderChange,
|
BaseOrderChange,
|
||||||
BaseOrderChangeAction,
|
BaseOrderChangeAction,
|
||||||
|
BaseOrderFulfillment,
|
||||||
BaseOrderLineItem,
|
BaseOrderLineItem,
|
||||||
BaseOrderShippingMethod,
|
BaseOrderShippingMethod,
|
||||||
} from "../common"
|
} from "../common"
|
||||||
|
|
||||||
export interface AdminOrder extends BaseOrder {
|
export interface AdminOrder extends BaseOrder {
|
||||||
payment_collections: AdminPaymentCollection[]
|
payment_collections: AdminPaymentCollection[]
|
||||||
|
fulfillments?: BaseOrderFulfillment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminOrderFulfillment extends BaseOrderFulfillment {}
|
||||||
|
|
||||||
export interface AdminOrderLineItem extends BaseOrderLineItem {}
|
export interface AdminOrderLineItem extends BaseOrderLineItem {}
|
||||||
export interface AdminOrderAddress extends BaseOrderAddress {}
|
export interface AdminOrderAddress extends BaseOrderAddress {}
|
||||||
export interface AdminOrderShippingMethod extends BaseOrderShippingMethod {}
|
export interface AdminOrderShippingMethod extends BaseOrderShippingMethod {}
|
||||||
@@ -23,4 +27,4 @@ export interface AdminOrderPreview
|
|||||||
shipping_methods: (BaseOrderShippingMethod & {
|
shipping_methods: (BaseOrderShippingMethod & {
|
||||||
actions?: BaseOrderChangeAction[]
|
actions?: BaseOrderChangeAction[]
|
||||||
})[]
|
})[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,3 +12,8 @@ export interface AdminCreateRefundReason {
|
|||||||
label: string
|
label: string
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminCreatePaymentCollection {
|
||||||
|
order_id: string
|
||||||
|
amount?: number
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PaginatedResponse } from "../../common"
|
import { DeleteResponse, PaginatedResponse } from "../../common"
|
||||||
import {
|
import {
|
||||||
AdminPayment,
|
AdminPayment,
|
||||||
AdminPaymentCollection,
|
AdminPaymentCollection,
|
||||||
@@ -11,6 +11,13 @@ export interface AdminPaymentCollectionResponse {
|
|||||||
payment_collection: AdminPaymentCollection
|
payment_collection: AdminPaymentCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminDeletePaymentCollectionResponse
|
||||||
|
extends DeleteResponse<"payment-collection"> {}
|
||||||
|
|
||||||
|
export interface AdminPaymentCollectionsResponse {
|
||||||
|
payment_collections: AdminPaymentCollection[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminPaymentResponse {
|
export interface AdminPaymentResponse {
|
||||||
payment: AdminPayment
|
payment: AdminPayment
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,6 +368,43 @@ export interface IPaymentModuleService extends IModuleService {
|
|||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method soft deletes payment collections by their IDs.
|
||||||
|
*
|
||||||
|
* @param {string[]} id - The IDs of payment collections.
|
||||||
|
* @param {SoftDeleteReturn<TReturnableLinkableKeys>} 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<void | Record<TReturnableLinkableKeys, string[]>>} 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.softDeletePaymentCollections(["paycol_123"])
|
||||||
|
*/
|
||||||
|
softDeletePaymentCollections<TReturnableLinkableKeys extends string = string>(
|
||||||
|
id: string[],
|
||||||
|
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<Record<TReturnableLinkableKeys, string[]> | void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method restores soft deleted payment collection by their IDs.
|
||||||
|
*
|
||||||
|
* @param {string[]} id - The IDs of payment collections.
|
||||||
|
* @param {RestoreReturn<TReturnableLinkableKeys>} config - Configurations determining which relations to restore along with each of the payment collection. You can pass to its `returnLinkableKeys`
|
||||||
|
* property any of the payment collection'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<void | Record<TReturnableLinkableKeys, string[]>>} 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.restorePaymentCollections(["paycol_123"])
|
||||||
|
*/
|
||||||
|
restorePaymentCollections<TReturnableLinkableKeys extends string = string>(
|
||||||
|
id: string[],
|
||||||
|
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<Record<TReturnableLinkableKeys, string[]> | void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method marks a payment collection as completed by settings its `completed_at` field to the current date and time.
|
* This method marks a payment collection as completed by settings its `completed_at` field to the current date and time.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { deleteOrderPaymentCollections } from "@medusajs/core-flows"
|
||||||
|
import { DeleteResponse } from "@medusajs/types"
|
||||||
|
import {
|
||||||
|
AuthenticatedMedusaRequest,
|
||||||
|
MedusaResponse,
|
||||||
|
} from "../../../../types/routing"
|
||||||
|
|
||||||
|
export const DELETE = async (
|
||||||
|
req: AuthenticatedMedusaRequest,
|
||||||
|
res: MedusaResponse<DeleteResponse<"payment-collection">>
|
||||||
|
) => {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
await deleteOrderPaymentCollections(req.scope).run({
|
||||||
|
input: { id },
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
id,
|
||||||
|
object: "payment-collection",
|
||||||
|
deleted: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -19,4 +19,9 @@ export const adminPaymentCollectionsMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["DELETE"],
|
||||||
|
matcher: "/admin/payment-collections/:id",
|
||||||
|
middlewares: [],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export const OrderPaymentCollection: ModuleJoinerConfig = {
|
|||||||
args: {
|
args: {
|
||||||
methodSuffix: "PaymentCollections",
|
methodSuffix: "PaymentCollections",
|
||||||
},
|
},
|
||||||
|
deleteCascade: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
extends: [
|
extends: [
|
||||||
|
|||||||
@@ -103,12 +103,12 @@ export default class PaymentCollection {
|
|||||||
payment_providers = new Collection<Rel<PaymentProvider>>(this)
|
payment_providers = new Collection<Rel<PaymentProvider>>(this)
|
||||||
|
|
||||||
@OneToMany(() => PaymentSession, (ps) => ps.payment_collection, {
|
@OneToMany(() => PaymentSession, (ps) => ps.payment_collection, {
|
||||||
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
cascade: [Cascade.PERSIST] as any,
|
||||||
})
|
})
|
||||||
payment_sessions = new Collection<Rel<PaymentSession>>(this)
|
payment_sessions = new Collection<Rel<PaymentSession>>(this)
|
||||||
|
|
||||||
@OneToMany(() => Payment, (payment) => payment.payment_collection, {
|
@OneToMany(() => Payment, (payment) => payment.payment_collection, {
|
||||||
cascade: [Cascade.PERSIST, "soft-remove"] as any,
|
cascade: [Cascade.PERSIST] as any,
|
||||||
})
|
})
|
||||||
payments = new Collection<Rel<Payment>>(this)
|
payments = new Collection<Rel<Payment>>(this)
|
||||||
|
|
||||||
|
|||||||
@@ -107,10 +107,14 @@ export default class PaymentSession {
|
|||||||
@BeforeCreate()
|
@BeforeCreate()
|
||||||
onCreate() {
|
onCreate() {
|
||||||
this.id = generateEntityId(this.id, "payses")
|
this.id = generateEntityId(this.id, "payses")
|
||||||
|
this.payment_collection_id ??=
|
||||||
|
this.payment_collection_id ?? this.payment_collection?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnInit()
|
@OnInit()
|
||||||
onInit() {
|
onInit() {
|
||||||
this.id = generateEntityId(this.id, "payses")
|
this.id = generateEntityId(this.id, "payses")
|
||||||
|
this.payment_collection_id ??=
|
||||||
|
this.payment_collection_id ?? this.payment_collection?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,10 +144,14 @@ export default class Payment {
|
|||||||
@BeforeCreate()
|
@BeforeCreate()
|
||||||
onCreate() {
|
onCreate() {
|
||||||
this.id = generateEntityId(this.id, "pay")
|
this.id = generateEntityId(this.id, "pay")
|
||||||
|
this.payment_collection_id ??=
|
||||||
|
this.payment_collection_id ?? this.payment_collection?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnInit()
|
@OnInit()
|
||||||
onInit() {
|
onInit() {
|
||||||
this.id = generateEntityId(this.id, "pay")
|
this.id = generateEntityId(this.id, "pay")
|
||||||
|
this.payment_collection_id ??=
|
||||||
|
this.payment_collection_id ?? this.payment_collection?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user