medusajs / medusa

The world's most flexible commerce platform.
https://medusajs.com
MIT License
26.26k stars 2.67k forks source link

Bug: medusajs/medusa/core-flows completeCartWorkflow #9878

Closed 420coupe closed 3 weeks ago

420coupe commented 1 month ago

Bug report

Describe the bug

When trying to complete an order and the reroute from placeOrder() the country_code is coming back undefined, but it is defined in the database entry. This is because completeCartWorkflow is only returning the order.id when it used to return the entire order.

System information

Medusa version (including plugins):

yarn list v1.22.22
├─ @medusajs/admin-bundler@2.0.0-rc-20241022183311
├─ @medusajs/admin-sdk@2.0.0-rc-20241022183311
├─ @medusajs/admin-shared@2.0.0-rc-20241022183311
├─ @medusajs/admin-vite-plugin@2.0.0-rc-20241022183311
├─ @medusajs/api-key@2.0.0-rc-20241022183311
├─ @medusajs/auth-emailpass@2.0.0-rc-20241022183311
├─ @medusajs/auth-github@2.0.0-rc-20241022183311
├─ @medusajs/auth-google@2.0.0-rc-20241022183311
├─ @medusajs/auth@2.0.0-rc-20241022183311
├─ @medusajs/cache-inmemory@2.0.0-rc-20241022183311
├─ @medusajs/cache-redis@2.0.0-rc-20241022183311
├─ @medusajs/cart@2.0.0-rc-20241022183311
├─ @medusajs/cli@2.0.0-rc-20241022183311
├─ @medusajs/core-flows@2.0.0-rc-20241022183311
├─ @medusajs/currency@2.0.0-rc-20241022183311
├─ @medusajs/customer@2.0.0-rc-20241022183311
├─ @medusajs/dashboard@2.0.0-rc-20241022183311
├─ @medusajs/event-bus-local@2.0.0-rc-20241022183311
├─ @medusajs/event-bus-redis@2.0.0-rc-20241022183311
├─ @medusajs/file-local@2.0.0-rc-20241022183311
├─ @medusajs/file-s3@2.0.0-rc-20241022183311
├─ @medusajs/file@2.0.0-rc-20241022183311
├─ @medusajs/framework@2.0.0-rc-20241022183311
├─ @medusajs/fulfillment-manual@2.0.0-rc-20241022183311
├─ @medusajs/fulfillment@2.0.0-rc-20241022183311
├─ @medusajs/icons@2.0.0-rc-20241022183311
├─ @medusajs/index@2.0.0-rc-20241022183311
├─ @medusajs/inventory@2.0.0-rc-20241022183311
├─ @medusajs/js-sdk@2.0.0-rc-20241022183311
├─ @medusajs/link-modules@2.0.0-rc-20241022183311
├─ @medusajs/locking-postgres@2.0.0-rc-20241022183311
├─ @medusajs/locking-redis@2.0.0-rc-20241022183311
├─ @medusajs/locking@2.0.0-rc-20241022183311
├─ @medusajs/medusa@2.0.0-rc-20241022183311
├─ @medusajs/modules-sdk@2.0.0-rc-20241022183311
├─ @medusajs/notification-local@2.0.0-rc-20241022183311
├─ @medusajs/notification-sendgrid@2.0.0-rc-20241022183311
├─ @medusajs/notification@2.0.0-rc-20241022183311
├─ @medusajs/orchestration@2.0.0-rc-20241022183311
├─ @medusajs/order@2.0.0-rc-20241022183311
├─ @medusajs/payment-stripe@2.0.0-rc-20241022183311
├─ @medusajs/payment@2.0.0-rc-20241022183311
├─ @medusajs/pricing@2.0.0-rc-20241022183311
├─ @medusajs/product@2.0.0-rc-20241022183311
├─ @medusajs/promotion@2.0.0-rc-20241022183311
├─ @medusajs/region@2.0.0-rc-20241022183311
├─ @medusajs/sales-channel@2.0.0-rc-20241022183311
├─ @medusajs/stock-location@2.0.0-rc-20241022183311
├─ @medusajs/store@2.0.0-rc-20241022183311
├─ @medusajs/tax@2.0.0-rc-20241022183311
├─ @medusajs/telemetry@2.0.0-rc-20241022183311
├─ @medusajs/test-utils@2.0.0-rc-20241022183311
├─ @medusajs/types@2.0.0-rc-20241022183311
├─ @medusajs/ui@4.0.0-rc-20241022183311
├─ @medusajs/user@2.0.0-rc-20241022183311
├─ @medusajs/utils@2.0.0-rc-20241022183311
├─ @medusajs/workflow-engine-inmemory@2.0.0-rc-20241022183311
├─ @medusajs/workflow-engine-redis@2.0.0-rc-20241022183311
└─ @medusajs/workflows-sdk@2.0.0-rc-20241022183311

