Skip to content

Commit

Permalink
Merge branch 'develop' into chore/improve-retrieve-provider-error-han…
Browse files Browse the repository at this point in the history
…dling
  • Loading branch information
adrien2p authored Nov 19, 2024
2 parents 15258e1 + 36460a3 commit 8d96729
Show file tree
Hide file tree
Showing 25 changed files with 762 additions and 20 deletions.
6 changes: 6 additions & 0 deletions .changeset/selfish-poems-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/payment": patch
"@medusajs/payment-stripe": patch
---

fix(payment): Idempotent cancellation and proper creationg fail handling
233 changes: 233 additions & 0 deletions integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
import {
adminHeaders,
createAdminUser,
generatePublishableKey,
generateStoreHeaders,
} from "../../../../helpers/create-admin-user"
import { createOrderSeeder } from "../../fixtures/order"

jest.setTimeout(300000)

medusaIntegrationTestRunner({
testSuite: ({ dbConnection, getContainer, api }) => {
let order
let customer
let user
let storeHeaders

beforeEach(async () => {
const container = getContainer()

user = (await createAdminUser(dbConnection, adminHeaders, container)).user
const publishableKey = await generatePublishableKey(container)
storeHeaders = generateStoreHeaders({ publishableKey })

const seeders = await createOrderSeeder({ api, container })

const registeredCustomerToken = (
await api.post("/auth/customer/emailpass/register", {
email: "[email protected]",
password: "password",
})
).data.token

customer = (
await api.post(
"/store/customers",
{
email: "[email protected]",
},
{
headers: {
Authorization: `Bearer ${registeredCustomerToken}`,
...storeHeaders.headers,
},
}
)
).data.customer

order = seeders.order
})

describe("Transfer Order flow", () => {
it("should pass order transfer flow from admin successfully", async () => {
// 1. Admin requests order transfer for a customer with an account
await api.post(
`/admin/orders/${order.id}/transfer`,
{
customer_id: customer.id,
},
adminHeaders
)

const orderResult = (
await api.get(
`/admin/orders/${order.id}?fields=+customer_id,+email`,
adminHeaders
)
).data.order

// 2. Order still belongs to the guest customer since the transfer hasn't been accepted yet
expect(orderResult.email).toEqual("[email protected]")
expect(orderResult.customer_id).not.toEqual(customer.id)

const orderPreviewResult = (
await api.get(`/admin/orders/${order.id}/preview`, adminHeaders)
).data.order

expect(orderPreviewResult).toEqual(
expect.objectContaining({
customer_id: customer.id,
order_change: expect.objectContaining({
change_type: "transfer",
status: "requested",
requested_by: user.id,
}),
})
)

const orderChangesResult = (
await api.get(`/admin/orders/${order.id}/changes`, adminHeaders)
).data.order_changes

expect(orderChangesResult.length).toEqual(1)
expect(orderChangesResult[0]).toEqual(
expect.objectContaining({
change_type: "transfer",
status: "requested",
requested_by: user.id,
created_by: user.id,
confirmed_by: null,
confirmed_at: null,
declined_by: null,
actions: expect.arrayContaining([
expect.objectContaining({
version: 2,
action: "TRANSFER_CUSTOMER",
reference: "customer",
reference_id: customer.id,
details: expect.objectContaining({
token: expect.any(String),
original_email: "[email protected]",
}),
}),
]),
})
)

// 3. Guest customer who received the token accepts the transfer
await api.post(
`/store/orders/${order.id}/transfer/accept`,
{ token: orderChangesResult[0].actions[0].details.token },
{
headers: {
...storeHeaders.headers,
},
}
)

const finalOrderResult = (
await api.get(
`/admin/orders/${order.id}?fields=+customer_id,+email`,
adminHeaders
)
).data.order

expect(finalOrderResult.email).toEqual("[email protected]")
// 4. Customer account is now associated with the order (email on the order is still as original, guest email)
expect(finalOrderResult.customer_id).toEqual(customer.id)
})

it("should fail to request order transfer to a guest customer", async () => {
const customer = (
await api.post(
"/admin/customers",
{
first_name: "guest",
email: "[email protected]",
},
adminHeaders
)
).data.customer

const err = await api
.post(
`/admin/orders/${order.id}/transfer`,
{
customer_id: customer.id,
},
adminHeaders
)
.catch((e) => e)

expect(err.response.status).toBe(400)
expect(err.response.data).toEqual(
expect.objectContaining({
type: "invalid_data",
message: `Cannot transfer order: ${order.id} to a guest customer account: [email protected]`,
})
)
})

it("should fail to accept order transfer with invalid token", async () => {
await api.post(
`/admin/orders/${order.id}/transfer`,
{
customer_id: customer.id,
},
adminHeaders
)

const orderChangesResult = (
await api.get(`/admin/orders/${order.id}/changes`, adminHeaders)
).data.order_changes

expect(orderChangesResult.length).toEqual(1)
expect(orderChangesResult[0]).toEqual(
expect.objectContaining({
change_type: "transfer",
status: "requested",
requested_by: user.id,
created_by: user.id,
confirmed_by: null,
confirmed_at: null,
declined_by: null,
actions: expect.arrayContaining([
expect.objectContaining({
version: 2,
action: "TRANSFER_CUSTOMER",
reference: "customer",
reference_id: customer.id,
details: expect.objectContaining({
token: expect.any(String),
original_email: "[email protected]",
}),
}),
]),
})
)

const err = await api
.post(
`/store/orders/${order.id}/transfer/accept`,
{ token: "fake-token" },
{
headers: {
...storeHeaders.headers,
},
}
)
.catch((e) => e)

expect(err.response.status).toBe(400)
expect(err.response.data).toEqual(
expect.objectContaining({
type: "not_allowed",
message: `Invalid token.`,
})
)
})
})
},
})
2 changes: 2 additions & 0 deletions packages/core/core-flows/src/order/workflows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,5 @@ export * from "./return/update-return-shipping-method"
export * from "./update-order-change-actions"
export * from "./update-order-changes"
export * from "./update-tax-lines"
export * from "./transfer/request-order-transfer"
export * from "./transfer/accept-order-transfer"
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
OrderChangeDTO,
OrderDTO,
OrderWorkflow,
} from "@medusajs/framework/types"
import {
WorkflowData,
WorkflowResponse,
createStep,
createWorkflow,
} from "@medusajs/framework/workflows-sdk"
import { OrderPreviewDTO } from "@medusajs/types"

