chore(cart): completed at (#8921)
This commit is contained in:
@@ -1972,6 +1972,10 @@ medusaIntegrationTestRunner({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
it("should create an order and create item reservations", async () => {
|
it("should create an order and create item reservations", async () => {
|
||||||
const cart = (
|
const cart = (
|
||||||
await api.post(`/store/carts`, {
|
await api.post(`/store/carts`, {
|
||||||
@@ -2136,6 +2140,62 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should fail to update cart when it is completed", async () => {
|
||||||
|
const cart = (
|
||||||
|
await api.post(`/store/carts`, {
|
||||||
|
currency_code: "usd",
|
||||||
|
email: "tony@stark-industries.com",
|
||||||
|
shipping_address: {
|
||||||
|
address_1: "test address 1",
|
||||||
|
address_2: "test address 2",
|
||||||
|
city: "ny",
|
||||||
|
country_code: "us",
|
||||||
|
province: "ny",
|
||||||
|
postal_code: "94016",
|
||||||
|
},
|
||||||
|
sales_channel_id: salesChannel.id,
|
||||||
|
items: [{ quantity: 1, variant_id: product.variants[0].id }],
|
||||||
|
})
|
||||||
|
).data.cart
|
||||||
|
|
||||||
|
const paymentCollection = (
|
||||||
|
await api.post(`/store/payment-collections`, {
|
||||||
|
cart_id: cart.id,
|
||||||
|
})
|
||||||
|
).data.payment_collection
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/store/payment-collections/${paymentCollection.id}/payment-sessions`,
|
||||||
|
{ provider_id: "pp_system_default" }
|
||||||
|
)
|
||||||
|
|
||||||
|
await api.post(`/store/carts/${cart.id}/complete`, {})
|
||||||
|
|
||||||
|
const cartRefetch = (await api.get(`/store/carts/${cart.id}`)).data
|
||||||
|
.cart
|
||||||
|
|
||||||
|
expect(cartRefetch.completed_at).toBeTruthy()
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
api.post(`/store/carts/${cart.id}/shipping-methods`, {
|
||||||
|
option_id: shippingOption.id,
|
||||||
|
})
|
||||||
|
).rejects.toThrow()
|
||||||
|
|
||||||
|
const error = await api
|
||||||
|
.post(`/store/carts/${cart.id}/line-items`, {
|
||||||
|
variant_id: product.variants[0].id,
|
||||||
|
quantity: 1,
|
||||||
|
})
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
type: "invalid_data",
|
||||||
|
message: `Cart ${cart.id} is already completed.`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("should return cart when payment authorization fails", async () => {
|
it("should return cart when payment authorization fails", async () => {
|
||||||
const authorizePaymentSessionSpy = jest.spyOn(
|
const authorizePaymentSessionSpy = jest.spyOn(
|
||||||
PaymentModuleService.prototype,
|
PaymentModuleService.prototype,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { CartDTO, CartWorkflowDTO } from "@medusajs/types"
|
||||||
|
import { MedusaError, isPresent } from "@medusajs/utils"
|
||||||
|
import { createStep } from "@medusajs/workflows-sdk"
|
||||||
|
|
||||||
|
export interface ValidateCartStepInput {
|
||||||
|
cart: CartWorkflowDTO | CartDTO
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateCartStepId = "validate-cart"
|
||||||
|
/**
|
||||||
|
* This step validates a cart's before editing it.
|
||||||
|
*/
|
||||||
|
export const validateCartStep = createStep(
|
||||||
|
validateCartStepId,
|
||||||
|
async (data: ValidateCartStepInput) => {
|
||||||
|
const { cart } = data
|
||||||
|
|
||||||
|
if (!isPresent(cart)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cart does not exist`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cart.completed_at) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_DATA,
|
||||||
|
`Cart ${cart.id} is already completed.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
export const cartFieldsForRefreshSteps = [
|
export const cartFieldsForRefreshSteps = [
|
||||||
|
"id",
|
||||||
"subtotal",
|
"subtotal",
|
||||||
"item_subtotal",
|
"item_subtotal",
|
||||||
"shipping_subtotal",
|
"shipping_subtotal",
|
||||||
"region_id",
|
"region_id",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
|
"completed_at",
|
||||||
"region.*",
|
"region.*",
|
||||||
"items.*",
|
"items.*",
|
||||||
"items.product.id",
|
"items.product.id",
|
||||||
@@ -26,6 +28,7 @@ export const completeCartFields = [
|
|||||||
"email",
|
"email",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"completed_at",
|
||||||
"total",
|
"total",
|
||||||
"subtotal",
|
"subtotal",
|
||||||
"tax_total",
|
"tax_total",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
removeShippingMethodFromCartStep,
|
removeShippingMethodFromCartStep,
|
||||||
validateCartShippingOptionsStep,
|
validateCartShippingOptionsStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
||||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||||
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
||||||
@@ -37,6 +38,8 @@ export const addShippingMethodToWorkflow = createWorkflow(
|
|||||||
list: false,
|
list: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
validateCartStep({ cart })
|
||||||
|
|
||||||
const optionIds = transform({ input }, (data) => {
|
const optionIds = transform({ input }, (data) => {
|
||||||
return (data.input.options ?? []).map((i) => i.id)
|
return (data.input.options ?? []).map((i) => i.id)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
refreshCartShippingMethodsStep,
|
refreshCartShippingMethodsStep,
|
||||||
updateLineItemsStep,
|
updateLineItemsStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
import {
|
import {
|
||||||
cartFieldsForRefreshSteps,
|
cartFieldsForRefreshSteps,
|
||||||
@@ -34,6 +35,8 @@ export const addToCartWorkflowId = "add-to-cart"
|
|||||||
export const addToCartWorkflow = createWorkflow(
|
export const addToCartWorkflow = createWorkflow(
|
||||||
addToCartWorkflowId,
|
addToCartWorkflowId,
|
||||||
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
|
(input: WorkflowData<AddToCartWorkflowInputDTO>) => {
|
||||||
|
validateCartStep(input)
|
||||||
|
|
||||||
const variantIds = transform({ input }, (data) => {
|
const variantIds = transform({ input }, (data) => {
|
||||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||||
})
|
})
|
||||||
@@ -67,9 +70,10 @@ export const addToCartWorkflow = createWorkflow(
|
|||||||
|
|
||||||
return prepareLineItemData({
|
return prepareLineItemData({
|
||||||
variant: variant,
|
variant: variant,
|
||||||
unitPrice: item.unit_price ||
|
unitPrice:
|
||||||
variant.calculated_price.calculated_amount,
|
item.unit_price || variant.calculated_price.calculated_amount,
|
||||||
isTaxInclusive: item.is_tax_inclusive ||
|
isTaxInclusive:
|
||||||
|
item.is_tax_inclusive ||
|
||||||
variant.calculated_price.is_calculated_price_tax_inclusive,
|
variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
metadata: item?.metadata ?? {},
|
metadata: item?.metadata ?? {},
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ import {
|
|||||||
} from "../../common"
|
} from "../../common"
|
||||||
import { createOrdersStep } from "../../order/steps/create-orders"
|
import { createOrdersStep } from "../../order/steps/create-orders"
|
||||||
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
|
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
|
||||||
import { validateCartPaymentsStep } from "../steps"
|
import { updateCartsStep, validateCartPaymentsStep } from "../steps"
|
||||||
import { reserveInventoryStep } from "../steps/reserve-inventory"
|
import { reserveInventoryStep } from "../steps/reserve-inventory"
|
||||||
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
import { completeCartFields } from "../utils/fields"
|
import { completeCartFields } from "../utils/fields"
|
||||||
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
|
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
|
||||||
import {
|
import {
|
||||||
@@ -44,6 +45,8 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
list: false,
|
list: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
validateCartStep({ cart })
|
||||||
|
|
||||||
const paymentSessions = validateCartPaymentsStep({ cart })
|
const paymentSessions = validateCartPaymentsStep({ cart })
|
||||||
|
|
||||||
authorizePaymentSessionStep({
|
authorizePaymentSessionStep({
|
||||||
@@ -158,6 +161,13 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
({ createdOrders }) => createdOrders[0]
|
({ createdOrders }) => createdOrders[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const updateCompletedAt = transform({ cart }, ({ cart }) => {
|
||||||
|
return {
|
||||||
|
id: cart.id,
|
||||||
|
completed_at: new Date(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
parallelize(
|
parallelize(
|
||||||
createRemoteLinkStep([
|
createRemoteLinkStep([
|
||||||
{
|
{
|
||||||
@@ -171,6 +181,7 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
updateCartsStep([updateCompletedAt]),
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: OrderWorkflowEvents.PLACED,
|
eventName: OrderWorkflowEvents.PLACED,
|
||||||
data: { id: order.id },
|
data: { id: order.id },
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { createRemoteLinkStep } from "../../common/steps/create-remote-links"
|
import { createRemoteLinkStep } from "../../common/steps/create-remote-links"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
import { createPaymentCollectionsStep } from "../steps/create-payment-collection"
|
import { createPaymentCollectionsStep } from "../steps/create-payment-collection"
|
||||||
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This step validates that a cart doesn't have a payment collection.
|
* This step validates that a cart doesn't have a payment collection.
|
||||||
@@ -40,6 +41,7 @@ export const createPaymentCollectionForCartWorkflow = createWorkflow(
|
|||||||
fields: [
|
fields: [
|
||||||
"id",
|
"id",
|
||||||
"region_id",
|
"region_id",
|
||||||
|
"completed_at",
|
||||||
"currency_code",
|
"currency_code",
|
||||||
"total",
|
"total",
|
||||||
"raw_total",
|
"raw_total",
|
||||||
@@ -50,6 +52,8 @@ export const createPaymentCollectionForCartWorkflow = createWorkflow(
|
|||||||
list: false,
|
list: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
validateCartStep({ cart })
|
||||||
|
|
||||||
validateExistingPaymentCollectionStep({ cart })
|
validateExistingPaymentCollectionStep({ cart })
|
||||||
|
|
||||||
const paymentData = transform({ cart }, ({ cart }) => {
|
const paymentData = transform({ cart }, ({ cart }) => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
|
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
|
||||||
import { refreshCartShippingMethodsStep } from "../steps"
|
import { refreshCartShippingMethodsStep } from "../steps"
|
||||||
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
import {
|
import {
|
||||||
cartFieldsForRefreshSteps,
|
cartFieldsForRefreshSteps,
|
||||||
@@ -27,6 +28,8 @@ export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
|||||||
export const updateLineItemInCartWorkflow = createWorkflow(
|
export const updateLineItemInCartWorkflow = createWorkflow(
|
||||||
updateLineItemInCartWorkflowId,
|
updateLineItemInCartWorkflowId,
|
||||||
(input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO>) => {
|
(input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO>) => {
|
||||||
|
validateCartStep(input)
|
||||||
|
|
||||||
const variantIds = transform({ input }, (data) => {
|
const variantIds = transform({ input }, (data) => {
|
||||||
return [data.input.item.variant_id]
|
return [data.input.item.variant_id]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -782,6 +782,11 @@ export interface CartDTO {
|
|||||||
*/
|
*/
|
||||||
metadata?: Record<string, unknown> | null
|
metadata?: Record<string, unknown> | null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the cart was completed.
|
||||||
|
*/
|
||||||
|
completed_at?: string | Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When the cart was created.
|
* When the cart was created.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const defaultStoreCartFields = [
|
|||||||
"email",
|
"email",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
|
"completed_at",
|
||||||
"total",
|
"total",
|
||||||
"subtotal",
|
"subtotal",
|
||||||
"tax_total",
|
"tax_total",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Migration } from "@mikro-orm/migrations"
|
||||||
|
|
||||||
|
export class Migration20240831125857 extends Migration {
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "cart" add column if not exists "completed_at" timestamptz null;'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql(
|
||||||
|
'alter table if exists "cart" drop column if exists "completed_at";'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -147,6 +147,9 @@ export default class Cart {
|
|||||||
})
|
})
|
||||||
shipping_methods = new Collection<Rel<ShippingMethod>>(this)
|
shipping_methods = new Collection<Rel<ShippingMethod>>(this)
|
||||||
|
|
||||||
|
@Property({ columnType: "timestamptz", nullable: true })
|
||||||
|
completed_at: Date | null = null
|
||||||
|
|
||||||
@Property({
|
@Property({
|
||||||
onCreate: () => new Date(),
|
onCreate: () => new Date(),
|
||||||
columnType: "timestamptz",
|
columnType: "timestamptz",
|
||||||
|
|||||||
Reference in New Issue
Block a user