Node.js version: v21.7.1 Database: Supabase (postgres) Operating system:Ubuntu 22.04.4 LTS`` Browser (if relevant):

Steps to reproduce the behavior

  1. Stand up latest RC medusajs backend
  2. Stand up nextjs starter front end
  3. add product and attempt to checkout
  4. See error

Expected behavior

When you complete the checkout it should redirect to the order/confirmed page, however it redirects to undefined/order/confrimed/[id]

Screenshots

image

Code snippets

export async function placeOrder(cartId?: string) {
  const currentCartId = cartId || getCartId()
  if (!currentCartId) {
    throw new Error("No existing cart found when placing an order")
  }

  const cartRes = await sdk.store.cart
    .complete(currentCartId, {}, getAuthHeaders())
    .then((cartRes) => {
      revalidateTag("cart")
      return cartRes
    })
    .catch(medusaError)

  if (cartRes?.type === "order") {
    const countryCode =
      cartRes.order.shipping_address?.country_code?.toLowerCase()
    removeCartId()
    redirect(`/${countryCode}/order/confirmed/${cartRes?.order.id}`)
  }

  return cartRes.cart
}
420coupe commented 1 month ago

it looks like the sdk.store.cart.complete() is not returning the complete order only the order.id see response to console.log('cartRes: ', cartRes) below.

cartRes:  {
  type: 'order',
  order: { id: 'order_01JBG1P59HK2BX2VDG1ER21FWK' },
  digital_product_order: {
    id: '01JBG1P89414WC8V01QMR6PZFW',
    status: 'pending',
    products: [ [Object] ],
    created_at: '2024-10-31T01:05:27.333Z',
    updated_at: '2024-10-31T01:05:27.333Z'
  }
}
420coupe commented 1 month ago

any update/feedback on this?

i've tried the following adding it as part of the complete query using +shipping_address,*shipping_address,shipping_address and none of these returned anything. Was there a change to the cart complete API that no longer returns the StoreOrder and only the order.id?

src/lib/data/cart.ts

export async function placeOrder(cartId?: string) {
  const currentCartId = cartId || getCartId()
  if (!currentCartId) {
    throw new Error("No existing cart found when placing an order")
  }

  const cartRes = await sdk.store.cart
    .complete(currentCartId, { fields: "shipping_address" }, getAuthHeaders())
    .then((cartRes) => {
      revalidateTag("cart")
      return cartRes
    })
    .catch(medusaError)

  if (cartRes?.type === "order") {
    console.log("cartRes: ", cartRes)
    const countryCode =
      cartRes.order.shipping_address?.country_code?.toLowerCase()
    removeCartId()
    redirect(`/${countryCode}/order/confirmed/${cartRes?.order.id}`)
  }

  return cartRes.cart
}
420coupe commented 1 month ago

