feat(medusa): Claim customer orders (#2710)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/medusa": minor
|
||||
---
|
||||
|
||||
Feat: allow customers to claim orders
|
||||
|
||||
BREAKING CHANGE: `customerService.retrieveByEmail` is being deprecated in favor of two methods: `customerService.retrieveRegisteredByEmail` and `customerService.retrieveUnRegisteredByEmail`. Please use `customerService.list({ email: <customer email> })` in the future
|
||||
@@ -1,4 +1,3 @@
|
||||
const jwt = require("jsonwebtoken")
|
||||
const path = require("path")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
|
||||
@@ -27,7 +27,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/payment-collections", () => {
|
||||
const cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||
const [process, connection] = await startServerWithEnvironment({
|
||||
cwd,
|
||||
env: { MEDUSA_FF_ORDER_EDITING: true }
|
||||
env: { MEDUSA_FF_ORDER_EDITING: true },
|
||||
})
|
||||
dbConnection = connection
|
||||
medusaProcess = process
|
||||
|
||||
@@ -1799,6 +1799,56 @@ describe("/store/carts", () => {
|
||||
expect(res.data.cart.payment_authorized_at).not.toBe(null)
|
||||
expect(res.data.cart.completed_at).not.toBe(null)
|
||||
})
|
||||
|
||||
it("completes cart with a non-customer and for a customer with the same email created later the order doesn't show up", async () => {
|
||||
const api = useApi()
|
||||
const customerEmail = "test-email-for-non-existent-customer@test.com"
|
||||
const product = await simpleProductFactory(dbConnection)
|
||||
|
||||
const region = await simpleRegionFactory(dbConnection, { tax_rate: 10 })
|
||||
|
||||
const cart = await simpleCartFactory(dbConnection, {
|
||||
customer: {
|
||||
email: customerEmail,
|
||||
has_account: false,
|
||||
},
|
||||
region: region.id,
|
||||
line_items: [
|
||||
{
|
||||
variant_id: product.variants[0].id,
|
||||
quantity: 1,
|
||||
unit_price: 1000,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await api.post(`/store/carts/${cart.id}/payment-sessions`)
|
||||
|
||||
const completeRes = await api.post(`/store/carts/${cart.id}/complete`)
|
||||
|
||||
expect(completeRes.status).toEqual(200)
|
||||
|
||||
const customerResponse = await api.post("/store/customers", {
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
email: customerEmail,
|
||||
password: "test",
|
||||
})
|
||||
|
||||
const [authCookie] = customerResponse.headers["set-cookie"][0].split(";")
|
||||
|
||||
const customerOrdersResponse = await api
|
||||
.get("/store/customers/me/orders?status[]=completed", {
|
||||
headers: {
|
||||
Cookie: authCookie,
|
||||
},
|
||||
})
|
||||
.catch((err) => {
|
||||
return err.response
|
||||
})
|
||||
expect(customerOrdersResponse.status).toEqual(200)
|
||||
expect(customerOrdersResponse.data.orders.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /store/carts/:id/shipping-methods", () => {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
const jwt = require("jsonwebtoken")
|
||||
const path = require("path")
|
||||
const { Address, Customer, Order, Region } = require("@medusajs/medusa")
|
||||
|
||||
const setupServer = require("../../../helpers/setup-server")
|
||||
const { useApi } = require("../../../helpers/use-api")
|
||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||
const { simpleOrderFactory } = require("../../factories")
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -28,6 +30,78 @@ describe("/store/customers", () => {
|
||||
medusaProcess.kill()
|
||||
})
|
||||
|
||||
describe("POST /store/customers/confirm-claim", () => {
|
||||
let orderId
|
||||
beforeEach(async () => {
|
||||
const manager = dbConnection.manager
|
||||
await manager.insert(Customer, {
|
||||
id: "test_customer",
|
||||
first_name: "John",
|
||||
last_name: "Deere",
|
||||
email: "john@deere.com",
|
||||
password_hash:
|
||||
"c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test"
|
||||
has_account: true,
|
||||
})
|
||||
|
||||
await manager.insert(Customer, {
|
||||
id: "test_customer-1",
|
||||
first_name: "John",
|
||||
last_name: "Deere",
|
||||
email: "john@deere.com",
|
||||
})
|
||||
|
||||
const order = await simpleOrderFactory(dbConnection, {
|
||||
customer: {
|
||||
id: "test_customer-1",
|
||||
},
|
||||
})
|
||||
orderId = order.id
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await doAfterEach()
|
||||
})
|
||||
|
||||
it("Successfully confirms a claim ", async () => {
|
||||
const api = useApi()
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
claimingCustomerId: "test_customer",
|
||||
orders: [orderId],
|
||||
},
|
||||
"test"
|
||||
)
|
||||
|
||||
const authResponse = await api.post("/store/auth", {
|
||||
email: "john@deere.com",
|
||||
password: "test",
|
||||
})
|
||||
|
||||
const [authCookie] = authResponse.headers["set-cookie"][0].split(";")
|
||||
|
||||
const authHeader = {
|
||||
headers: {
|
||||
Cookie: authCookie,
|
||||
},
|
||||
}
|
||||
|
||||
const ordersRes1 = await api.get(`/store/customers/me/orders`, authHeader)
|
||||
|
||||
expect(ordersRes1.data.orders.length).toEqual(0)
|
||||
|
||||
const response = await api.post("/store/orders/customer/confirm", {
|
||||
token,
|
||||
})
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const ordersRes2 = await api.get(`/store/customers/me/orders`, authHeader)
|
||||
|
||||
expect(ordersRes2.data.orders.length).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("POST /store/customers", () => {
|
||||
beforeEach(async () => {
|
||||
const manager = dbConnection.manager
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import faker from "faker"
|
||||
import { Customer } from "@medusajs/medusa"
|
||||
import { Connection } from "typeorm"
|
||||
import {
|
||||
CustomerGroupFactoryData,
|
||||
simpleCustomerGroupFactory
|
||||
simpleCustomerGroupFactory,
|
||||
} from "./simple-customer-group-factory"
|
||||
|
||||
export type CustomerFactoryData = {
|
||||
@@ -28,7 +28,7 @@ export const simpleCustomerFactory = async (
|
||||
const customerId = data.id || `simple-customer-${Math.random() * 1000}`
|
||||
const c = manager.create(Customer, {
|
||||
id: customerId,
|
||||
email: data.email,
|
||||
email: data.email ?? faker.internet.email(),
|
||||
password_hash:
|
||||
data.password_hash ??
|
||||
"c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test"
|
||||
|
||||
@@ -27,6 +27,10 @@ import {
|
||||
SalesChannelFactoryData,
|
||||
simpleSalesChannelFactory,
|
||||
} from "./simple-sales-channel-factory"
|
||||
import {
|
||||
CustomerFactoryData,
|
||||
simpleCustomerFactory,
|
||||
} from "./simple-customer-factory"
|
||||
|
||||
export type OrderFactoryData = {
|
||||
id?: string
|
||||
@@ -34,6 +38,7 @@ export type OrderFactoryData = {
|
||||
fulfillment_status?: FulfillmentStatus
|
||||
region?: RegionFactoryData | string
|
||||
email?: string | null
|
||||
customer?: CustomerFactoryData | null
|
||||
currency_code?: string
|
||||
tax_rate?: number | null
|
||||
line_items?: LineItemFactoryData[]
|
||||
@@ -70,11 +75,10 @@ export const simpleOrderFactory = async (
|
||||
}
|
||||
const address = await simpleAddressFactory(connection, data.shipping_address)
|
||||
|
||||
const customerToSave = manager.create(Customer, {
|
||||
email:
|
||||
typeof data.email !== "undefined" ? data.email : faker.internet.email(),
|
||||
const customer = await simpleCustomerFactory(connection, {
|
||||
...data.customer,
|
||||
email: data.email ?? undefined,
|
||||
})
|
||||
const customer = await manager.save(customerToSave)
|
||||
|
||||
let discounts = []
|
||||
if (typeof data.discounts !== "undefined") {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { StoreGetOrdersParams, StoreOrdersRes } from "@medusajs/medusa"
|
||||
import {
|
||||
StoreGetOrdersParams,
|
||||
StoreOrdersRes,
|
||||
StorePostCustomersCustomerAcceptClaimReq,
|
||||
StorePostCustomersCustomerOrderClaimReq,
|
||||
} from "@medusajs/medusa"
|
||||
import qs from "qs"
|
||||
import { ResponsePromise } from "../typings"
|
||||
import BaseResource from "./base"
|
||||
@@ -49,6 +54,34 @@ class OrdersResource extends BaseResource {
|
||||
|
||||
return this.client.request("GET", path, payload, {}, customHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Request access to a list of orders
|
||||
* @param {string[]} payload display ids of orders to request
|
||||
* @param customHeaders
|
||||
* @return {ResponsePromise}
|
||||
*/
|
||||
requestCustomerOrders(
|
||||
payload: StorePostCustomersCustomerOrderClaimReq,
|
||||
customHeaders: Record<string, any> = {}
|
||||
): ResponsePromise {
|
||||
const path = `/store/orders/batch/customer/token`
|
||||
return this.client.request("POST", path, payload, {}, customHeaders)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Grant access to a list of orders
|
||||
* @param {string} payload signed token to grant access
|
||||
* @param customHeaders
|
||||
* @return {ResponsePromise}
|
||||
*/
|
||||
confirmRequest(
|
||||
payload: StorePostCustomersCustomerAcceptClaimReq,
|
||||
customHeaders: Record<string, any> = {}
|
||||
): ResponsePromise {
|
||||
const path = `/store/orders/customer/confirm`
|
||||
return this.client.request("POST", path, payload, {}, customHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
export default OrdersResource
|
||||
|
||||
@@ -121,6 +121,14 @@ export const storeHandlers = [
|
||||
)
|
||||
}),
|
||||
|
||||
rest.post("/store/orders/customer/confirm", (req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
}),
|
||||
|
||||
rest.post("/store/orders/batch/customer/token", (req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
}),
|
||||
|
||||
rest.get("/store/return-reasons/", (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.status(200),
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./queries"
|
||||
export * from "./mutations"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
|
||||
import { Response } from "@medusajs/medusa-js"
|
||||
|
||||
import {
|
||||
StorePostCustomersCustomerAcceptClaimReq,
|
||||
StorePostCustomersCustomerOrderClaimReq,
|
||||
} from "@medusajs/medusa"
|
||||
|
||||
import { buildOptions } from "../../utils/buildOptions"
|
||||
import { useMedusa } from "../../../contexts"
|
||||
import { orderKeys } from "."
|
||||
|
||||
export const useRequestOrderAccess = (
|
||||
options?: UseMutationOptions<
|
||||
Response<{}>,
|
||||
Error,
|
||||
StorePostCustomersCustomerOrderClaimReq
|
||||
>
|
||||
) => {
|
||||
const { client } = useMedusa()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation(
|
||||
(payload: StorePostCustomersCustomerOrderClaimReq) =>
|
||||
client.orders.requestCustomerOrders(payload),
|
||||
buildOptions(queryClient, [orderKeys.all], options)
|
||||
)
|
||||
}
|
||||
export const useGrantOrderAccess = (
|
||||
options?: UseMutationOptions<
|
||||
Response<{}>,
|
||||
Error,
|
||||
StorePostCustomersCustomerAcceptClaimReq
|
||||
>
|
||||
) => {
|
||||
const { client } = useMedusa()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation(
|
||||
(payload: StorePostCustomersCustomerAcceptClaimReq) =>
|
||||
client.orders.confirmRequest(payload),
|
||||
buildOptions(queryClient, [orderKeys.all], options)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useRequestOrderAccess, useGrantOrderAccess } from "../../../../src/"
|
||||
import { renderHook } from "@testing-library/react-hooks"
|
||||
import { createWrapper } from "../../../utils"
|
||||
|
||||
describe("useGrantOrderAccess hook", () => {
|
||||
test("Grant access to token", async () => {
|
||||
const { result, waitFor } = renderHook(() => useGrantOrderAccess(), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
result.current.mutate({ token: "store_order_edit" })
|
||||
|
||||
await waitFor(() => result.current.isSuccess)
|
||||
|
||||
expect(result.current.data.response.status).toEqual(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe("useRequestOrderAccess hook", () => {
|
||||
test("Requests access to ids", async () => {
|
||||
const { result, waitFor } = renderHook(() => useRequestOrderAccess(), {
|
||||
wrapper: createWrapper(),
|
||||
})
|
||||
|
||||
result.current.mutate({ order_ids: [""] })
|
||||
|
||||
await waitFor(() => result.current.isSuccess)
|
||||
|
||||
expect(result.current.data.response.status).toEqual(200)
|
||||
})
|
||||
})
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class updateCustomerEmailConstraint_1669032280562
|
||||
implements MigrationInterface
|
||||
{
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_fdb2f3ad8115da4c7718109a6e"`
|
||||
)
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customer" ADD CONSTRAINT "UQ_unique_email_for_guests_and_customer_accounts" UNIQUE ("email", "has_account")`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "customer" DROP CONSTRAINT "UQ_unique_email_for_guests_and_customer_accounts"`
|
||||
)
|
||||
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_fdb2f3ad8115da4c7718109a6e" ON "customer" ("email") `
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ManyToMany,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
Unique,
|
||||
} from "typeorm"
|
||||
|
||||
import { Address } from "./address"
|
||||
@@ -18,8 +19,9 @@ import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
|
||||
@Entity()
|
||||
@Unique(["email", "has_account"])
|
||||
export class Customer extends SoftDeletableEntity {
|
||||
@Index({ unique: true })
|
||||
@Index()
|
||||
@Column()
|
||||
email: string
|
||||
|
||||
|
||||
@@ -46,12 +46,23 @@ export const CustomerServiceMock = {
|
||||
password_hash: "1234",
|
||||
})
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
retrieveRegisteredByEmail: jest.fn().mockImplementation((email) => {
|
||||
if (email === "oliver@test.dk") {
|
||||
return Scrypt.kdf("123456789", { logN: 1, r: 1, p: 1 }).then((hash) => ({
|
||||
email,
|
||||
password_hash: hash.toString("base64"),
|
||||
has_account: true,
|
||||
}))
|
||||
}
|
||||
if (email === "lebron@james1.com") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("lebron"),
|
||||
email,
|
||||
password_hash: "1234",
|
||||
})
|
||||
}
|
||||
return Promise.resolve(undefined)
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ describe("AuthService", () => {
|
||||
const authService = new AuthService({
|
||||
manager: managerMock,
|
||||
userService: UserServiceMock,
|
||||
customerService: CustomerServiceMock
|
||||
customerService: CustomerServiceMock,
|
||||
})
|
||||
|
||||
describe("authenticate", () => {
|
||||
|
||||
@@ -167,10 +167,18 @@ describe("CartService", () => {
|
||||
})
|
||||
const cartRepository = MockRepository()
|
||||
const customerService = {
|
||||
retrieveByEmail: jest.fn().mockReturnValue(
|
||||
retrieveUnregisteredByEmail: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("customer"),
|
||||
email: "email@test.com",
|
||||
has_account: false,
|
||||
})
|
||||
),
|
||||
retrieveRegisteredByEmail: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("customer"),
|
||||
email: "email@test.com",
|
||||
has_account: true,
|
||||
})
|
||||
),
|
||||
withTransaction: function () {
|
||||
@@ -970,12 +978,13 @@ describe("CartService", () => {
|
||||
|
||||
describe("updateEmail", () => {
|
||||
const customerService = {
|
||||
retrieveByEmail: jest.fn().mockImplementation((email) => {
|
||||
retrieveUnregisteredByEmail: jest.fn().mockImplementation((email) => {
|
||||
if (email === "no@mail.com") {
|
||||
return Promise.reject()
|
||||
}
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("existing"),
|
||||
has_account: false,
|
||||
email,
|
||||
})
|
||||
}),
|
||||
@@ -1025,6 +1034,7 @@ describe("CartService", () => {
|
||||
customer: {
|
||||
id: IdMap.getId("existing"),
|
||||
email: "test@testdom.com",
|
||||
has_account: false,
|
||||
},
|
||||
email: "test@testdom.com",
|
||||
})
|
||||
|
||||
@@ -64,6 +64,32 @@ describe("CustomerService", () => {
|
||||
|
||||
expect(result.id).toEqual(IdMap.getId("ironman"))
|
||||
})
|
||||
|
||||
it("successfully retrieves a guest customer by email", async () => {
|
||||
const result = await customerService.retrieveUnregisteredByEmail(
|
||||
"tony@stark.com"
|
||||
)
|
||||
|
||||
expect(customerRepository.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(customerRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { email: "tony@stark.com", has_account: false },
|
||||
})
|
||||
|
||||
expect(result.id).toEqual(IdMap.getId("ironman"))
|
||||
})
|
||||
|
||||
it("successfully retrieves a registered customer by email", async () => {
|
||||
const result = await customerService.retrieveRegisteredByEmail(
|
||||
"tony@stark.com"
|
||||
)
|
||||
|
||||
expect(customerRepository.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(customerRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { email: "tony@stark.com", has_account: true },
|
||||
})
|
||||
|
||||
expect(result.id).toEqual(IdMap.getId("ironman"))
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieveByPhone", () => {
|
||||
@@ -92,57 +118,118 @@ describe("CustomerService", () => {
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
const customerRepository = MockRepository({
|
||||
findOne: (query) => {
|
||||
const customerRepository = {
|
||||
...MockRepository({
|
||||
findOne: (query) => {
|
||||
if (query.where.email === "tony@stark.com") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("exists"),
|
||||
has_account: true,
|
||||
password_hash: "test",
|
||||
})
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
}),
|
||||
listAndCount: jest.fn().mockImplementation((query, q) => {
|
||||
if (query.where.email === "tony@stark.com") {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("exists"),
|
||||
password_hash: "test",
|
||||
})
|
||||
return Promise.resolve([
|
||||
[
|
||||
{
|
||||
id: IdMap.getId("exists"),
|
||||
has_account: true,
|
||||
password_hash: "test",
|
||||
},
|
||||
],
|
||||
0,
|
||||
])
|
||||
}
|
||||
return Promise.resolve({ id: IdMap.getId("ironman") })
|
||||
},
|
||||
})
|
||||
return Promise.resolve([[], 0])
|
||||
}),
|
||||
}
|
||||
|
||||
const configModule = {
|
||||
projectConfig: {
|
||||
jwt_secret: "test",
|
||||
},
|
||||
}
|
||||
const customerService = new CustomerService({
|
||||
manager: MockManager,
|
||||
customerRepository,
|
||||
eventBusService,
|
||||
configModule,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully create a customer", async () => {
|
||||
it("successfully creates a customer with password", async () => {
|
||||
await customerService.create({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
email: "john@doe.com",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
password: "test",
|
||||
})
|
||||
|
||||
expect(customerRepository.create).toBeCalledTimes(1)
|
||||
expect(customerRepository.create).toBeCalledWith({
|
||||
email: "oliver@medusa.com",
|
||||
first_name: "Oliver",
|
||||
last_name: "Juhl",
|
||||
email: "john@doe.com",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
has_account: true,
|
||||
password_hash: expect.anything(),
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully updates an existing customer on create", async () => {
|
||||
it("calls listAndCount with email", async () => {
|
||||
await customerService.create({
|
||||
email: "tony@stark.com",
|
||||
password: "stark123",
|
||||
has_account: false,
|
||||
email: "john@doe.com",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
password: "test",
|
||||
})
|
||||
|
||||
expect(customerRepository.save).toBeCalledTimes(1)
|
||||
expect(customerRepository.save).toBeCalledWith({
|
||||
id: IdMap.getId("exists"),
|
||||
email: "tony@stark.com",
|
||||
password_hash: expect.anything(),
|
||||
has_account: true,
|
||||
expect(customerRepository.listAndCount).toHaveBeenCalledWith(
|
||||
{
|
||||
relations: [],
|
||||
skip: 0,
|
||||
take: 2,
|
||||
where: {
|
||||
email: "john@doe.com",
|
||||
},
|
||||
},
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
it("successfully creates a one time customer", async () => {
|
||||
await customerService.create({
|
||||
email: "john@doe.com",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
})
|
||||
|
||||
expect(customerRepository.create).toBeCalledTimes(1)
|
||||
expect(customerRepository.create).toBeCalledWith({
|
||||
email: "john@doe.com",
|
||||
first_name: "John",
|
||||
last_name: "Doe",
|
||||
})
|
||||
})
|
||||
|
||||
it("Fails to create a customer with an existing account", async () => {
|
||||
expect.assertions(1)
|
||||
await customerService
|
||||
.create({
|
||||
email: "tony@stark.com",
|
||||
password: "stark123",
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.message).toEqual(
|
||||
"A customer with the given email already has an account. Log in instead"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -149,21 +149,21 @@ class AuthService extends TransactionBaseService {
|
||||
): Promise<AuthenticateResult> {
|
||||
return await this.atomicPhase_(async (transactionManager) => {
|
||||
try {
|
||||
const customerPasswordHash: Customer = await this.customerService_
|
||||
const customer: Customer = await this.customerService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieveByEmail(email, {
|
||||
select: ["password_hash"],
|
||||
.retrieveRegisteredByEmail(email, {
|
||||
select: ["id", "password_hash"],
|
||||
})
|
||||
if (customerPasswordHash.password_hash) {
|
||||
if (customer.password_hash) {
|
||||
const passwordsMatch = await this.comparePassword_(
|
||||
password,
|
||||
customerPasswordHash.password_hash
|
||||
customer.password_hash
|
||||
)
|
||||
|
||||
if (passwordsMatch) {
|
||||
const customer = await this.customerService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieveByEmail(email)
|
||||
.retrieveRegisteredByEmail(email)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
@@ -325,8 +325,20 @@ class CartService extends TransactionBaseService {
|
||||
).id
|
||||
}
|
||||
|
||||
if (data.email) {
|
||||
const customer = await this.createOrFetchUserFromEmail_(data.email)
|
||||
if (data.customer_id) {
|
||||
const customer = await this.customerService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(data.customer_id)
|
||||
.catch(() => undefined)
|
||||
rawCart.customer = customer
|
||||
rawCart.customer_id = customer?.id
|
||||
rawCart.email = customer?.email
|
||||
}
|
||||
|
||||
if (!rawCart.email && data.email) {
|
||||
const customer = await this.createOrFetchGuestCustomerFromEmail_(
|
||||
data.email
|
||||
)
|
||||
rawCart.customer = customer
|
||||
rawCart.customer_id = customer.id
|
||||
rawCart.email = customer.email
|
||||
@@ -992,7 +1004,9 @@ class CartService extends TransactionBaseService {
|
||||
if (data.customer_id) {
|
||||
await this.updateCustomerId_(cart, data.customer_id)
|
||||
} else if (isDefined(data.email)) {
|
||||
const customer = await this.createOrFetchUserFromEmail_(data.email)
|
||||
const customer = await this.createOrFetchGuestCustomerFromEmail_(
|
||||
data.email
|
||||
)
|
||||
cart.customer = customer
|
||||
cart.customer_id = customer.id
|
||||
cart.email = customer.email
|
||||
@@ -1181,14 +1195,14 @@ class CartService extends TransactionBaseService {
|
||||
* @param email - the email to use
|
||||
* @return the resultign customer object
|
||||
*/
|
||||
protected async createOrFetchUserFromEmail_(
|
||||
protected async createOrFetchGuestCustomerFromEmail_(
|
||||
email: string
|
||||
): Promise<Customer> {
|
||||
const validatedEmail = validateEmail(email)
|
||||
|
||||
let customer = await this.customerService_
|
||||
.withTransaction(this.transactionManager_)
|
||||
.retrieveByEmail(validatedEmail)
|
||||
.retrieveUnregisteredByEmail(validatedEmail)
|
||||
.catch(() => undefined)
|
||||
|
||||
if (!customer) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import Scrypt from "scrypt-kdf"
|
||||
import { DeepPartial, EntityManager } from "typeorm"
|
||||
import { EventBusService } from "."
|
||||
import { StorePostCustomersCustomerAddressesAddressReq } from "../api"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { Address, Customer, CustomerGroup } from "../models"
|
||||
@@ -10,7 +11,6 @@ import { CustomerRepository } from "../repositories/customer"
|
||||
import { AddressCreatePayload, FindConfig, Selector } from "../types/common"
|
||||
import { CreateCustomerInput, UpdateCustomerInput } from "../types/customers"
|
||||
import { buildQuery, isDefined, setMetadata } from "../utils"
|
||||
import EventBusService from "./event-bus"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -186,10 +186,11 @@ class CustomerService extends TransactionBaseService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a customer by email.
|
||||
* Gets a registered customer by email.
|
||||
* @param {string} email - the email of the customer to get.
|
||||
* @param {Object} config - the config object containing query settings
|
||||
* @return {Promise<Customer>} the customer document.
|
||||
* @deprecated
|
||||
*/
|
||||
async retrieveByEmail(
|
||||
email: string,
|
||||
@@ -198,6 +199,31 @@ class CustomerService extends TransactionBaseService {
|
||||
return await this.retrieve_({ email: email.toLowerCase() }, config)
|
||||
}
|
||||
|
||||
async retrieveUnregisteredByEmail(
|
||||
email: string,
|
||||
config: FindConfig<Customer> = {}
|
||||
): Promise<Customer | never> {
|
||||
return await this.retrieve_(
|
||||
{ email: email.toLowerCase(), has_account: false },
|
||||
config
|
||||
)
|
||||
}
|
||||
async retrieveRegisteredByEmail(
|
||||
email: string,
|
||||
config: FindConfig<Customer> = {}
|
||||
): Promise<Customer | never> {
|
||||
return await this.retrieve_(
|
||||
{ email: email.toLowerCase(), has_account: true },
|
||||
config
|
||||
)
|
||||
}
|
||||
|
||||
async listByEmail(
|
||||
email: string,
|
||||
config: FindConfig<Customer> = { relations: [], skip: 0, take: 2 }
|
||||
): Promise<Customer[]> {
|
||||
return await this.list({ email: email.toLowerCase() }, config)
|
||||
}
|
||||
/**
|
||||
* Gets a customer by phone.
|
||||
* @param {string} phone - the phone of the customer to get.
|
||||
@@ -249,44 +275,45 @@ class CustomerService extends TransactionBaseService {
|
||||
)
|
||||
|
||||
customer.email = customer.email.toLowerCase()
|
||||
|
||||
const { email, password } = customer
|
||||
|
||||
const existing = await this.retrieveByEmail(email).catch(() => undefined)
|
||||
// should be a list of customers at this point
|
||||
const existing = await this.listByEmail(email).catch(() => undefined)
|
||||
|
||||
if (existing && existing.has_account) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
"A customer with the given email already has an account. Log in instead"
|
||||
)
|
||||
// should validate that "existing.some(acc => acc.has_account) && password"
|
||||
if (existing) {
|
||||
if (existing.some((customer) => customer.has_account) && password) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
"A customer with the given email already has an account. Log in instead"
|
||||
)
|
||||
} else if (
|
||||
existing?.some((customer) => !customer.has_account) &&
|
||||
!password
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.DUPLICATE_ERROR,
|
||||
"Guest customer with email already exists"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (existing && password && !existing.has_account) {
|
||||
if (password) {
|
||||
const hashedPassword = await this.hashPassword_(password)
|
||||
customer.password_hash = hashedPassword
|
||||
customer.has_account = true
|
||||
delete customer.password
|
||||
|
||||
const toUpdate = { ...existing, ...customer }
|
||||
const updated = await customerRepository.save(toUpdate)
|
||||
await this.eventBusService_
|
||||
.withTransaction(manager)
|
||||
.emit(CustomerService.Events.UPDATED, updated)
|
||||
return updated
|
||||
} else {
|
||||
if (password) {
|
||||
const hashedPassword = await this.hashPassword_(password)
|
||||
customer.password_hash = hashedPassword
|
||||
customer.has_account = true
|
||||
delete customer.password
|
||||
}
|
||||
|
||||
const created = customerRepository.create(customer)
|
||||
const result = await customerRepository.save(created)
|
||||
await this.eventBusService_
|
||||
.withTransaction(manager)
|
||||
.emit(CustomerService.Events.CREATED, result)
|
||||
return result
|
||||
}
|
||||
|
||||
const created = customerRepository.create(customer)
|
||||
const result = await customerRepository.save(created)
|
||||
|
||||
await this.eventBusService_
|
||||
.withTransaction(manager)
|
||||
.emit(CustomerService.Events.CREATED, result)
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export { default as SwapService } from "./swap"
|
||||
export { default as SystemPaymentProviderService } from "./system-payment-provider"
|
||||
export { default as TaxProviderService } from "./tax-provider"
|
||||
export { default as TaxRateService } from "./tax-rate"
|
||||
export { default as TokenService } from "./token"
|
||||
export { default as TotalsService } from "./totals"
|
||||
export { default as NewTotalsService } from "./new-totals"
|
||||
export { default as UserService } from "./user"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { Brackets, EntityManager } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
@@ -45,6 +46,8 @@ import ShippingOptionService from "./shipping-option"
|
||||
import ShippingProfileService from "./shipping-profile"
|
||||
import TotalsService from "./totals"
|
||||
import { NewTotalsService, TaxProviderService } from "./index"
|
||||
import { ConfigModule } from "../types/global"
|
||||
import logger from "../loaders/logger"
|
||||
|
||||
export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists"
|
||||
|
||||
@@ -94,6 +97,7 @@ class OrderService extends TransactionBaseService {
|
||||
UPDATED: "order.updated",
|
||||
CANCELED: "order.canceled",
|
||||
COMPLETED: "order.completed",
|
||||
ORDERS_CLAIMED: "order.orders_claimed",
|
||||
}
|
||||
|
||||
protected manager_: EntityManager
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import jwt, { Jwt, JwtPayload, SignOptions, VerifyOptions } from "jsonwebtoken"
|
||||
import { ConfigModule } from "../types/global"
|
||||
import formatRegistrationName from "../utils/format-registration-name"
|
||||
import { resolve } from "path"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
configModule: ConfigModule
|
||||
}
|
||||
|
||||
class TokenService {
|
||||
static RESOLUTION_KEY = formatRegistrationName(resolve(__dirname, __filename))
|
||||
|
||||
protected readonly configModule_: ConfigModule
|
||||
|
||||
constructor({ configModule }: InjectedDependencies) {
|
||||
this.configModule_ = configModule
|
||||
}
|
||||
|
||||
verifyToken(
|
||||
token: string,
|
||||
options?: VerifyOptions
|
||||
): Jwt | JwtPayload | string {
|
||||
const { jwt_secret } = this.configModule_.projectConfig
|
||||
if (jwt_secret) {
|
||||
return jwt.verify(token, jwt_secret, options)
|
||||
}
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
"Please configure jwt_secret"
|
||||
)
|
||||
}
|
||||
|
||||
signToken(data: string | Buffer | object, options?: SignOptions): string {
|
||||
const { jwt_secret } = this.configModule_.projectConfig
|
||||
if (jwt_secret) {
|
||||
return jwt.sign(data, jwt_secret, options)
|
||||
} else {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Please configure a jwt token"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TokenService
|
||||
@@ -0,0 +1,3 @@
|
||||
export enum TokenEvents {
|
||||
ORDER_UPDATE_TOKEN_CREATED = "order-update-token.created",
|
||||
}
|
||||
Reference in New Issue
Block a user