Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard,core-flows,js-sdk,types): ability to mark payment as paid #8679

Merged
merged 8 commits into from
Aug 20, 2024
32 changes: 26 additions & 6 deletions integration-tests/http/__tests__/claims/claims.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -746,20 +747,20 @@ 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 },
adminHeaders
)
).data.payment_collection

expect(paymentCollection).toEqual(
expect(createdPaymentCollection).toEqual(
expect.objectContaining({
currency_code: "usd",
amount: 100,
Expand All @@ -769,16 +770,35 @@ 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

expect(finalPaymentCollection).toEqual(
expect.objectContaining({
currency_code: "usd",
amount: paymentDelta,
status: "authorized",
authorized_amount: paymentDelta,
captured_amount: paymentDelta,
refunded_amount: 0,
})
)
})
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
3 changes: 3 additions & 0 deletions packages/admin-next/dashboard/src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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}}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -54,6 +59,7 @@ type OrderSummarySectionProps = {
export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
const { t } = useTranslation()
const navigate = useNavigate()
const prompt = usePrompt()

const { reservations } = useReservationItems(
{
Expand Down Expand Up @@ -104,10 +110,54 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
(pc) => pc.status === "not_paid"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Can we rely on this check? couldn't there be multiple unpaid collections?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way to get in this situation through the UI as of yet. So not possible right now. But when we link the payment collection to the order change, we can get rid of this check

)

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 (
<Container className="divide-y divide-dashed p-0">
<Header order={order} orderPreview={orderPreview} />
Expand Down Expand Up @@ -152,6 +202,16 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
order={order}
/>
)}

{showPayment && (
<Button
size="small"
variant="secondary"
onClick={() => handleMarkAsPaid(unpaidPaymentCollection)}
>
{t("orders.payment.markAsPaid")}
</Button>
)}
</div>
)}
</Container>
Expand Down
1 change: 1 addition & 0 deletions packages/core/core-flows/src/order/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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
captured_by?: 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: input.captured_by,
amount: paymentCollection.amount,
},
})

return new WorkflowResponse(payment)
}
)
17 changes: 17 additions & 0 deletions packages/core/js-sdk/src/admin/payment-collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,21 @@ export class PaymentCollection {
}
)
}

async markAsPaid(
id: string,
body: HttpTypes.AdminMarkPaymentCollectionAsPaid,
query?: SelectParams,
headers?: ClientHeaders
) {
return await this.client.fetch<HttpTypes.AdminPaymentCollectionResponse>(
`/admin/payment-collections/${id}/mark-as-paid`,
{
method: "POST",
headers,
body,
query,
}
)
}
}
4 changes: 4 additions & 0 deletions packages/core/types/src/http/payment/admin/payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@ export interface AdminCreatePaymentCollection {
order_id: string
amount?: number
}

export interface AdminMarkPaymentCollectionAsPaid {
order_id: string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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<AdminMarkPaymentCollectionPaidType>,
res: MedusaResponse<HttpTypes.AdminPaymentCollectionResponse>
) => {
const { id } = req.params

await markPaymentCollectionAsPaid(req.scope).run({
input: {
...req.body,
payment_collection_id: id,
captured_by: req.auth_context.actor_id,
},
})

const paymentCollection = await refetchEntity(
"payment_collection",
id,
req.scope,
req.remoteQueryConfig.fields
)

res.status(200).json({ payment_collection: paymentCollection })
}
Loading
Loading