ok think i have pin pointed the issue, i forgot i had the default endpoint overrode with the digital products recipe endpoint. It's the completeCartWorkflow is only returning the order.id when it used to return the entire order.

import {
  createWorkflow,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk";
import {
  completeCartWorkflow,
  useRemoteQueryStep,
  createRemoteLinkStep,
  createOrderFulfillmentWorkflow,
  emitEventStep,
} from "@medusajs/medusa/core-flows";
import { Modules } from "@medusajs/framework/utils";
import createDigitalProductOrderStep from "./steps/create-digital-product-order";
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product";

type WorkflowInput = {
  cart_id: string;
};

const createDigitalProductOrderWorkflow = createWorkflow(
  "create-digital-product-order",
  (input: WorkflowInput) => {
    const order = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    });

    const { items } = useRemoteQueryStep({
      entry_point: "order",
      fields: [
        "*",
        "items.*",
        "items.variant.*",
        "items.variant.digital_product.*",
      ],
      variables: {
        filters: {
          id: order.id,
        },
      },
      throw_if_key_not_found: true,
      list: false,
    });

    const itemsWithDigitalProducts = transform(
      {
        items,
      },
      (data) => {
        return data.items.filter(
          (item) => item.variant.digital_product !== undefined
        );
      }
    );

    const digital_product_order = when(
      itemsWithDigitalProducts,
      (itemsWithDigitalProducts) => {
        return itemsWithDigitalProducts.length;
      }
    ).then(() => {
      const { digital_product_order } = createDigitalProductOrderStep({
        items,
      });

      createRemoteLinkStep([
        {
          [DIGITAL_PRODUCT_MODULE]: {
            digital_product_order_id: digital_product_order.id,
          },
          [Modules.ORDER]: {
            order_id: order.id,
          },
        },
      ]);

      createOrderFulfillmentWorkflow.runAsStep({
        input: {
          order_id: order.id,
          items: transform(
            {
              itemsWithDigitalProducts,
            },
            (data) => {
              return data.itemsWithDigitalProducts.map((item) => ({
                id: item.id,
                quantity: item.quantity,
              }));
            }
          ),
        },
      });

      emitEventStep({
        eventName: "digital_product_order.created",
        data: {
          id: digital_product_order.id,
        },
      });

      return digital_product_order;
    });

    return new WorkflowResponse({
      order,
      digital_product_order,
    });
  }
);

export default createDigitalProductOrderWorkflow;
420coupe commented 1 month ago

looks like commit below changed the response for completeCartWorkflow. is there a way to pass a custom query to the completeCartWorkflow to get the order.shipping_address returned

https://github.com/medusajs/medusa/commit/f7472a6fa6e44c0d8e3d766cb6db9d50dc4753a3#diff-d2e435ab4b47e02a3aa716045297394e41576effaf3bb0bd6e832a8500688bb7

sradevski commented 3 weeks ago

@420coupe this is intentional, as the HTTP layer can then refetch the order and also do the field selection, among other things, see https://github.com/medusajs/medusa/blob/develop/packages/medusa/src/api/store/carts/%5Bid%5D/complete/route.ts

I think you can just do the same in your HTTP route handler. I'll close this as the behavior is intended, but let me know if something doesn't work as expected.

420coupe commented 3 weeks ago

@420coupe this is intentional, as the HTTP layer can then refetch the order and also do the field selection, among other things, see https://github.com/medusajs/medusa/blob/develop/packages/medusa/src/api/store/carts/%5Bid%5D/complete/route.ts

I think you can just do the same in your HTTP route handler. I'll close this as the behavior is intended, but let me know if something doesn't work as expected.

So how would this be resolved for the digital product recipe to get it to return the complete order instead of just the order.id?

src/workflows/create-digital-product-order/index.ts

