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:
@@ -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({
|
||||
entry_point: "payment_collection",
|
||||
fields: ["id"],
|
||||
fields: ["id", "status"],
|
||||
variables: {
|
||||
id: orderPaymentCollectionIds,
|
||||
status: [
|
||||
PaymentCollectionStatus.NOT_PAID,
|
||||
PaymentCollectionStatus.AWAITING,
|
||||
],
|
||||
filters: {
|
||||
id: orderPaymentCollectionIds,
|
||||
status: [PaymentCollectionStatus.NOT_PAID],
|
||||
},
|
||||
},
|
||||
list: false,
|
||||
}).config({ name: "payment-collection-query" })
|
||||
@@ -81,10 +80,7 @@ export const createOrderPaymentCollectionWorkflow = createWorkflow(
|
||||
const paymentCollectionData = transform(
|
||||
{ order, input },
|
||||
({ order, input }) => {
|
||||
const pendingPayment = MathBN.sub(
|
||||
order.summary.raw_current_order_total,
|
||||
order.summary.raw_original_order_total
|
||||
)
|
||||
const pendingPayment = order.summary.raw_pending_difference
|
||||
|
||||
if (MathBN.lte(pendingPayment, 0)) {
|
||||
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(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`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 "./delete-order-change"
|
||||
export * from "./delete-order-change-actions"
|
||||
export * from "./delete-order-payment-collection"
|
||||
export * from "./exchange/begin-order-exchange"
|
||||
export * from "./exchange/cancel-begin-order-exchange"
|
||||
export * from "./exchange/cancel-exchange"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Invite } from "./invite"
|
||||
import { Notification } from "./notification"
|
||||
import { Order } from "./order"
|
||||
import { Payment } from "./payment"
|
||||
import { PaymentCollection } from "./payment-collection"
|
||||
import { PriceList } from "./price-list"
|
||||
import { PricePreference } from "./price-preference"
|
||||
import { Product } from "./product"
|
||||
@@ -67,6 +68,7 @@ export class Admin {
|
||||
public payment: Payment
|
||||
public productVariant: ProductVariant
|
||||
public refundReason: RefundReason
|
||||
public paymentCollection: PaymentCollection
|
||||
|
||||
constructor(client: Client) {
|
||||
this.invite = new Invite(client)
|
||||
@@ -102,5 +104,6 @@ export class Admin {
|
||||
this.productVariant = new ProductVariant(client)
|
||||
this.refundReason = new RefundReason(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,
|
||||
BaseOrderChange,
|
||||
BaseOrderChangeAction,
|
||||
BaseOrderFulfillment,
|
||||
BaseOrderLineItem,
|
||||
BaseOrderShippingMethod,
|
||||
} from "../common"
|
||||
|
||||
export interface AdminOrder extends BaseOrder {
|
||||
payment_collections: AdminPaymentCollection[]
|
||||
fulfillments?: BaseOrderFulfillment[]
|
||||
}
|
||||
|
||||
export interface AdminOrderFulfillment extends BaseOrderFulfillment {}
|
||||
|
||||
export interface AdminOrderLineItem extends BaseOrderLineItem {}
|
||||
export interface AdminOrderAddress extends BaseOrderAddress {}
|
||||
export interface AdminOrderShippingMethod extends BaseOrderShippingMethod {}
|
||||
@@ -23,4 +27,4 @@ export interface AdminOrderPreview
|
||||
shipping_methods: (BaseOrderShippingMethod & {
|
||||
actions?: BaseOrderChangeAction[]
|
||||
})[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,8 @@ export interface AdminCreateRefundReason {
|
||||
label: 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 {
|
||||
AdminPayment,
|
||||
AdminPaymentCollection,
|
||||
@@ -11,6 +11,13 @@ export interface AdminPaymentCollectionResponse {
|
||||
payment_collection: AdminPaymentCollection
|
||||
}
|
||||
|
||||
export interface AdminDeletePaymentCollectionResponse
|
||||
extends DeleteResponse<"payment-collection"> {}
|
||||
|
||||
export interface AdminPaymentCollectionsResponse {
|
||||
payment_collections: AdminPaymentCollection[]
|
||||
}
|
||||
|
||||
export interface AdminPaymentResponse {
|
||||
payment: AdminPayment
|
||||
}
|
||||
|
||||
@@ -368,6 +368,43 @@ export interface IPaymentModuleService extends IModuleService {
|
||||
sharedContext?: Context
|
||||
): 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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user