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 20:03:22 +01:00
committed by GitHub
parent 5ab15a2988
commit 9dd62d93bd
9 changed files with 122 additions and 33 deletions

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

View File

@@ -1085,7 +1085,9 @@ medusaIntegrationTestRunner({
{ option_id: shippingOption.id },
storeHeaders
)
})
it("should successfully complete cart", async () => {
const paymentCollection = (
await api.post(
`/store/payment-collections`,
@@ -1099,9 +1101,7 @@ medusaIntegrationTestRunner({
{ provider_id: "pp_system_default" },
storeHeaders
)
})
it("should successfully complete cart", async () => {
createCartCreditLinesWorkflow.run({
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 () => {
/**
* Product has a shipping profile so cart item should not require shipping
@@ -1181,7 +1228,7 @@ medusaIntegrationTestRunner({
)
).data.product
let cart = (
cart = (
await api.post(
`/store/carts`,
{

View File

@@ -1148,7 +1148,7 @@ medusaIntegrationTestRunner({
expect.objectContaining({
id: order.id,
total: 0,
subtotal: -6,
subtotal: 100,
summary: expect.objectContaining({
current_order_total: 0,
accounting_total: 0,

View File

@@ -1,6 +1,7 @@
import { CartWorkflowDTO } from "@medusajs/framework/types"
import {
isPresent,
MathBN,
MedusaError,
PaymentSessionStatus,
} 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
* be `pending` or `requires_more`. If not valid, the step throws an error.
*
*
* :::tip
*
*
* You can use the {@link retrieveCartStep} to retrieve a cart's details.
*
*
* :::
*
*
* @example
* const data = validateCartPaymentsStep({
* // retrieve the details of the cart from another workflow
@@ -38,9 +39,16 @@ export const validateCartPaymentsStep = createStep(
validateCartPaymentsStepId,
async (data: ValidateCartPaymentsStepInput) => {
const {
cart: { payment_collection: paymentCollection },
cart: { payment_collection: paymentCollection, total, credit_line_total },
} = data
const canSkipPayment =
MathBN.convert(credit_line_total).gte(0) && MathBN.convert(total).lte(0)
if (canSkipPayment) {
return new StepResponse([])
}
if (!isPresent(paymentCollection)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,

View File

@@ -4,6 +4,7 @@ import {
UsageComputedActions,
} from "@medusajs/framework/types"
import {
isDefined,
Modules,
OrderStatus,
OrderWorkflowEvents,
@@ -167,14 +168,16 @@ export const completeCartWorkflow = createWorkflow(
const cartToOrder = transform({ cart, payment }, ({ cart, payment }) => {
const transactions =
payment?.captures?.map((capture) => {
return {
amount: capture.raw_amount ?? capture.amount,
currency_code: payment.currency_code,
reference: "capture",
reference_id: capture.id,
}
}) ?? []
(payment &&
payment?.captures?.map((capture) => {
return {
amount: capture.raw_amount ?? capture.amount,
currency_code: payment.currency_code,
reference: "capture",
reference_id: capture.id,
}
})) ??
[]
const allItems = (cart.items ?? []).map((item) => {
const input: PrepareLineItemDataInput = {
@@ -280,19 +283,31 @@ export const completeCartWorkflow = createWorkflow(
}
})
parallelize(
createRemoteLinkStep([
{
[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,
const linksToCreate = transform(
{ cart, createdOrder },
({ cart, createdOrder }) => {
const links: Record<string, any>[] = [
{
[Modules.ORDER]: { order_id: createdOrder.id },
[Modules.CART]: { cart_id: cart.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]),
reserveInventoryStep(formatedInventoryItems),
emitEventStep({

View File

@@ -45,6 +45,10 @@ export const authorizePaymentSessionStep = createStep(
Modules.PAYMENT
)
if (!input.id) {
return new StepResponse(null)
}
try {
payment = await paymentModule.authorizePaymentSession(
input.id,

View File

@@ -1033,6 +1033,16 @@ export interface CartDTO {
* The raw original shipping tax total of the cart.
*/
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
}
/**

View File

@@ -768,7 +768,7 @@ describe("Total calculation", function () {
},
],
total: 48,
subtotal: 60,
subtotal: 100,
tax_total: 8,
discount_total: 22,
discount_subtotal: 20,

View File

@@ -208,8 +208,6 @@ export function decorateCartTotals(
taxRate: creditLinesSumTaxRate,
})
subtotal = MathBN.sub(subtotal, creditLinesSubtotal)
const taxTotal = MathBN.add(itemsTaxTotal, shippingTaxTotal)
const originalTaxTotal = MathBN.add(
@@ -222,7 +220,7 @@ export function decorateCartTotals(
// TODO: subtract (cart.gift_card_total + cart.gift_card_tax_total)
const tempTotal = MathBN.add(subtotal, taxTotal)
const total = MathBN.sub(tempTotal, discountSubtotal)
const total = MathBN.sub(tempTotal, discountSubtotal, creditLinesTotal)
const cart = cartLike as any