import {
  createWorkflow,
  transform,
  when,
  WorkflowResponse,
} from "@medusajs/framework/workflows-sdk";
import {
  completeCartWorkflow,
  useRemoteQueryStep,
  createRemoteLinkStep,
  createOrderFulfillmentWorkflow,
  emitEventStep,
} from "@medusajs/medusa/core-flows";
import { Modules } from "@medusajs/framework/utils";
import createDigitalProductOrderStep from "./steps/create-digital-product-order";
import { DIGITAL_PRODUCT_MODULE } from "../../modules/digital-product";

type WorkflowInput = {
  cart_id: string;
};

const createDigitalProductOrderWorkflow = createWorkflow(
  "create-digital-product-order",
  (input: WorkflowInput) => {
    const order = completeCartWorkflow.runAsStep({
      input: {
        id: input.cart_id,
      },
    });

    const { items } = useRemoteQueryStep({
      entry_point: "order",
      fields: [
        "*",
        "items.*",
        "items.variant.*",
        "items.variant.digital_product.*",
      ],
      variables: {
        filters: {
          id: order.id,
        },
      },
      throw_if_key_not_found: true,
      list: false,
    });

    const itemsWithDigitalProducts = transform(
      {
        items,
      },
      (data) => {
        return data.items.filter(
          (item) => item.variant.digital_product !== undefined
        );
      }
    );

    const digital_product_order = when(
      itemsWithDigitalProducts,
      (itemsWithDigitalProducts) => {
        return itemsWithDigitalProducts.length;
      }
    ).then(() => {
      const { digital_product_order } = createDigitalProductOrderStep({
        items,
      });

      createRemoteLinkStep([
        {
          [DIGITAL_PRODUCT_MODULE]: {
            digital_product_order_id: digital_product_order.id,
          },
          [Modules.ORDER]: {
            order_id: order.id,
          },
        },
      ]);

      createOrderFulfillmentWorkflow.runAsStep({
        input: {
          order_id: order.id,
          items: transform(
            {
              itemsWithDigitalProducts,
            },
            (data) => {
              return data.itemsWithDigitalProducts.map((item) => ({
                id: item.id,
                quantity: item.quantity,
              }));
            }
          ),
        },
      });

      emitEventStep({
        eventName: "digital_product_order.created",
        data: {
          id: digital_product_order.id,
        },
      });

      return digital_product_order;
    });

    return new WorkflowResponse({
      order,
      digital_product_order,
    });
  }
);

export default createDigitalProductOrderWorkflow;

src/api/store/carts/[id]/complete/route.ts

import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http";
import createDigitalProductOrderWorkflow from "../../../../../workflows/create-digital-product-order";

export const POST = async (req: MedusaRequest, res: MedusaResponse) => {
  const { result } = await createDigitalProductOrderWorkflow(req.scope).run({
    input: {
      cart_id: req.params.id,
    },
  });

  res.json({
    type: "order",
    ...result,
  });
};
sradevski commented 3 weeks ago

You are returning the order in the workflow, so before res.json you would do

  const { data } = await query.graph({
    entity: "order",
    fields: req.remoteQueryConfig.fields,
    filters: { id: result.order.id }, // or result.digital_product_order.id, depending what you want to return
  })

  res.json({
    type: "order",
    order: data,
  });

but let me know if I am missing something here.

420coupe commented 3 weeks ago

You are returning the order in the workflow, so before res.json you would do

  const { data } = await query.graph({
    entity: "order",
    fields: req.remoteQueryConfig.fields,
    filters: { id: result.order.id }, // or result.digital_product_order.id, depending what you want to return
  })

  res.json({
    type: "order",
    order: data,
  });

but let me know if I am missing something here.

i was trying to avoid another query and have it return in the same original query how it did prior.

sradevski commented 3 weeks ago

This is a common pattern we follow for most endpoints, and since it's an ID-based query it is indexed and very performant, so I wouldn't worry too much about the additional request. Eg. in this case if you have 10k orders per month, it equates to 10k additional DB queries, which is insignificant, and it doesn't make sense to prematurely optimize