feat(medusa): Claim customer orders (#2710)

This commit is contained in:
Philip Korsholm
2022-12-08 17:48:49 +01:00
committed by GitHub
parent 86f9455d00
commit a6243618fe
36 changed files with 872 additions and 107 deletions
@@ -54,8 +54,8 @@ export default async (req, res) => {
try {
const customerService: CustomerService =
req.scope.resolve("customerService")
const customer = await customerService.retrieveByEmail(email, {
select: ["has_account"],
const customer = await customerService.retrieveRegisteredByEmail(email, {
select: ["id", "has_account"],
})
res.status(200).json({ exists: customer.has_account })
} catch (err) {
@@ -137,6 +137,10 @@ export default async (req, res) => {
const cartService: CartService = req.scope.resolve("cartService")
const manager: EntityManager = req.scope.resolve("manager")
if (req.user?.customer_id) {
validated.customer_id = req.user.customer_id
}
await manager.transaction(async (transactionManager) => {
await cartService.withTransaction(transactionManager).update(id, validated)
@@ -8,7 +8,7 @@ describe("POST /store/customers/password-token", () => {
beforeAll(async () => {
subject = await request("POST", `/store/customers/password-token`, {
payload: {
email: "lebron@james.com",
email: "lebron@james1.com",
},
})
})
@@ -18,10 +18,12 @@ describe("POST /store/customers/password-token", () => {
})
it("calls CustomerService retrieve", () => {
expect(CustomerServiceMock.retrieveByEmail).toHaveBeenCalledTimes(1)
expect(CustomerServiceMock.retrieveByEmail).toHaveBeenCalledWith(
"lebron@james.com"
)
expect(
CustomerServiceMock.retrieveRegisteredByEmail
).toHaveBeenCalledTimes(1)
expect(
CustomerServiceMock.retrieveRegisteredByEmail
).toHaveBeenCalledWith("lebron@james1.com")
})
it("calls CustomerService retrieve", () => {
@@ -33,7 +35,7 @@ describe("POST /store/customers/password-token", () => {
).toHaveBeenCalledWith(IdMap.getId("lebron"))
})
it("returns customer decorated", () => {
it("returns success", () => {
expect(subject.status).toEqual(204)
})
})
@@ -9,7 +9,7 @@ describe("POST /store/customers/password-reset", () => {
beforeAll(async () => {
subject = await request("POST", `/store/customers/password-reset`, {
payload: {
email: "lebron@james.com",
email: "lebron@james1.com",
token: jwt.sign({ customer_id: IdMap.getId("lebron") }, "1234"),
password: "TheGame",
},
@@ -47,7 +47,7 @@ describe("POST /store/customers/password-reset", () => {
beforeAll(async () => {
subject = await request("POST", `/store/customers/password-reset`, {
payload: {
email: "lebron@james.com",
email: "lebron@james1.com",
token: jwt.sign({ customer_id: IdMap.getId("not-lebron") }, "1234"),
password: "TheGame",
},
@@ -120,6 +120,11 @@ export default async (req, res) => {
}
)
customer = await customerService.retrieve(customer.id, {
relations: defaultStoreCustomersRelations,
select: defaultStoreCustomersFields,
})
// Add JWT to cookie
const {
projectConfig: { jwt_secret },
@@ -128,19 +133,16 @@ export default async (req, res) => {
expiresIn: "30d",
})
customer = await customerService.retrieve(customer.id, {
relations: defaultStoreCustomersRelations,
select: defaultStoreCustomersFields,
})
res.status(200).json({ customer })
}
export class StorePostCustomersReq {
@IsString()
@IsOptional()
first_name: string
@IsString()
@IsOptional()
last_name: string
@IsEmail()
@@ -1,7 +1,10 @@
import { Router } from "express"
import { Customer, Order } from "../../../.."
import { Customer, Order, StorePostCustomersReq } from "../../../.."
import { PaginatedResponse } from "../../../../types/common"
import middlewares, { transformQuery } from "../../../middlewares"
import middlewares, {
transformBody,
transformQuery,
} from "../../../middlewares"
import {
defaultStoreOrdersFields,
defaultStoreOrdersRelations,
@@ -62,16 +62,18 @@ import { EntityManager } from "typeorm"
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(
const validated = (await validator(
StorePostCustomersCustomerPasswordTokenReq,
req.body
)
)) as StorePostCustomersCustomerPasswordTokenReq
const customerService: CustomerService = req.scope.resolve(
"customerService"
) as CustomerService
const customer = await customerService.retrieveByEmail(validated.email)
const customer = await customerService.retrieveRegisteredByEmail(
validated.email
)
// Will generate a token and send it to the customer via an email provider
const manager: EntityManager = req.scope.resolve("manager")
@@ -81,15 +81,18 @@ import { EntityManager } from "typeorm"
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const validated = await validator(
const validated = (await validator(
StorePostCustomersResetPasswordReq,
req.body
)
)) as StorePostCustomersResetPasswordReq
const customerService: CustomerService = req.scope.resolve("customerService")
let customer = await customerService.retrieveByEmail(validated.email, {
select: ["id", "password_hash"],
})
let customer = await customerService.retrieveRegisteredByEmail(
validated.email,
{
select: ["id", "password_hash"],
}
)
const decodedToken = jwt.verify(
validated.token,
@@ -0,0 +1,117 @@
import { IsJWT, IsNotEmpty } from "class-validator"
import { EntityManager } from "typeorm"
import {
CustomerService,
OrderService,
TokenService,
} from "../../../../services"
/**
* @oas [post] /orders/customer/confirm
* operationId: "PostOrdersCustomerOrderClaimsCustomerOrderClaimAccept"
* summary: "Verify a claim to orders"
* description: "Verifies the claim order token provided to the customer upon request of order ownership"
* requestBody:
* content:
* application/json:
* schema:
* required:
* - token
* properties:
* token:
* description: "The invite token provided by the admin."
* type: string
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.orders.confirmRequest(
* token,
* )
* .then(() => {
* // successful
* })
* .catch(() => {
* // an error occurred
* });
* - lang: Shell
* label: cURL
* source: |
* curl --location --request POST 'https://medusa-url.com/store/orders/customer/confirm' \
* --header 'Content-Type: application/json' \
* --data-raw '{
* "token": "{token}",
* }'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - Invite
* responses:
* 200:
* description: OK
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { token } = req.validatedBody
const orderSerivce: OrderService = req.scope.resolve("orderService")
const customerService: CustomerService = req.scope.resolve("customerService")
const tokenService: TokenService = req.scope.resolve(
TokenService.RESOLUTION_KEY
)
const orderService: OrderService = req.scope.resolve("orderService")
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
const { claimingCustomerId, orders: orderIds } = tokenService.verifyToken(
token,
{
maxAge: "15m",
}
) as {
claimingCustomerId: string
orders: string[]
}
const customer = await customerService
.withTransaction(transactionManager)
.retrieve(claimingCustomerId)
const orders = await orderService.list({ id: orderIds })
await Promise.all(
orders.map(async (order) => {
await orderSerivce
.withTransaction(transactionManager)
.update(order.id, {
customer_id: claimingCustomerId,
email: customer.email,
})
})
)
})
res.sendStatus(200)
}
export class StorePostCustomersCustomerAcceptClaimReq {
@IsNotEmpty()
@IsJWT()
token: string
}
@@ -1,7 +1,10 @@
import { Router } from "express"
import "reflect-metadata"
import { Order } from "../../../.."
import middlewares from "../../../middlewares"
import middlewares, { transformBody } from "../../../middlewares"
import requireCustomerAuthentication from "../../../middlewares/require-customer-authentication"
import { StorePostCustomersCustomerOrderClaimReq } from "./request-order"
import { StorePostCustomersCustomerAcceptClaimReq } from "./confirm-order-request"
const route = Router()
@@ -26,6 +29,19 @@ export default (app) => {
middlewares.wrap(require("./get-order-by-cart").default)
)
route.post(
"/customer/confirm",
transformBody(StorePostCustomersCustomerAcceptClaimReq),
middlewares.wrap(require("./confirm-order-request").default)
)
route.post(
"/batch/customer/token",
requireCustomerAuthentication(),
transformBody(StorePostCustomersCustomerOrderClaimReq),
middlewares.wrap(require("./request-order").default)
)
return app
}
@@ -111,3 +127,5 @@ export type StoreOrdersRes = {
}
export * from "./lookup-order"
export * from "./confirm-order-request"
export * from "./request-order"
@@ -0,0 +1,132 @@
import { IsNotEmpty, IsString } from "class-validator"
import { MedusaError } from "medusa-core-utils"
import {
CustomerService,
EventBusService,
OrderService,
} from "../../../../services"
import TokenService from "../../../../services/token"
import { TokenEvents } from "../../../../types/token"
/**
* @oas [post] /orders/batch/customer/token
* operationId: "PostOrdersCustomerOrderClaim"
* summary: "Claim orders for signed in account"
* description: "Sends an email to emails registered to orders provided with link to transfer order ownership"
* requestBody:
* content:
* application/json:
* schema:
* required:
* - order_ids
* properties:
* order_ids:
* description: "The ids of the orders to claim"
* type: array
* items:
* type: string
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
* source: |
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.orders.claimOrders({
* display_ids,
* })
* .then(() => {
* // successful
* })
* .catch(() => {
* // an error occurred
* });
* - lang: Shell
* label: cURL
* source: |
* curl --location --request POST 'https://medusa-url.com/store/batch/customer/token' \
* --header 'Content-Type: application/json' \
* --data-raw '{
* "display_ids": ["id"],
* }'
* security:
* - api_token: []
* - cookie_auth: []
* tags:
* - Invite
* responses:
* 200:
* description: OK
* "400":
* $ref: "#/components/responses/400_error"
* "401":
* $ref: "#/components/responses/unauthorized"
* "404":
* $ref: "#/components/responses/not_found_error"
* "409":
* $ref: "#/components/responses/invalid_state_error"
* "422":
* $ref: "#/components/responses/invalid_request_error"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
const { order_ids } = req.validatedBody
const eventBusService: EventBusService = req.scope.resolve("eventBusService")
const orderService: OrderService = req.scope.resolve("orderService")
const customerService: CustomerService = req.scope.resolve("customerService")
const tokenService: TokenService = req.scope.resolve(
TokenService.RESOLUTION_KEY
)
const customerId: string = req.user?.customer_id
const customer = await customerService.retrieve(customerId)
if (!customer.has_account) {
throw new MedusaError(
MedusaError.Types.UNAUTHORIZED,
"Customer does not have an account"
)
}
const orders = await orderService.list(
{ id: order_ids },
{ select: ["id", "email"] }
)
const emailOrderMapping: { [email: string]: string[] } = orders.reduce(
(acc, order) => {
acc[order.email] = [...(acc[order.email] || []), order.id]
return acc
},
{}
)
await Promise.all(
Object.entries(emailOrderMapping).map(async ([email, order_ids]) => {
const token = tokenService.signToken(
{
claimingCustomerId: customerId,
orders: order_ids,
},
{ expiresIn: "15m" }
)
await eventBusService.emit(TokenEvents.ORDER_UPDATE_TOKEN_CREATED, {
old_email: email,
new_customer_id: customer.id,
orders: order_ids,
token,
})
})
)
res.sendStatus(200)
}
export class StorePostCustomersCustomerOrderClaimReq {
@IsNotEmpty({ each: true })
@IsString({ each: true })
order_ids: string[]
}