feat(core-flows,types,utils): make payment optional when cart balance is 0 (#11872)

what:

- make payment optional when cart balance is 0
This commit is contained in:
Riqwan Thamir
2025-03-17 19:03:22 +00:00
committed by GitHub
parent 5ab15a2988
commit 9dd62d93bd
9 changed files with 122 additions and 33 deletions
+7
View File
@@ -0,0 +1,7 @@
---
"@medusajs/core-flows": patch
"@medusajs/types": patch
"@medusajs/utils": patch
---
feat(core-flows,types,utils): make payment optional when cart balance is 0
@@ -1085,7 +1085,9 @@ medusaIntegrationTestRunner({
{ option_id: shippingOption.id }, { option_id: shippingOption.id },
storeHeaders storeHeaders
) )
})
it("should successfully complete cart", async () => {
const paymentCollection = ( const paymentCollection = (
await api.post( await api.post(
`/store/payment-collections`, `/store/payment-collections`,
@@ -1099,9 +1101,7 @@ medusaIntegrationTestRunner({
{ provider_id: "pp_system_default" }, { provider_id: "pp_system_default" },
storeHeaders storeHeaders
) )
})
it("should successfully complete cart", async () => {
createCartCreditLinesWorkflow.run({ createCartCreditLinesWorkflow.run({
input: [ input: [
{ {
@@ -1144,6 +1144,53 @@ medusaIntegrationTestRunner({
) )
}) })
it("should successfully complete cart with credit lines alone", async () => {
const oldCart = (
await api.get(`/store/carts/${cart.id}`, storeHeaders)
).data.cart
createCartCreditLinesWorkflow.run({
input: [
{
cart_id: oldCart.id,
amount: oldCart.total,
currency_code: "usd",
reference: "test",
reference_id: "test",
},
],
container: appContainer,
})
const response = await api.post(
`/store/carts/${cart.id}/complete`,
{},
storeHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order).toEqual(
expect.objectContaining({
id: expect.any(String),
currency_code: "usd",
credit_line_total: 2395,
discount_total: 100,
credit_lines: [
expect.objectContaining({
amount: 2395,
}),
],
items: expect.arrayContaining([
expect.objectContaining({
unit_price: 1500,
compare_at_unit_price: null,
quantity: 1,
}),
]),
})
)
})
it("should successfully complete cart without shipping for digital products", async () => { it("should successfully complete cart without shipping for digital products", async () => {
/** /**
* Product has a shipping profile so cart item should not require shipping * Product has a shipping profile so cart item should not require shipping
@@ -1181,7 +1228,7 @@ medusaIntegrationTestRunner({
) )
).data.product ).data.product
let cart = ( cart = (
await api.post( await api.post(
`/store/carts`, `/store/carts`,
{ {
@@ -1148,7 +1148,7 @@ medusaIntegrationTestRunner({
expect.objectContaining({ expect.objectContaining({
id: order.id, id: order.id,
total: 0, total: 0,
subtotal: -6, subtotal: 100,
summary: expect.objectContaining({ summary: expect.objectContaining({
current_order_total: 0, current_order_total: 0,
accounting_total: 0, accounting_total: 0,
@@ -1,6 +1,7 @@
import { CartWorkflowDTO } from "@medusajs/framework/types" import { CartWorkflowDTO } from "@medusajs/framework/types"
import { import {
isPresent, isPresent,
MathBN,
MedusaError, MedusaError,
PaymentSessionStatus, PaymentSessionStatus,
} from "@medusajs/framework/utils" } from "@medusajs/framework/utils"
@@ -20,13 +21,13 @@ export const validateCartPaymentsStepId = "validate-cart-payments"
/** /**
* This step validates a cart's payment sessions. Their status must * This step validates a cart's payment sessions. Their status must
* be `pending` or `requires_more`. If not valid, the step throws an error. * be `pending` or `requires_more`. If not valid, the step throws an error.
* *
* :::tip * :::tip
* *
* You can use the {@link retrieveCartStep} to retrieve a cart's details. * You can use the {@link retrieveCartStep} to retrieve a cart's details.
* *
* ::: * :::
* *
* @example * @example
* const data = validateCartPaymentsStep({ * const data = validateCartPaymentsStep({
* // retrieve the details of the cart from another workflow * // retrieve the details of the cart from another workflow
@@ -38,9 +39,16 @@ export const validateCartPaymentsStep = createStep(
validateCartPaymentsStepId, validateCartPaymentsStepId,
async (data: ValidateCartPaymentsStepInput) => { async (data: ValidateCartPaymentsStepInput) => {
const { const {
cart: { payment_collection: paymentCollection }, cart: { payment_collection: paymentCollection, total, credit_line_total },
} = data } = data
const canSkipPayment =
MathBN.convert(credit_line_total).gte(0) && MathBN.convert(total).lte(0)
if (canSkipPayment) {
return new StepResponse([])
}
if (!isPresent(paymentCollection)) { if (!isPresent(paymentCollection)) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -4,6 +4,7 @@ import {
UsageComputedActions, UsageComputedActions,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { import {
isDefined,
Modules, Modules,
OrderStatus, OrderStatus,
OrderWorkflowEvents, OrderWorkflowEvents,
@@ -167,14 +168,16 @@ export const completeCartWorkflow = createWorkflow(
const cartToOrder = transform({ cart, payment }, ({ cart, payment }) => { const cartToOrder = transform({ cart, payment }, ({ cart, payment }) => {
const transactions = const transactions =
payment?.captures?.map((capture) => { (payment &&
return { payment?.captures?.map((capture) => {
amount: capture.raw_amount ?? capture.amount, return {
currency_code: payment.currency_code, amount: capture.raw_amount ?? capture.amount,
reference: "capture", currency_code: payment.currency_code,
reference_id: capture.id, reference: "capture",
} reference_id: capture.id,
}) ?? [] }
})) ??
[]
const allItems = (cart.items ?? []).map((item) => { const allItems = (cart.items ?? []).map((item) => {
const input: PrepareLineItemDataInput = { const input: PrepareLineItemDataInput = {
@@ -280,19 +283,31 @@ export const completeCartWorkflow = createWorkflow(
} }
}) })
parallelize( const linksToCreate = transform(
createRemoteLinkStep([ { cart, createdOrder },
{ ({ cart, createdOrder }) => {
[Modules.ORDER]: { order_id: createdOrder.id }, const links: Record<string, any>[] = [
[Modules.CART]: { cart_id: cart.id }, {
}, [Modules.ORDER]: { order_id: createdOrder.id },
{ [Modules.CART]: { cart_id: cart.id },
[Modules.ORDER]: { order_id: createdOrder.id },
[Modules.PAYMENT]: {
payment_collection_id: cart.payment_collection.id,
}, },
}, ]
]),
if (isDefined(cart.payment_collection?.id)) {
links.push({
[Modules.ORDER]: { order_id: createdOrder.id },
[Modules.PAYMENT]: {
payment_collection_id: cart.payment_collection.id,
},
})
}
return links
}
)
parallelize(
createRemoteLinkStep(linksToCreate),
updateCartsStep([updateCompletedAt]), updateCartsStep([updateCompletedAt]),
reserveInventoryStep(formatedInventoryItems), reserveInventoryStep(formatedInventoryItems),
emitEventStep({ emitEventStep({
@@ -45,6 +45,10 @@ export const authorizePaymentSessionStep = createStep(
Modules.PAYMENT Modules.PAYMENT
) )
if (!input.id) {
return new StepResponse(null)
}
try { try {
payment = await paymentModule.authorizePaymentSession( payment = await paymentModule.authorizePaymentSession(
input.id, input.id,
+10
View File
@@ -1033,6 +1033,16 @@ export interface CartDTO {
* The raw original shipping tax total of the cart. * The raw original shipping tax total of the cart.
*/ */
raw_original_shipping_tax_total: BigNumberRawValue raw_original_shipping_tax_total: BigNumberRawValue
/**
* The raw credit lines total of the cart.
*/
raw_credit_line_total: BigNumberRawValue
/**
* The credit lines total of the cart.
*/
credit_line_total: BigNumberValue
} }
/** /**
@@ -768,7 +768,7 @@ describe("Total calculation", function () {
}, },
], ],
total: 48, total: 48,
subtotal: 60, subtotal: 100,
tax_total: 8, tax_total: 8,
discount_total: 22, discount_total: 22,
discount_subtotal: 20, discount_subtotal: 20,
+1 -3
View File
@@ -208,8 +208,6 @@ export function decorateCartTotals(
taxRate: creditLinesSumTaxRate, taxRate: creditLinesSumTaxRate,
}) })
subtotal = MathBN.sub(subtotal, creditLinesSubtotal)
const taxTotal = MathBN.add(itemsTaxTotal, shippingTaxTotal) const taxTotal = MathBN.add(itemsTaxTotal, shippingTaxTotal)
const originalTaxTotal = MathBN.add( const originalTaxTotal = MathBN.add(
@@ -222,7 +220,7 @@ export function decorateCartTotals(
// TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total) // TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total)
const tempTotal = MathBN.add(subtotal, taxTotal) const tempTotal = MathBN.add(subtotal, taxTotal)
const total = MathBN.sub(tempTotal, discountSubtotal) const total = MathBN.sub(tempTotal, discountSubtotal, creditLinesTotal)
const cart = cartLike as any const cart = cartLike as any