-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'develop' into chore/improve-retrieve-provider-error-han…
…dling
- Loading branch information
Showing
25 changed files
with
762 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
233
integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`, | ||
}) | ||
) | ||
}) | ||
}) | ||
}, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} | ||
) |
Oops, something went wrong.