import { useRemoteQueryStep } from "../../../common"
import { throwIfOrderIsCancelled } from "../../utils/order-validation"
import { previewOrderChangeStep } from "../../steps"
import {
ChangeActionType,
MedusaError,
OrderChangeStatus,
} from "@medusajs/utils"
import { confirmOrderChanges } from "../../steps/confirm-order-changes"

/**
* This step validates that an order transfer can be accepted.
*/
export const acceptOrderTransferValidationStep = createStep(
"accept-order-transfer-validation",
async function ({
token,
order,
orderChange,
}: {
token: string
order: OrderDTO
orderChange: OrderChangeDTO
}) {
throwIfOrderIsCancelled({ order })

if (!orderChange || orderChange.change_type !== "transfer") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Order ${order.id} does not have an order transfer request.`
)
}
const transferCustomerAction = orderChange.actions.find(
(a) => a.action === ChangeActionType.TRANSFER_CUSTOMER
)

if (!token.length || token !== transferCustomerAction?.details!.token) {
throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Invalid token.")
}
}
)

export const acceptOrderTransferWorkflowId = "accept-order-transfer-workflow"
/**
* This workflow accepts an order transfer.
*/
export const acceptOrderTransferWorkflow = createWorkflow(
acceptOrderTransferWorkflowId,
function (
input: WorkflowData<OrderWorkflow.AcceptOrderTransferWorkflowInput>
): WorkflowResponse<OrderPreviewDTO> {
const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders",
fields: ["id", "email", "status", "customer_id"],
variables: { id: input.order_id },
list: false,
throw_if_key_not_found: true,
})

const orderChange: OrderChangeDTO = useRemoteQueryStep({
entry_point: "order_change",
fields: [
"id",
"status",
"change_type",
"actions.id",
"actions.order_id",
"actions.action",
"actions.details",
"actions.reference",
"actions.reference_id",
"actions.internal_note",
],
variables: {
filters: {
order_id: input.order_id,
status: [OrderChangeStatus.REQUESTED],
},
},
list: false,
}).config({ name: "order-change-query" })

acceptOrderTransferValidationStep({
order,
orderChange,
token: input.token,
})

confirmOrderChanges({
changes: [orderChange],
orderId: order.id,
})

return new WorkflowResponse(previewOrderChangeStep(input.order_id))
}
)
Loading

0 comments on commit 8d96729

Please sign in to comment.