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:
Riqwan Thamir
2024-08-20 10:30:17 +00:00
committed by GitHub
parent 69830ca89c
commit fa44e3f5a8
35 changed files with 631 additions and 94 deletions
@@ -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"
+3
View File
@@ -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.
*