diff --git a/.changeset/tricky-zoos-perform.md b/.changeset/tricky-zoos-perform.md new file mode 100644 index 0000000000..37d7f8c787 --- /dev/null +++ b/.changeset/tricky-zoos-perform.md @@ -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 diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index f6ae1b3d21..321c32df1f 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -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`, { diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index 7e5e5ea124..bb8b26bc83 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -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, diff --git a/packages/core/core-flows/src/cart/steps/validate-cart-payments.ts b/packages/core/core-flows/src/cart/steps/validate-cart-payments.ts index 10687628c5..af00ba9c24 100644 --- a/packages/core/core-flows/src/cart/steps/validate-cart-payments.ts +++ b/packages/core/core-flows/src/cart/steps/validate-cart-payments.ts @@ -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, diff --git a/packages/core/core-flows/src/cart/workflows/complete-cart.ts b/packages/core/core-flows/src/cart/workflows/complete-cart.ts index 95ad4d0f24..f0b69fde68 100644 --- a/packages/core/core-flows/src/cart/workflows/complete-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/complete-cart.ts @@ -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[] = [ + { + [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({ diff --git a/packages/core/core-flows/src/payment/steps/authorize-payment-session.ts b/packages/core/core-flows/src/payment/steps/authorize-payment-session.ts index 826556b473..b621f2d1d3 100644 --- a/packages/core/core-flows/src/payment/steps/authorize-payment-session.ts +++ b/packages/core/core-flows/src/payment/steps/authorize-payment-session.ts @@ -45,6 +45,10 @@ export const authorizePaymentSessionStep = createStep( Modules.PAYMENT ) + if (!input.id) { + return new StepResponse(null) + } + try { payment = await paymentModule.authorizePaymentSession( input.id, diff --git a/packages/core/types/src/cart/common.ts b/packages/core/types/src/cart/common.ts index bafe642ea8..9e95e7a625 100644 --- a/packages/core/types/src/cart/common.ts +++ b/packages/core/types/src/cart/common.ts @@ -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 } /** diff --git a/packages/core/utils/src/totals/__tests__/totals.ts b/packages/core/utils/src/totals/__tests__/totals.ts index 4d0d89b11f..7cbddda32b 100644 --- a/packages/core/utils/src/totals/__tests__/totals.ts +++ b/packages/core/utils/src/totals/__tests__/totals.ts @@ -768,7 +768,7 @@ describe("Total calculation", function () { }, ], total: 48, - subtotal: 60, + subtotal: 100, tax_total: 8, discount_total: 22, discount_subtotal: 20, diff --git a/packages/core/utils/src/totals/cart/index.ts b/packages/core/utils/src/totals/cart/index.ts index 66ef9d322b..5db41f2b40 100644 --- a/packages/core/utils/src/totals/cart/index.ts +++ b/packages/core/utils/src/totals/cart/index.ts @@ -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