diff --git a/.changeset/loud-ducks-do.md b/.changeset/loud-ducks-do.md new file mode 100644 index 0000000000..1776a3c754 --- /dev/null +++ b/.changeset/loud-ducks-do.md @@ -0,0 +1,6 @@ +--- +"@medusajs/dashboard": patch +"@medusajs/types": patch +--- + +feat(dashboard,types): add credit lines + loyalty changes diff --git a/packages/admin/dashboard/src/hooks/api/index.ts b/packages/admin/dashboard/src/hooks/api/index.ts index 59a39978f2..ce51741a67 100644 --- a/packages/admin/dashboard/src/hooks/api/index.ts +++ b/packages/admin/dashboard/src/hooks/api/index.ts @@ -15,6 +15,7 @@ export * from "./notification" export * from "./orders" export * from "./payment-collections" export * from "./payments" +export * from "./plugins" export * from "./price-lists" export * from "./product-types" export * from "./product-variants" diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx index 923a50e5f6..f089ae59e7 100644 --- a/packages/admin/dashboard/src/hooks/api/orders.tsx +++ b/packages/admin/dashboard/src/hooks/api/orders.tsx @@ -1,5 +1,5 @@ import { FetchError } from "@medusajs/js-sdk" -import { HttpTypes } from "@medusajs/types" +import { CreateOrderCreditLineDTO, HttpTypes } from "@medusajs/types" import { QueryKey, useMutation, @@ -354,3 +354,28 @@ export const useCancelOrderTransfer = ( ...options, }) } + +export const useCreateOrderCreditLine = ( + orderId: string, + options?: UseMutationOptions< + HttpTypes.AdminOrderResponse, + FetchError, + Omit + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.order.createCreditLine(orderId, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.preview(orderId), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin/dashboard/src/hooks/api/plugins.tsx b/packages/admin/dashboard/src/hooks/api/plugins.tsx new file mode 100644 index 0000000000..3317e59772 --- /dev/null +++ b/packages/admin/dashboard/src/hooks/api/plugins.tsx @@ -0,0 +1,28 @@ +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" +import { QueryKey, UseQueryOptions, useQuery } from "@tanstack/react-query" +import { sdk } from "../../lib/client" +import { queryKeysFactory } from "../../lib/query-key-factory" + +const PLUGINS_QUERY_KEY = "plugins" as const +export const pluginsQueryKeys = queryKeysFactory(PLUGINS_QUERY_KEY) + +export const usePlugins = ( + options?: Omit< + UseQueryOptions< + any, + FetchError, + HttpTypes.AdminPluginsListResponse, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => sdk.admin.plugin.list(), + queryKey: pluginsQueryKeys.list(), + ...options, + }) + + return { ...data, ...rest } +} diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index a035252d62..ab2a83906d 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -3783,6 +3783,78 @@ "orders": { "type": "object", "properties": { + "giftCardsStoreCreditLines": { + "type": "string" + }, + "creditLines": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "total": { + "type": "string" + }, + "creditOrDebit": { + "type": "string" + }, + "createCreditLine": { + "type": "string" + }, + "createCreditLineSuccess": { + "type": "string" + }, + "createCreditLineError": { + "type": "string" + }, + "createCreditLineDescription": { + "type": "string" + }, + "operation": { + "type": "string" + }, + "credit": { + "type": "string" + }, + "debit": { + "type": "string" + }, + "debitDescription": { + "type": "string" + }, + "creditDescription": { + "type": "string" + } + } + }, + "balanceSettlement": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "settlementType": { + "type": "string" + }, + "settlementTypes": { + "type": "object", + "properties": { + "paymentMethod": { + "type": "string" + }, + "paymentMethodDescription": { + "type": "string" + }, + "creditLine": { + "type": "string" + }, + "creditLineDescription": { + "type": "string" + } + } + } + } + }, "domain": { "type": "string" }, @@ -3954,6 +4026,9 @@ "totalPaidByCustomer": { "type": "string" }, + "totalStoreCreditRefunds": { + "type": "string" + }, "capture": { "type": "string" }, @@ -4055,6 +4130,7 @@ "title", "isReadyToBeCaptured", "totalPaidByCustomer", + "totalStoreCreditRefunds", "capture", "capture_short", "refund", @@ -10601,6 +10677,12 @@ "amount": { "type": "string" }, + "reference": { + "type": "string" + }, + "reference_id": { + "type": "string" + }, "refundAmount": { "type": "string" }, @@ -10859,6 +10941,9 @@ "paidTotal": { "type": "string" }, + "creditTotal": { + "type": "string" + }, "totalExclTax": { "type": "string" }, diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index cd8993825c..50615e7033 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1010,6 +1010,31 @@ } }, "orders": { + "giftCardsStoreCreditLines": "Gift cards & credit lines", + "creditLines": { + "title": "Credit lines", + "total": "Sum of all credit lines", + "creditOrDebit": "Credit / Debit", + "createCreditLine": "Create credit line", + "createCreditLineSuccess": "Credit line created successfully", + "createCreditLineError": "Error creating credit line", + "createCreditLineDescription": "Create a credit line for amount {{amount}}", + "operation": "Operation", + "credit": "Credit", + "creditDescription": "Adds a positive sum to the order", + "debit": "Debit", + "debitDescription": "Subtracts a negative sum from the order" + }, + "balanceSettlement": { + "title": "Balance settlement", + "settlementType": "Settlement type", + "settlementTypes": { + "paymentMethod": "Payment method", + "paymentMethodDescription": "Refund amount to the payment method", + "creditLine": "Store credit", + "creditLineDescription": "Refund amount as store credit" + } + }, "domain": "Orders", "claim": "Claim", "exchange": "Exchange", @@ -1056,6 +1081,7 @@ "title": "Payments", "isReadyToBeCaptured": "Payment <0/> is ready to be captured.", "totalPaidByCustomer": "Total paid by customer", + "totalStoreCreditRefunds": "Total store credit refunds", "capture": "Capture payment", "capture_short": "Capture", "refund": "Refund", @@ -2847,6 +2873,8 @@ }, "fields": { "amount": "Amount", + "reference": "Reference", + "reference_id": "Reference ID", "refundAmount": "Refund amount", "name": "Name", "default": "Default", @@ -2932,7 +2960,8 @@ "orders": "Orders", "account": "Account", "total": "Order Total", - "paidTotal": "Total captured", + "paidTotal": "Paid Total", + "creditTotal": "Credit Lines Total", "totalExclTax": "Total excl. tax", "subtotal": "Subtotal", "shipping": "Shipping", diff --git a/packages/admin/dashboard/src/lib/credit-line.ts b/packages/admin/dashboard/src/lib/credit-line.ts new file mode 100644 index 0000000000..628334c008 --- /dev/null +++ b/packages/admin/dashboard/src/lib/credit-line.ts @@ -0,0 +1,8 @@ +import { OrderCreditLineDTO } from "@medusajs/types" + +export const getTotalCreditLines = (creditLines: OrderCreditLineDTO[]) => + creditLines.reduce((acc, creditLine) => { + acc = acc + (creditLine.amount as number) + + return acc + }, 0) diff --git a/packages/admin/dashboard/src/lib/orders.ts b/packages/admin/dashboard/src/lib/orders.ts new file mode 100644 index 0000000000..39ef381353 --- /dev/null +++ b/packages/admin/dashboard/src/lib/orders.ts @@ -0,0 +1,8 @@ +import { HttpTypes } from "@medusajs/types" + +export const getPaymentsFromOrder = (order: HttpTypes.AdminOrder) => { + return order.payment_collections + .map((collection: HttpTypes.AdminPaymentCollection) => collection.payments) + .flat(1) + .filter(Boolean) as HttpTypes.AdminPayment[] +} diff --git a/packages/admin/dashboard/src/lib/plugins.ts b/packages/admin/dashboard/src/lib/plugins.ts new file mode 100644 index 0000000000..5fc1fc8726 --- /dev/null +++ b/packages/admin/dashboard/src/lib/plugins.ts @@ -0,0 +1,7 @@ +import { HttpTypes } from "@medusajs/types" + +export const LOYALTY_PLUGIN_NAME = "@medusajs/loyalty-plugin" + +export const getLoyaltyPlugin = (plugins: HttpTypes.AdminPlugin[]) => { + return plugins?.find((plugin) => plugin.name === LOYALTY_PLUGIN_NAME) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-balance-settlement/components/order-balance-settlement-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-balance-settlement/components/order-balance-settlement-form/index.ts new file mode 100644 index 0000000000..3607a1ac8d --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-balance-settlement/components/order-balance-settlement-form/index.ts @@ -0,0 +1 @@ +export * from "./order-balance-settlement-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-balance-settlement/components/order-balance-settlement-form/order-balance-settlement-form.tsx b/packages/admin/dashboard/src/routes/orders/order-balance-settlement/components/order-balance-settlement-form/order-balance-settlement-form.tsx new file mode 100644 index 0000000000..44cfbf8a92 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-balance-settlement/components/order-balance-settlement-form/order-balance-settlement-form.tsx @@ -0,0 +1,398 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { AdminOrder, AdminPayment } from "@medusajs/types" +import { + Button, + clx, + CurrencyInput, + Divider, + Input, + Label, + RadioGroup, + Select, + Textarea, + toast, +} from "@medusajs/ui" +import { useEffect, useMemo, useState } from "react" +import { formatValue } from "react-currency-input-field" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { + useCreateOrderCreditLine, + useRefundPayment, +} from "../../../../../hooks/api" +import { currencies } from "../../../../../lib/data/currencies" +import { formatCurrency } from "../../../../../lib/format-currency" +import { getLocaleAmount } from "../../../../../lib/money-amount-helpers" +import { getPaymentsFromOrder } from "../../../../../lib/orders" + +const OrderBalanceSettlementSchema = zod.object({ + settlement_type: zod.enum(["credit_line", "refund"]), + refund: zod + .object({ + amount: zod.string().or(zod.number()).optional(), + note: zod.string().optional(), + }) + .optional(), + credit_line: zod + .object({ + amount: zod.string().or(zod.number()).optional(), + reference: zod.string().optional(), + reference_id: zod.string().optional(), + note: zod.string().optional(), + }) + .optional(), +}) + +export const OrderBalanceSettlementForm = ({ + order, +}: { + order: AdminOrder +}) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + const [activePayment, setActivePayment] = useState(null) + const payments = getPaymentsFromOrder(order) + const pendingDifference = order.summary.pending_difference * -1 + + const form = useForm>({ + defaultValues: { + settlement_type: "refund", + refund: { + amount: 0, + }, + credit_line: { + amount: 0, + }, + }, + resolver: zodResolver(OrderBalanceSettlementSchema), + }) + + const { mutateAsync: createCreditLine, isPending: isCreditLinePending } = + useCreateOrderCreditLine(order.id) + + const { mutateAsync: createRefund, isPending: isRefundPending } = + useRefundPayment(order.id, activePayment?.id!) + + const settlementType = form.watch("settlement_type") + + const handleSubmit = form.handleSubmit(async (data) => { + if (data.settlement_type === "credit_line") { + await createCreditLine( + { + amount: parseFloat(data.credit_line!.amount! as string) * -1, + reference: data.credit_line!.reference ?? "order", + reference_id: data.credit_line!.reference_id ?? order.id, + }, + { + onSuccess: () => { + toast.success(t("orders.creditLines.createCreditLineSuccess")) + + handleSuccess() + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + + if (data.settlement_type === "refund") { + await createRefund( + { + amount: parseFloat(data.refund!.amount! as string), + note: data.refund!.note, + }, + { + onSuccess: () => { + toast.success( + t("orders.payment.refundPaymentSuccess", { + amount: formatCurrency( + parseFloat(data.refund!.amount! as string), + order.currency_code! + ), + }) + ) + + handleSuccess() + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + } + }) + + const currency = useMemo( + () => currencies[order.currency_code.toUpperCase()], + [order.currency_code] + ) + + useEffect(() => { + form.clearErrors() + + const minimum = activePayment?.amount + ? Math.min(pendingDifference, activePayment.amount) + : pendingDifference + + if (settlementType === "refund") { + form.setValue("refund.amount", activePayment ? minimum : 0) + } + + if (settlementType === "credit_line") { + form.setValue("credit_line.amount", minimum) + } + }, [settlementType, activePayment, pendingDifference, form]) + + return ( + + + +
+
+ + + + form.setValue("settlement_type", value) + } + > + + + + +
+ + + + {settlementType === "refund" && ( + <> +
+ +
+ + { + return ( + + {t("fields.amount")} + + + + onChange(values?.value ? values?.value : "") + } + autoFocus + /> + + + + + ) + }} + /> + + { + return ( + + {t("fields.note")} + + +