feat(medusa): Claim customer orders (#2710)
This commit is contained in:
1 parent
86f9455d00
commit
a6243618fe
36 files changed
+872
-107
No files matched your search
@@ -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 path = require("path")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
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 cwd = path.resolve(path.join(__dirname, "..", ".."))
|
||||||
const [process, connection] = await startServerWithEnvironment({
|
const [process, connection] = await startServerWithEnvironment({
|
||||||
cwd,
|
cwd,
|
||||||
env: { MEDUSA_FF_ORDER_EDITING: true }
|
env: { MEDUSA_FF_ORDER_EDITING: true },
|
||||||
})
|
})
|
||||||
dbConnection = connection
|
dbConnection = connection
|
||||||
medusaProcess = process
|
medusaProcess = process
|
||||||
|
|||||||
@@ -1799,6 +1799,56 @@ describe("/store/carts", () => {
|
|||||||
expect(res.data.cart.payment_authorized_at).not.toBe(null)
|
expect(res.data.cart.payment_authorized_at).not.toBe(null)
|
||||||
expect(res.data.cart.completed_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", () => {
|
describe("POST /store/carts/:id/shipping-methods", () => {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
const jwt = require("jsonwebtoken")
|
||||||
const path = require("path")
|
const path = require("path")
|
||||||
const { Address, Customer, Order, Region } = require("@medusajs/medusa")
|
const { Address, Customer, Order, Region } = require("@medusajs/medusa")
|
||||||
|
|
||||||
const setupServer = require("../../../helpers/setup-server")
|
const setupServer = require("../../../helpers/setup-server")
|
||||||
const { useApi } = require("../../../helpers/use-api")
|
const { useApi } = require("../../../helpers/use-api")
|
||||||
const { initDb, useDb } = require("../../../helpers/use-db")
|
const { initDb, useDb } = require("../../../helpers/use-db")
|
||||||
|
const { simpleOrderFactory } = require("../../factories")
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -28,6 +30,78 @@ describe("/store/customers", () => {
|
|||||||
medusaProcess.kill()
|
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", () => {
|
describe("POST /store/customers", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const manager = dbConnection.manager
|
const manager = dbConnection.manager
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Customer } from "@medusajs/medusa"
|
|
||||||
import faker from "faker"
|
import faker from "faker"
|
||||||
|
import { Customer } from "@medusajs/medusa"
|
||||||
import { Connection } from "typeorm"
|
import { Connection } from "typeorm"
|
||||||
import {
|
import {
|
||||||
CustomerGroupFactoryData,
|
CustomerGroupFactoryData,
|
||||||
simpleCustomerGroupFactory
|
simpleCustomerGroupFactory,
|
||||||
} from "./simple-customer-group-factory"
|
} from "./simple-customer-group-factory"
|
||||||
|
|
||||||
export type CustomerFactoryData = {
|
export type CustomerFactoryData = {
|
||||||
@@ -28,7 +28,7 @@ export const simpleCustomerFactory = async (
|
|||||||
const customerId = data.id || `simple-customer-${Math.random() * 1000}`
|
const customerId = data.id || `simple-customer-${Math.random() * 1000}`
|
||||||
const c = manager.create(Customer, {
|
const c = manager.create(Customer, {
|
||||||
id: customerId,
|
id: customerId,
|
||||||
email: data.email,
|
email: data.email ?? faker.internet.email(),
|
||||||
password_hash:
|
password_hash:
|
||||||
data.password_hash ??
|
data.password_hash ??
|
||||||
"c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test"
|
"c2NyeXB0AAEAAAABAAAAAVMdaddoGjwU1TafDLLlBKnOTQga7P2dbrfgf3fB+rCD/cJOMuGzAvRdKutbYkVpuJWTU39P7OpuWNkUVoEETOVLMJafbI8qs8Qx/7jMQXkN", // password matching "test"
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import {
|
|||||||
SalesChannelFactoryData,
|
SalesChannelFactoryData,
|
||||||
simpleSalesChannelFactory,
|
simpleSalesChannelFactory,
|
||||||
} from "./simple-sales-channel-factory"
|
} from "./simple-sales-channel-factory"
|
||||||
|
import {
|
||||||
|
CustomerFactoryData,
|
||||||
|
simpleCustomerFactory,
|
||||||
|
} from "./simple-customer-factory"
|
||||||
|
|
||||||
export type OrderFactoryData = {
|
export type OrderFactoryData = {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -34,6 +38,7 @@ export type OrderFactoryData = {
|
|||||||
fulfillment_status?: FulfillmentStatus
|
fulfillment_status?: FulfillmentStatus
|
||||||
region?: RegionFactoryData | string
|
region?: RegionFactoryData | string
|
||||||
email?: string | null
|
email?: string | null
|
||||||
|
customer?: CustomerFactoryData | null
|
||||||
currency_code?: string
|
currency_code?: string
|
||||||
tax_rate?: number | null
|
tax_rate?: number | null
|
||||||
line_items?: LineItemFactoryData[]
|
line_items?: LineItemFactoryData[]
|
||||||
@@ -70,11 +75,10 @@ export const simpleOrderFactory = async (
|
|||||||
}
|
}
|
||||||
const address = await simpleAddressFactory(connection, data.shipping_address)
|
const address = await simpleAddressFactory(connection, data.shipping_address)
|
||||||
|
|
||||||
const customerToSave = manager.create(Customer, {
|
const customer = await simpleCustomerFactory(connection, {
|
||||||
email:
|
...data.customer,
|
||||||
typeof data.email !== "undefined" ? data.email : faker.internet.email(),
|
email: data.email ?? undefined,
|
||||||
})
|
})
|
||||||
const customer = await manager.save(customerToSave)
|
|
||||||
|
|
||||||
let discounts = []
|
let discounts = []
|
||||||
if (typeof data.discounts !== "undefined") {
|
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 qs from "qs"
|
||||||
import { ResponsePromise } from "../typings"
|
import { ResponsePromise } from "../typings"
|
||||||
import BaseResource from "./base"
|
import BaseResource from "./base"
|
||||||
@@ -49,6 +54,34 @@ class OrdersResource extends BaseResource {
|
|||||||
|
|
||||||
return this.client.request("GET", path, payload, {}, customHeaders)
|
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
|
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) => {
|
rest.get("/store/return-reasons/", (req, res, ctx) => {
|
||||||
return res(
|
return res(
|
||||||
ctx.status(200),
|
ctx.status(200),
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from "./queries"
|
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 {
|
try {
|
||||||
const customerService: CustomerService =
|
const customerService: CustomerService =
|
||||||
req.scope.resolve("customerService")
|
req.scope.resolve("customerService")
|
||||||
const customer = await customerService.retrieveByEmail(email, {
|
const customer = await customerService.retrieveRegisteredByEmail(email, {
|
||||||
select: ["has_account"],
|
select: ["id", "has_account"],
|
||||||
})
|
})
|
||||||
res.status(200).json({ exists: customer.has_account })
|
res.status(200).json({ exists: customer.has_account })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ export default async (req, res) => {
|
|||||||
const cartService: CartService = req.scope.resolve("cartService")
|
const cartService: CartService = req.scope.resolve("cartService")
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
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 manager.transaction(async (transactionManager) => {
|
||||||
await cartService.withTransaction(transactionManager).update(id, validated)
|
await cartService.withTransaction(transactionManager).update(id, validated)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ describe("POST /store/customers/password-token", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
subject = await request("POST", `/store/customers/password-token`, {
|
subject = await request("POST", `/store/customers/password-token`, {
|
||||||
payload: {
|
payload: {
|
||||||
email: "lebron@james.com",
|
email: "lebron@james1.com",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -18,10 +18,12 @@ describe("POST /store/customers/password-token", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("calls CustomerService retrieve", () => {
|
it("calls CustomerService retrieve", () => {
|
||||||
expect(CustomerServiceMock.retrieveByEmail).toHaveBeenCalledTimes(1)
|
expect(
|
||||||
expect(CustomerServiceMock.retrieveByEmail).toHaveBeenCalledWith(
|
CustomerServiceMock.retrieveRegisteredByEmail
|
||||||
"lebron@james.com"
|
).toHaveBeenCalledTimes(1)
|
||||||
)
|
expect(
|
||||||
|
CustomerServiceMock.retrieveRegisteredByEmail
|
||||||
|
).toHaveBeenCalledWith("lebron@james1.com")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calls CustomerService retrieve", () => {
|
it("calls CustomerService retrieve", () => {
|
||||||
@@ -33,7 +35,7 @@ describe("POST /store/customers/password-token", () => {
|
|||||||
).toHaveBeenCalledWith(IdMap.getId("lebron"))
|
).toHaveBeenCalledWith(IdMap.getId("lebron"))
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns customer decorated", () => {
|
it("returns success", () => {
|
||||||
expect(subject.status).toEqual(204)
|
expect(subject.status).toEqual(204)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("POST /store/customers/password-reset", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
subject = await request("POST", `/store/customers/password-reset`, {
|
subject = await request("POST", `/store/customers/password-reset`, {
|
||||||
payload: {
|
payload: {
|
||||||
email: "lebron@james.com",
|
email: "lebron@james1.com",
|
||||||
token: jwt.sign({ customer_id: IdMap.getId("lebron") }, "1234"),
|
token: jwt.sign({ customer_id: IdMap.getId("lebron") }, "1234"),
|
||||||
password: "TheGame",
|
password: "TheGame",
|
||||||
},
|
},
|
||||||
@@ -47,7 +47,7 @@ describe("POST /store/customers/password-reset", () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
subject = await request("POST", `/store/customers/password-reset`, {
|
subject = await request("POST", `/store/customers/password-reset`, {
|
||||||
payload: {
|
payload: {
|
||||||
email: "lebron@james.com",
|
email: "lebron@james1.com",
|
||||||
token: jwt.sign({ customer_id: IdMap.getId("not-lebron") }, "1234"),
|
token: jwt.sign({ customer_id: IdMap.getId("not-lebron") }, "1234"),
|
||||||
password: "TheGame",
|
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
|
// Add JWT to cookie
|
||||||
const {
|
const {
|
||||||
projectConfig: { jwt_secret },
|
projectConfig: { jwt_secret },
|
||||||
@@ -128,19 +133,16 @@ export default async (req, res) => {
|
|||||||
expiresIn: "30d",
|
expiresIn: "30d",
|
||||||
})
|
})
|
||||||
|
|
||||||
customer = await customerService.retrieve(customer.id, {
|
|
||||||
relations: defaultStoreCustomersRelations,
|
|
||||||
select: defaultStoreCustomersFields,
|
|
||||||
})
|
|
||||||
|
|
||||||
res.status(200).json({ customer })
|
res.status(200).json({ customer })
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorePostCustomersReq {
|
export class StorePostCustomersReq {
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
first_name: string
|
first_name: string
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
last_name: string
|
last_name: string
|
||||||
|
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Router } from "express"
|
import { Router } from "express"
|
||||||
import { Customer, Order } from "../../../.."
|
import { Customer, Order, StorePostCustomersReq } from "../../../.."
|
||||||
import { PaginatedResponse } from "../../../../types/common"
|
import { PaginatedResponse } from "../../../../types/common"
|
||||||
import middlewares, { transformQuery } from "../../../middlewares"
|
import middlewares, {
|
||||||
|
transformBody,
|
||||||
|
transformQuery,
|
||||||
|
} from "../../../middlewares"
|
||||||
import {
|
import {
|
||||||
defaultStoreOrdersFields,
|
defaultStoreOrdersFields,
|
||||||
defaultStoreOrdersRelations,
|
defaultStoreOrdersRelations,
|
||||||
|
|||||||
@@ -62,16 +62,18 @@ import { EntityManager } from "typeorm"
|
|||||||
* $ref: "#/components/responses/500_error"
|
* $ref: "#/components/responses/500_error"
|
||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const validated = await validator(
|
const validated = (await validator(
|
||||||
StorePostCustomersCustomerPasswordTokenReq,
|
StorePostCustomersCustomerPasswordTokenReq,
|
||||||
req.body
|
req.body
|
||||||
)
|
)) as StorePostCustomersCustomerPasswordTokenReq
|
||||||
|
|
||||||
const customerService: CustomerService = req.scope.resolve(
|
const customerService: CustomerService = req.scope.resolve(
|
||||||
"customerService"
|
"customerService"
|
||||||
) as 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
|
// Will generate a token and send it to the customer via an email provider
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
|
|||||||
@@ -81,15 +81,18 @@ import { EntityManager } from "typeorm"
|
|||||||
* $ref: "#/components/responses/500_error"
|
* $ref: "#/components/responses/500_error"
|
||||||
*/
|
*/
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const validated = await validator(
|
const validated = (await validator(
|
||||||
StorePostCustomersResetPasswordReq,
|
StorePostCustomersResetPasswordReq,
|
||||||
req.body
|
req.body
|
||||||
)
|
)) as StorePostCustomersResetPasswordReq
|
||||||
|
|
||||||
const customerService: CustomerService = req.scope.resolve("customerService")
|
const customerService: CustomerService = req.scope.resolve("customerService")
|
||||||
let customer = await customerService.retrieveByEmail(validated.email, {
|
let customer = await customerService.retrieveRegisteredByEmail(
|
||||||
select: ["id", "password_hash"],
|
validated.email,
|
||||||
})
|
{
|
||||||
|
select: ["id", "password_hash"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const decodedToken = jwt.verify(
|
const decodedToken = jwt.verify(
|
||||||
validated.token,
|
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 { Router } from "express"
|
||||||
import "reflect-metadata"
|
import "reflect-metadata"
|
||||||
import { Order } from "../../../.."
|
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()
|
const route = Router()
|
||||||
|
|
||||||
@@ -26,6 +29,19 @@ export default (app) => {
|
|||||||
middlewares.wrap(require("./get-order-by-cart").default)
|
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
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,3 +127,5 @@ export type StoreOrdersRes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export * from "./lookup-order"
|
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,
|
ManyToMany,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
|
Unique,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
|
|
||||||
import { Address } from "./address"
|
import { Address } from "./address"
|
||||||
@@ -18,8 +19,9 @@ import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity"
|
|||||||
import { generateEntityId } from "../utils/generate-entity-id"
|
import { generateEntityId } from "../utils/generate-entity-id"
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
|
@Unique(["email", "has_account"])
|
||||||
export class Customer extends SoftDeletableEntity {
|
export class Customer extends SoftDeletableEntity {
|
||||||
@Index({ unique: true })
|
@Index()
|
||||||
@Column()
|
@Column()
|
||||||
email: string
|
email: string
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,23 @@ export const CustomerServiceMock = {
|
|||||||
password_hash: "1234",
|
password_hash: "1234",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
retrieveRegisteredByEmail: jest.fn().mockImplementation((email) => {
|
||||||
if (email === "oliver@test.dk") {
|
if (email === "oliver@test.dk") {
|
||||||
return Scrypt.kdf("123456789", { logN: 1, r: 1, p: 1 }).then((hash) => ({
|
return Scrypt.kdf("123456789", { logN: 1, r: 1, p: 1 }).then((hash) => ({
|
||||||
email,
|
email,
|
||||||
password_hash: hash.toString("base64"),
|
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)
|
return Promise.resolve(undefined)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ describe("AuthService", () => {
|
|||||||
const authService = new AuthService({
|
const authService = new AuthService({
|
||||||
manager: managerMock,
|
manager: managerMock,
|
||||||
userService: UserServiceMock,
|
userService: UserServiceMock,
|
||||||
customerService: CustomerServiceMock
|
customerService: CustomerServiceMock,
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("authenticate", () => {
|
describe("authenticate", () => {
|
||||||
|
|||||||
@@ -167,10 +167,18 @@ describe("CartService", () => {
|
|||||||
})
|
})
|
||||||
const cartRepository = MockRepository()
|
const cartRepository = MockRepository()
|
||||||
const customerService = {
|
const customerService = {
|
||||||
retrieveByEmail: jest.fn().mockReturnValue(
|
retrieveUnregisteredByEmail: jest.fn().mockReturnValue(
|
||||||
Promise.resolve({
|
Promise.resolve({
|
||||||
id: IdMap.getId("customer"),
|
id: IdMap.getId("customer"),
|
||||||
email: "email@test.com",
|
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 () {
|
withTransaction: function () {
|
||||||
@@ -970,12 +978,13 @@ describe("CartService", () => {
|
|||||||
|
|
||||||
describe("updateEmail", () => {
|
describe("updateEmail", () => {
|
||||||
const customerService = {
|
const customerService = {
|
||||||
retrieveByEmail: jest.fn().mockImplementation((email) => {
|
retrieveUnregisteredByEmail: jest.fn().mockImplementation((email) => {
|
||||||
if (email === "no@mail.com") {
|
if (email === "no@mail.com") {
|
||||||
return Promise.reject()
|
return Promise.reject()
|
||||||
}
|
}
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: IdMap.getId("existing"),
|
id: IdMap.getId("existing"),
|
||||||
|
has_account: false,
|
||||||
email,
|
email,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -1025,6 +1034,7 @@ describe("CartService", () => {
|
|||||||
customer: {
|
customer: {
|
||||||
id: IdMap.getId("existing"),
|
id: IdMap.getId("existing"),
|
||||||
email: "test@testdom.com",
|
email: "test@testdom.com",
|
||||||
|
has_account: false,
|
||||||
},
|
},
|
||||||
email: "test@testdom.com",
|
email: "test@testdom.com",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -64,6 +64,32 @@ describe("CustomerService", () => {
|
|||||||
|
|
||||||
expect(result.id).toEqual(IdMap.getId("ironman"))
|
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", () => {
|
describe("retrieveByPhone", () => {
|
||||||
@@ -92,57 +118,118 @@ describe("CustomerService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("create", () => {
|
describe("create", () => {
|
||||||
const customerRepository = MockRepository({
|
const customerRepository = {
|
||||||
findOne: (query) => {
|
...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") {
|
if (query.where.email === "tony@stark.com") {
|
||||||
return Promise.resolve({
|
return Promise.resolve([
|
||||||
id: IdMap.getId("exists"),
|
[
|
||||||
password_hash: "test",
|
{
|
||||||
})
|
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({
|
const customerService = new CustomerService({
|
||||||
manager: MockManager,
|
manager: MockManager,
|
||||||
customerRepository,
|
customerRepository,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
|
configModule,
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully create a customer", async () => {
|
it("successfully creates a customer with password", async () => {
|
||||||
await customerService.create({
|
await customerService.create({
|
||||||
email: "oliver@medusa.com",
|
email: "john@doe.com",
|
||||||
first_name: "Oliver",
|
first_name: "John",
|
||||||
last_name: "Juhl",
|
last_name: "Doe",
|
||||||
|
password: "test",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(customerRepository.create).toBeCalledTimes(1)
|
expect(customerRepository.create).toBeCalledTimes(1)
|
||||||
expect(customerRepository.create).toBeCalledWith({
|
expect(customerRepository.create).toBeCalledWith({
|
||||||
email: "oliver@medusa.com",
|
email: "john@doe.com",
|
||||||
first_name: "Oliver",
|
first_name: "John",
|
||||||
last_name: "Juhl",
|
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({
|
await customerService.create({
|
||||||
email: "tony@stark.com",
|
email: "john@doe.com",
|
||||||
password: "stark123",
|
first_name: "John",
|
||||||
has_account: false,
|
last_name: "Doe",
|
||||||
|
password: "test",
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(customerRepository.save).toBeCalledTimes(1)
|
expect(customerRepository.listAndCount).toHaveBeenCalledWith(
|
||||||
expect(customerRepository.save).toBeCalledWith({
|
{
|
||||||
id: IdMap.getId("exists"),
|
relations: [],
|
||||||
email: "tony@stark.com",
|
skip: 0,
|
||||||
password_hash: expect.anything(),
|
take: 2,
|
||||||
has_account: true,
|
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> {
|
): Promise<AuthenticateResult> {
|
||||||
return await this.atomicPhase_(async (transactionManager) => {
|
return await this.atomicPhase_(async (transactionManager) => {
|
||||||
try {
|
try {
|
||||||
const customerPasswordHash: Customer = await this.customerService_
|
const customer: Customer = await this.customerService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.retrieveByEmail(email, {
|
.retrieveRegisteredByEmail(email, {
|
||||||
select: ["password_hash"],
|
select: ["id", "password_hash"],
|
||||||
})
|
})
|
||||||
if (customerPasswordHash.password_hash) {
|
if (customer.password_hash) {
|
||||||
const passwordsMatch = await this.comparePassword_(
|
const passwordsMatch = await this.comparePassword_(
|
||||||
password,
|
password,
|
||||||
customerPasswordHash.password_hash
|
customer.password_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
if (passwordsMatch) {
|
if (passwordsMatch) {
|
||||||
const customer = await this.customerService_
|
const customer = await this.customerService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.retrieveByEmail(email)
|
.retrieveRegisteredByEmail(email)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -325,8 +325,20 @@ class CartService extends TransactionBaseService {
|
|||||||
).id
|
).id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.email) {
|
if (data.customer_id) {
|
||||||
const customer = await this.createOrFetchUserFromEmail_(data.email)
|
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 = customer
|
||||||
rawCart.customer_id = customer.id
|
rawCart.customer_id = customer.id
|
||||||
rawCart.email = customer.email
|
rawCart.email = customer.email
|
||||||
@@ -992,7 +1004,9 @@ class CartService extends TransactionBaseService {
|
|||||||
if (data.customer_id) {
|
if (data.customer_id) {
|
||||||
await this.updateCustomerId_(cart, data.customer_id)
|
await this.updateCustomerId_(cart, data.customer_id)
|
||||||
} else if (isDefined(data.email)) {
|
} else if (isDefined(data.email)) {
|
||||||
const customer = await this.createOrFetchUserFromEmail_(data.email)
|
const customer = await this.createOrFetchGuestCustomerFromEmail_(
|
||||||
|
data.email
|
||||||
|
)
|
||||||
cart.customer = customer
|
cart.customer = customer
|
||||||
cart.customer_id = customer.id
|
cart.customer_id = customer.id
|
||||||
cart.email = customer.email
|
cart.email = customer.email
|
||||||
@@ -1181,14 +1195,14 @@ class CartService extends TransactionBaseService {
|
|||||||
* @param email - the email to use
|
* @param email - the email to use
|
||||||
* @return the resultign customer object
|
* @return the resultign customer object
|
||||||
*/
|
*/
|
||||||
protected async createOrFetchUserFromEmail_(
|
protected async createOrFetchGuestCustomerFromEmail_(
|
||||||
email: string
|
email: string
|
||||||
): Promise<Customer> {
|
): Promise<Customer> {
|
||||||
const validatedEmail = validateEmail(email)
|
const validatedEmail = validateEmail(email)
|
||||||
|
|
||||||
let customer = await this.customerService_
|
let customer = await this.customerService_
|
||||||
.withTransaction(this.transactionManager_)
|
.withTransaction(this.transactionManager_)
|
||||||
.retrieveByEmail(validatedEmail)
|
.retrieveUnregisteredByEmail(validatedEmail)
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken"
|
|||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import Scrypt from "scrypt-kdf"
|
import Scrypt from "scrypt-kdf"
|
||||||
import { DeepPartial, EntityManager } from "typeorm"
|
import { DeepPartial, EntityManager } from "typeorm"
|
||||||
|
import { EventBusService } from "."
|
||||||
import { StorePostCustomersCustomerAddressesAddressReq } from "../api"
|
import { StorePostCustomersCustomerAddressesAddressReq } from "../api"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
import { Address, Customer, CustomerGroup } from "../models"
|
import { Address, Customer, CustomerGroup } from "../models"
|
||||||
@@ -10,7 +11,6 @@ import { CustomerRepository } from "../repositories/customer"
|
|||||||
import { AddressCreatePayload, FindConfig, Selector } from "../types/common"
|
import { AddressCreatePayload, FindConfig, Selector } from "../types/common"
|
||||||
import { CreateCustomerInput, UpdateCustomerInput } from "../types/customers"
|
import { CreateCustomerInput, UpdateCustomerInput } from "../types/customers"
|
||||||
import { buildQuery, isDefined, setMetadata } from "../utils"
|
import { buildQuery, isDefined, setMetadata } from "../utils"
|
||||||
import EventBusService from "./event-bus"
|
|
||||||
|
|
||||||
type InjectedDependencies = {
|
type InjectedDependencies = {
|
||||||
manager: EntityManager
|
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 {string} email - the email of the customer to get.
|
||||||
* @param {Object} config - the config object containing query settings
|
* @param {Object} config - the config object containing query settings
|
||||||
* @return {Promise<Customer>} the customer document.
|
* @return {Promise<Customer>} the customer document.
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
async retrieveByEmail(
|
async retrieveByEmail(
|
||||||
email: string,
|
email: string,
|
||||||
@@ -198,6 +199,31 @@ class CustomerService extends TransactionBaseService {
|
|||||||
return await this.retrieve_({ email: email.toLowerCase() }, config)
|
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.
|
* Gets a customer by phone.
|
||||||
* @param {string} phone - the phone of the customer to get.
|
* @param {string} phone - the phone of the customer to get.
|
||||||
@@ -249,44 +275,45 @@ class CustomerService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
customer.email = customer.email.toLowerCase()
|
customer.email = customer.email.toLowerCase()
|
||||||
|
|
||||||
const { email, password } = customer
|
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) {
|
// should validate that "existing.some(acc => acc.has_account) && password"
|
||||||
throw new MedusaError(
|
if (existing) {
|
||||||
MedusaError.Types.DUPLICATE_ERROR,
|
if (existing.some((customer) => customer.has_account) && password) {
|
||||||
"A customer with the given email already has an account. Log in instead"
|
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)
|
const hashedPassword = await this.hashPassword_(password)
|
||||||
customer.password_hash = hashedPassword
|
customer.password_hash = hashedPassword
|
||||||
customer.has_account = true
|
customer.has_account = true
|
||||||
delete customer.password
|
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 SystemPaymentProviderService } from "./system-payment-provider"
|
||||||
export { default as TaxProviderService } from "./tax-provider"
|
export { default as TaxProviderService } from "./tax-provider"
|
||||||
export { default as TaxRateService } from "./tax-rate"
|
export { default as TaxRateService } from "./tax-rate"
|
||||||
|
export { default as TokenService } from "./token"
|
||||||
export { default as TotalsService } from "./totals"
|
export { default as TotalsService } from "./totals"
|
||||||
export { default as NewTotalsService } from "./new-totals"
|
export { default as NewTotalsService } from "./new-totals"
|
||||||
export { default as UserService } from "./user"
|
export { default as UserService } from "./user"
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import jwt, { JwtPayload } from "jsonwebtoken"
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { Brackets, EntityManager } from "typeorm"
|
import { Brackets, EntityManager } from "typeorm"
|
||||||
import { TransactionBaseService } from "../interfaces"
|
import { TransactionBaseService } from "../interfaces"
|
||||||
@@ -45,6 +46,8 @@ import ShippingOptionService from "./shipping-option"
|
|||||||
import ShippingProfileService from "./shipping-profile"
|
import ShippingProfileService from "./shipping-profile"
|
||||||
import TotalsService from "./totals"
|
import TotalsService from "./totals"
|
||||||
import { NewTotalsService, TaxProviderService } from "./index"
|
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"
|
export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists"
|
||||||
|
|
||||||
@@ -94,6 +97,7 @@ class OrderService extends TransactionBaseService {
|
|||||||
UPDATED: "order.updated",
|
UPDATED: "order.updated",
|
||||||
CANCELED: "order.canceled",
|
CANCELED: "order.canceled",
|
||||||
COMPLETED: "order.completed",
|
COMPLETED: "order.completed",
|
||||||
|
ORDERS_CLAIMED: "order.orders_claimed",
|
||||||
}
|
}
|
||||||
|
|
||||||
protected manager_: EntityManager
|
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