fix merge conflicts
This commit is contained in:
commit
283f8fbe54
152 files changed
+5559
-2541
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||
|
||||
describe("POST /admin/collections", () => {
|
||||
describe("successful creation", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/admin/collections", {
|
||||
payload: {
|
||||
title: "Suits",
|
||||
handle: "suits",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns created product collection", () => {
|
||||
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||
})
|
||||
|
||||
it("calls production collection service create", () => {
|
||||
expect(ProductCollectionServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(ProductCollectionServiceMock.create).toHaveBeenCalledWith({
|
||||
title: "Suits",
|
||||
handle: "suits",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("invalid data returns error details", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request("POST", "/admin/collections", {
|
||||
payload: {
|
||||
handle: "no-title-collection",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 400", () => {
|
||||
expect(subject.status).toEqual(400)
|
||||
})
|
||||
|
||||
it("returns error details", () => {
|
||||
expect(subject.body.name).toEqual("invalid_data")
|
||||
expect(subject.body.message[0].message).toEqual(`"title" is required`)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||
|
||||
describe("DELETE /admin/collections/:id", () => {
|
||||
describe("successful removes collection", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"DELETE",
|
||||
`/admin/collections/${IdMap.getId("collection")}`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls product collection service delete", () => {
|
||||
expect(ProductCollectionServiceMock.delete).toHaveBeenCalledTimes(1)
|
||||
expect(ProductCollectionServiceMock.delete).toHaveBeenCalledWith(
|
||||
IdMap.getId("collection")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns delete result", () => {
|
||||
expect(subject.body).toEqual({
|
||||
id: IdMap.getId("collection"),
|
||||
object: "product-collection",
|
||||
deleted: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||
|
||||
describe("GET /admin/categories/:id", () => {
|
||||
describe("get collection by id successfully", () => {
|
||||
let subject
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"GET",
|
||||
`/admin/collections/${IdMap.getId("col")}`,
|
||||
{
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls retrieve from product collection service", () => {
|
||||
expect(ProductCollectionServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(ProductCollectionServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("col")
|
||||
)
|
||||
})
|
||||
|
||||
it("returns variant decorated", () => {
|
||||
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||
|
||||
describe("GET /admin/collections", () => {
|
||||
describe("successful retrieval", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
subject = await request("GET", `/admin/collections`, {
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("calls product collection service list", () => {
|
||||
expect(ProductCollectionServiceMock.list).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
import { request } from "../../../../../helpers/test-request"
|
||||
import { ProductCollectionServiceMock } from "../../../../../services/__mocks__/product-collection"
|
||||
|
||||
describe("POST /admin/collections/:id", () => {
|
||||
describe("successful update", () => {
|
||||
let subject
|
||||
|
||||
beforeAll(async () => {
|
||||
subject = await request(
|
||||
"POST",
|
||||
`/admin/collections/${IdMap.getId("col")}`,
|
||||
{
|
||||
payload: {
|
||||
title: "Suits and vests",
|
||||
},
|
||||
adminSession: {
|
||||
jwt: {
|
||||
userId: IdMap.getId("admin_user"),
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it("returns 200", () => {
|
||||
expect(subject.status).toEqual(200)
|
||||
})
|
||||
|
||||
it("returns updated product collection", () => {
|
||||
expect(subject.body.collection.id).toEqual(IdMap.getId("col"))
|
||||
})
|
||||
|
||||
it("product collection service update", () => {
|
||||
expect(ProductCollectionServiceMock.update).toHaveBeenCalledTimes(1)
|
||||
expect(ProductCollectionServiceMock.update).toHaveBeenCalledWith(
|
||||
IdMap.getId("col"),
|
||||
{
|
||||
title: "Suits and vests",
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
title: Validator.string().required(),
|
||||
handle: Validator.string()
|
||||
.optional()
|
||||
.allow(""),
|
||||
metadata: Validator.object().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const productCollectionService = req.scope.resolve(
|
||||
"productCollectionService"
|
||||
)
|
||||
|
||||
const created = await productCollectionService.create(value)
|
||||
const collection = await productCollectionService.retrieve(created.id)
|
||||
|
||||
res.status(200).json({ collection })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
try {
|
||||
const productCollectionService = req.scope.resolve(
|
||||
"productCollectionService"
|
||||
)
|
||||
await productCollectionService.delete(id)
|
||||
|
||||
res.json({
|
||||
id,
|
||||
object: "product-collection",
|
||||
deleted: true,
|
||||
})
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
const productCollectionService = req.scope.resolve(
|
||||
"productCollectionService"
|
||||
)
|
||||
|
||||
const collection = await productCollectionService.retrieve(id)
|
||||
res.status(200).json({ collection })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/collections", route)
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-collection").default))
|
||||
route.post("/:id", middlewares.wrap(require("./update-collection").default))
|
||||
|
||||
route.delete("/:id", middlewares.wrap(require("./delete-collection").default))
|
||||
|
||||
route.get("/:id", middlewares.wrap(require("./get-collection").default))
|
||||
route.get("/", middlewares.wrap(require("./list-collections").default))
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultFields = ["id", "title", "handle"]
|
||||
export const defaultRelations = ["products"]
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defaultFields, defaultRelations } from "."
|
||||
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const selector = {}
|
||||
|
||||
const limit = parseInt(req.query.limit) || 10
|
||||
const offset = parseInt(req.query.offset) || 0
|
||||
|
||||
const productCollectionService = req.scope.resolve(
|
||||
"productCollectionService"
|
||||
)
|
||||
|
||||
const listConfig = {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
}
|
||||
|
||||
const collections = await productCollectionService.list(
|
||||
selector,
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.status(200).json({ collections })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
title: Validator.string().optional(),
|
||||
handle: Validator.string().optional(),
|
||||
metadata: Validator.object().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const productCollectionService = req.scope.resolve(
|
||||
"productCollectionService"
|
||||
)
|
||||
|
||||
const updated = await productCollectionService.update(id, value)
|
||||
const collection = await productCollectionService.retrieve(updated.id)
|
||||
|
||||
res.status(200).json({ collection })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import swapRoutes from "./swaps"
|
||||
import returnRoutes from "./returns"
|
||||
import variantRoutes from "./variants"
|
||||
import draftOrderRoutes from "./draft-orders"
|
||||
import collectionRoutes from "./collections"
|
||||
import notificationRoutes from "./notifications"
|
||||
|
||||
const route = Router()
|
||||
|
||||
@@ -62,6 +64,8 @@ export default (app, container, config) => {
|
||||
returnRoutes(route)
|
||||
variantRoutes(route)
|
||||
draftOrderRoutes(route)
|
||||
collectionRoutes(route)
|
||||
notificationRoutes(route)
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Router } from "express"
|
||||
import middlewares from "../../../middlewares"
|
||||
|
||||
const route = Router()
|
||||
|
||||
export default app => {
|
||||
app.use("/notifications", route)
|
||||
|
||||
/**
|
||||
* List notifications
|
||||
*/
|
||||
route.get("/", middlewares.wrap(require("./list-notifications").default))
|
||||
|
||||
/**
|
||||
* Resend a notification
|
||||
*/
|
||||
route.post(
|
||||
"/:id/resend",
|
||||
middlewares.wrap(require("./resend-notification").default)
|
||||
)
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
export const defaultRelations = ["resends"]
|
||||
export const allowedRelations = ["resends"]
|
||||
|
||||
export const defaultFields = [
|
||||
"id",
|
||||
"resource_type",
|
||||
"resource_id",
|
||||
"event_name",
|
||||
"to",
|
||||
"provider_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
export const allowedFields = [
|
||||
"id",
|
||||
"resource_type",
|
||||
"resource_id",
|
||||
"provider_id",
|
||||
"event_name",
|
||||
"to",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
]
|
||||
@@ -0,0 +1,64 @@
|
||||
import _ from "lodash"
|
||||
import { defaultRelations, defaultFields } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const notificationService = req.scope.resolve("notificationService")
|
||||
|
||||
const limit = parseInt(req.query.limit) || 50
|
||||
const offset = parseInt(req.query.offset) || 0
|
||||
|
||||
let selector = {}
|
||||
|
||||
let includeFields = []
|
||||
if ("fields" in req.query) {
|
||||
includeFields = req.query.fields.split(",")
|
||||
}
|
||||
|
||||
let expandFields = []
|
||||
if ("expand" in req.query) {
|
||||
expandFields = req.query.expand.split(",")
|
||||
}
|
||||
|
||||
if ("event_name" in req.query) {
|
||||
const values = req.query.event_name.split(",")
|
||||
selector.event_name = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if ("resource_type" in req.query) {
|
||||
const values = req.query.resource_type.split(",")
|
||||
selector.resource_type = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if ("resource_id" in req.query) {
|
||||
const values = req.query.resource_id.split(",")
|
||||
selector.resource_id = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if ("to" in req.query) {
|
||||
const values = req.query.to.split(",")
|
||||
selector.to = values.length > 1 ? values : values[0]
|
||||
}
|
||||
|
||||
if (!("include_resends" in req.query)) {
|
||||
selector.parent_id = null
|
||||
}
|
||||
|
||||
const listConfig = {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: expandFields.length ? expandFields : defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
|
||||
const notifications = await notificationService.list(selector, listConfig)
|
||||
|
||||
const fields = [...listConfig.select, ...listConfig.relations]
|
||||
const data = notifications.map(o => _.pick(o, fields))
|
||||
|
||||
res.json({ notifications: data, offset, limit })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultFields, defaultRelations } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
to: Validator.string().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const notificationService = req.scope.resolve("notificationService")
|
||||
|
||||
const config = {}
|
||||
|
||||
if (value.to) {
|
||||
config.to = value.to
|
||||
}
|
||||
|
||||
await notificationService.resend(id, config)
|
||||
|
||||
const notification = await notificationService.retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.json({ notification })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultFields, defaultRelations } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, claim_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
fulfillment_id: Validator.string().required(),
|
||||
tracking_numbers: Validator.array()
|
||||
.items(Validator.string())
|
||||
.optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const claimService = req.scope.resolve("claimService")
|
||||
|
||||
await claimService.createShipment(
|
||||
claim_id,
|
||||
value.fulfillment_id,
|
||||
value.tracking_numbers
|
||||
)
|
||||
|
||||
const order = await orderService.retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.json({ order })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -161,6 +161,14 @@ export default app => {
|
||||
middlewares.wrap(require("./fulfill-claim").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates claim fulfillment
|
||||
*/
|
||||
route.post(
|
||||
"/:id/claims/:claim_id/shipments",
|
||||
middlewares.wrap(require("./create-claim-shipment").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete metadata key / value pair.
|
||||
*/
|
||||
|
||||
@@ -53,6 +53,7 @@ export default async (req, res) => {
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const returnService = req.scope.resolve("returnService")
|
||||
const eventBus = req.scope.resolve("eventBusService")
|
||||
|
||||
let inProgress = true
|
||||
let err = false
|
||||
@@ -98,6 +99,13 @@ export default async (req, res) => {
|
||||
.fulfill(createdReturn.id)
|
||||
}
|
||||
|
||||
await eventBus
|
||||
.withTransaction(manager)
|
||||
.emit("order.return_requested", {
|
||||
id,
|
||||
return_id: createdReturn.id,
|
||||
})
|
||||
|
||||
return {
|
||||
recovery_point: "return_requested",
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("POST /admin/products", () => {
|
||||
payload: {
|
||||
title: "Test Product",
|
||||
description: "Test Description",
|
||||
tags: "hi,med,dig",
|
||||
tags: [{ id: "test", value: "test" }],
|
||||
handle: "test-product",
|
||||
},
|
||||
adminSession: {
|
||||
@@ -36,7 +36,7 @@ describe("POST /admin/products", () => {
|
||||
expect(ProductServiceMock.create).toHaveBeenCalledWith({
|
||||
title: "Test Product",
|
||||
description: "Test Description",
|
||||
tags: "hi,med,dig",
|
||||
tags: [{ id: "test", value: "test" }],
|
||||
handle: "test-product",
|
||||
is_giftcard: false,
|
||||
profile_id: IdMap.getId("default_shipping_profile"),
|
||||
|
||||
@@ -34,11 +34,12 @@ describe("GET /admin/products/:id", () => {
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"tags",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
@@ -57,6 +58,9 @@ describe("GET /admin/products/:id", () => {
|
||||
"variants.options",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,13 +6,28 @@ export default async (req, res) => {
|
||||
title: Validator.string().required(),
|
||||
subtitle: Validator.string().allow(""),
|
||||
description: Validator.string().allow(""),
|
||||
tags: Validator.string().optional(),
|
||||
is_giftcard: Validator.boolean().default(false),
|
||||
images: Validator.array()
|
||||
.items(Validator.string())
|
||||
.optional(),
|
||||
thumbnail: Validator.string().optional(),
|
||||
handle: Validator.string().optional(),
|
||||
type: Validator.object()
|
||||
.keys({
|
||||
id: Validator.string().optional(),
|
||||
value: Validator.string().required(),
|
||||
})
|
||||
.allow(null)
|
||||
.optional(),
|
||||
collection_id: Validator.string()
|
||||
.allow(null)
|
||||
.optional(),
|
||||
tags: Validator.array()
|
||||
.items({
|
||||
id: Validator.string().optional(),
|
||||
value: Validator.string().required(),
|
||||
})
|
||||
.optional(),
|
||||
options: Validator.array().items({
|
||||
title: Validator.string().required(),
|
||||
}),
|
||||
@@ -48,7 +63,7 @@ export default async (req, res) => {
|
||||
Validator.object()
|
||||
.keys({
|
||||
region_id: Validator.string(),
|
||||
currency_code: Validator.string().required(),
|
||||
currency_code: Validator.string(),
|
||||
amount: Validator.number()
|
||||
.integer()
|
||||
.required(),
|
||||
|
||||
@@ -8,6 +8,11 @@ export default app => {
|
||||
|
||||
route.post("/", middlewares.wrap(require("./create-product").default))
|
||||
route.post("/:id", middlewares.wrap(require("./update-product").default))
|
||||
route.get("/types", middlewares.wrap(require("./list-types").default))
|
||||
route.get(
|
||||
"/tag-usage",
|
||||
middlewares.wrap(require("./list-tag-usage-count").default)
|
||||
)
|
||||
|
||||
route.post(
|
||||
"/:id/variants",
|
||||
@@ -52,6 +57,9 @@ export const defaultRelations = [
|
||||
"variants.options",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
]
|
||||
|
||||
export const defaultFields = [
|
||||
@@ -59,11 +67,12 @@ export const defaultFields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"tags",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
@@ -82,11 +91,12 @@ export const allowedFields = [
|
||||
"title",
|
||||
"subtitle",
|
||||
"description",
|
||||
"tags",
|
||||
"handle",
|
||||
"is_giftcard",
|
||||
"thumbnail",
|
||||
"profile_id",
|
||||
"collection_id",
|
||||
"type_id",
|
||||
"weight",
|
||||
"length",
|
||||
"height",
|
||||
@@ -105,4 +115,7 @@ export const allowedRelations = [
|
||||
"variants.prices",
|
||||
"images",
|
||||
"options",
|
||||
"tags",
|
||||
"type",
|
||||
"collection",
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const productService = req.scope.resolve("productService")
|
||||
|
||||
const tags = await productService.listTagsByUsage()
|
||||
|
||||
res.json({ tags })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default async (req, res) => {
|
||||
try {
|
||||
const productService = req.scope.resolve("productService")
|
||||
|
||||
const types = await productService.listTypes()
|
||||
|
||||
res.json({ types })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,22 @@ export default async (req, res) => {
|
||||
const schema = Validator.object().keys({
|
||||
title: Validator.string().optional(),
|
||||
description: Validator.string().optional(),
|
||||
tags: Validator.string().optional(),
|
||||
type: Validator.object()
|
||||
.keys({
|
||||
id: Validator.string().optional(),
|
||||
value: Validator.string().required(),
|
||||
})
|
||||
.allow(null)
|
||||
.optional(),
|
||||
collection_id: Validator.string()
|
||||
.allow(null)
|
||||
.optional(),
|
||||
tags: Validator.array()
|
||||
.items({
|
||||
id: Validator.string().optional(),
|
||||
value: Validator.string().required(),
|
||||
})
|
||||
.optional(),
|
||||
handle: Validator.string().optional(),
|
||||
weight: Validator.number().optional(),
|
||||
length: Validator.number().optional(),
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("POST /store/customers", () => {
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{ relations: ["orders", "shipping_addresses"] }
|
||||
{ relations: ["shipping_addresses"] }
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("POST /store/customers/:id", () => {
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledTimes(1)
|
||||
expect(CustomerServiceMock.retrieve).toHaveBeenCalledWith(
|
||||
IdMap.getId("lebron"),
|
||||
{ relations: ["orders", "shipping_addresses"] }
|
||||
{ relations: ["shipping_addresses"] }
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export default async (req, res) => {
|
||||
|
||||
let customer = await customerService.addAddress(id, value.address)
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async (req, res) => {
|
||||
})
|
||||
|
||||
customer = await customerService.retrieve(customer.id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
@@ -6,7 +6,7 @@ export default async (req, res) => {
|
||||
let customer = await customerService.removeAddress(id, address_id)
|
||||
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.json({ customer: data })
|
||||
|
||||
@@ -3,7 +3,7 @@ export default async (req, res) => {
|
||||
try {
|
||||
const customerService = req.scope.resolve("customerService")
|
||||
const customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
res.json({ customer })
|
||||
} catch (err) {
|
||||
|
||||
@@ -33,6 +33,8 @@ export default (app, container) => {
|
||||
route.get("/:id", middlewares.wrap(require("./get-customer").default))
|
||||
route.post("/:id", middlewares.wrap(require("./update-customer").default))
|
||||
|
||||
route.get("/:id/orders", middlewares.wrap(require("./list-orders").default))
|
||||
|
||||
route.post(
|
||||
"/:id/addresses",
|
||||
middlewares.wrap(require("./create-address").default)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import _ from "lodash"
|
||||
import {
|
||||
defaultRelations,
|
||||
defaultFields,
|
||||
allowedFields,
|
||||
allowedRelations,
|
||||
} from "../orders"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
|
||||
let selector = {
|
||||
customer_id: id,
|
||||
}
|
||||
|
||||
const limit = parseInt(req.query.limit) || 10
|
||||
const offset = parseInt(req.query.offset) || 0
|
||||
|
||||
let includeFields = []
|
||||
if ("fields" in req.query) {
|
||||
includeFields = req.query.fields.split(",")
|
||||
includeFields = includeFields.filter(f => allowedFields.includes(f))
|
||||
}
|
||||
|
||||
let expandFields = []
|
||||
if ("expand" in req.query) {
|
||||
expandFields = req.query.expand.split(",")
|
||||
expandFields = expandFields.filter(f => allowedRelations.includes(f))
|
||||
}
|
||||
|
||||
const listConfig = {
|
||||
select: includeFields.length ? includeFields : defaultFields,
|
||||
relations: expandFields.length ? expandFields : defaultRelations,
|
||||
skip: offset,
|
||||
take: limit,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
|
||||
const [orders, count] = await orderService.listAndCount(
|
||||
selector,
|
||||
listConfig
|
||||
)
|
||||
|
||||
res.json({ orders, count, offset, limit })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export default async (req, res) => {
|
||||
)
|
||||
|
||||
customer = await customerService.retrieve(id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.json({ customer })
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async (req, res) => {
|
||||
let customer = await customerService.update(id, value)
|
||||
|
||||
customer = await customerService.retrieve(customer.id, {
|
||||
relations: ["orders", "shipping_addresses"],
|
||||
relations: ["shipping_addresses"],
|
||||
})
|
||||
|
||||
res.status(200).json({ customer })
|
||||
|
||||
@@ -18,6 +18,7 @@ export default app => {
|
||||
|
||||
export const defaultRelations = [
|
||||
"shipping_address",
|
||||
"fulfillments",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
@@ -46,3 +47,36 @@ export const defaultFields = [
|
||||
"subtotal",
|
||||
"total",
|
||||
]
|
||||
|
||||
export const allowedRelations = [
|
||||
"shipping_address",
|
||||
"fulfillments",
|
||||
"billing_address",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
"shipping_methods",
|
||||
"discounts",
|
||||
"customer",
|
||||
"payments",
|
||||
"region",
|
||||
]
|
||||
|
||||
export const allowedFields = [
|
||||
"id",
|
||||
"display_id",
|
||||
"cart_id",
|
||||
"customer_id",
|
||||
"email",
|
||||
"region_id",
|
||||
"currency_code",
|
||||
"tax_rate",
|
||||
"created_at",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"refunded_total",
|
||||
"gift_card_total",
|
||||
"subtotal",
|
||||
"total",
|
||||
]
|
||||
@@ -21,10 +21,13 @@ export { Order } from "./models/order"
|
||||
export { PaymentProvider } from "./models/payment-provider"
|
||||
export { PaymentSession } from "./models/payment-session"
|
||||
export { Payment } from "./models/payment"
|
||||
export { ProductOptionValue } from "./models/product-option-value"
|
||||
export { ProductOption } from "./models/product-option"
|
||||
export { ProductVariant } from "./models/product-variant"
|
||||
export { Product } from "./models/product"
|
||||
export { ProductCollection } from "./models/product-collection"
|
||||
export { ProductOption } from "./models/product-option"
|
||||
export { ProductOptionValue } from "./models/product-option-value"
|
||||
export { ProductVariant } from "./models/product-variant"
|
||||
export { ProductTag } from "./models/product-tag"
|
||||
export { ProductType } from "./models/product-type"
|
||||
export { Refund } from "./models/refund"
|
||||
export { Region } from "./models/region"
|
||||
export { ReturnItem } from "./models/return-item"
|
||||
|
||||
@@ -13,6 +13,12 @@ export default async ({ container }) => {
|
||||
payIds = payProviders.map(p => p.getIdentifier())
|
||||
await pProviderService.registerInstalledProviders(payIds)
|
||||
|
||||
let notiIds
|
||||
const nProviderService = container.resolve("notificationService")
|
||||
const notiProviders = container.resolve("notificationProviders")
|
||||
notiIds = notiProviders.map(p => p.getIdentifier())
|
||||
await nProviderService.registerInstalledProviders(notiIds)
|
||||
|
||||
let fulfilIds
|
||||
const fProviderService = container.resolve("fulfillmentProviderService")
|
||||
const fulfilProviders = container.resolve("fulfillmentProviders")
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
BaseService,
|
||||
PaymentService,
|
||||
FulfillmentService,
|
||||
NotificationService,
|
||||
FileService,
|
||||
OauthService,
|
||||
} from "medusa-interfaces"
|
||||
@@ -228,6 +229,20 @@ async function registerServices(pluginDetails, container) {
|
||||
).singleton(),
|
||||
[`fp_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof NotificationService) {
|
||||
container.registerAdd(
|
||||
"notificationProviders",
|
||||
asFunction(cradle => new loaded(cradle, pluginDetails.options))
|
||||
)
|
||||
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which payment provider we need to use
|
||||
container.register({
|
||||
[name]: asFunction(
|
||||
cradle => new loaded(cradle, pluginDetails.options)
|
||||
).singleton(),
|
||||
[`noti_${loaded.identifier}`]: aliasTo(name),
|
||||
})
|
||||
} else if (loaded.prototype instanceof FileService) {
|
||||
// Add the service directly to the container in order to make simple
|
||||
// resolution if we already know which payment provider we need to use
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class notifications1613146953072 implements MigrationInterface {
|
||||
name = "notifications1613146953072"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notification_provider" ("id" character varying NOT NULL, "is_installed" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_0425c2423e2ce9fdfd5c23761d9" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "notification" ("id" character varying NOT NULL, "event_name" character varying, "resource_type" character varying NOT NULL, "resource_id" character varying NOT NULL, "customer_id" character varying, "to" character varying NOT NULL, "data" jsonb NOT NULL, "parent_id" character varying, "provider_id" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_705b6c7cdf9b2c2ff7ac7872cb7" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_df1494d263740fcfb1d09a98fc" ON "notification" ("resource_type") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ea6a358d9ce41c16499aae55f9" ON "notification" ("resource_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_b5df0f53a74b9d0c0a2b652c88" ON "notification" ("customer_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" ADD CONSTRAINT "FK_b5df0f53a74b9d0c0a2b652c88d" FOREIGN KEY ("customer_id") REFERENCES "customer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" ADD CONSTRAINT "FK_371db513192c083f48ba63c33be" FOREIGN KEY ("parent_id") REFERENCES "notification"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" ADD CONSTRAINT "FK_0425c2423e2ce9fdfd5c23761d9" FOREIGN KEY ("provider_id") REFERENCES "notification_provider"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" DROP CONSTRAINT "FK_0425c2423e2ce9fdfd5c23761d9"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" DROP CONSTRAINT "FK_371db513192c083f48ba63c33be"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "notification" DROP CONSTRAINT "FK_b5df0f53a74b9d0c0a2b652c88d"`
|
||||
)
|
||||
await queryRunner.query(`DROP INDEX "IDX_b5df0f53a74b9d0c0a2b652c88"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_ea6a358d9ce41c16499aae55f9"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_df1494d263740fcfb1d09a98fc"`)
|
||||
await queryRunner.query(`DROP TABLE "notification"`)
|
||||
await queryRunner.query(`DROP TABLE "notification_provider"`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class productTypeCategoryTags1613146953073
|
||||
implements MigrationInterface {
|
||||
name = "productTypeCategoryTags1613146953073"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_collection" ("id" character varying NOT NULL, "title" character varying NOT NULL, "handle" character varying, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_49d419fc77d3aed46c835c558ac" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE UNIQUE INDEX "IDX_6910923cb678fd6e99011a21cc" ON "product_collection" ("handle") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_tag" ("id" character varying NOT NULL, "value" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_1439455c6528caa94fcc8564fda" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_type" ("id" character varying NOT NULL, "value" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, CONSTRAINT "PK_e0843930fbb8854fe36ca39dae1" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "product_tags" ("product_id" character varying NOT NULL, "product_tag_id" character varying NOT NULL, CONSTRAINT "PK_1cf5c9537e7198df494b71b993f" PRIMARY KEY ("product_id", "product_tag_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_5b0c6fc53c574299ecc7f9ee22" ON "product_tags" ("product_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_21683a063fe82dafdf681ecc9c" ON "product_tags" ("product_tag_id") `
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "tags"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" ADD "collection_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" ADD "type_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" ADD CONSTRAINT "FK_49d419fc77d3aed46c835c558ac" FOREIGN KEY ("collection_id") REFERENCES "product_collection"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tags" ADD CONSTRAINT "FK_5b0c6fc53c574299ecc7f9ee22e" FOREIGN KEY ("product_id") REFERENCES "product"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tags" ADD CONSTRAINT "FK_21683a063fe82dafdf681ecc9c4" FOREIGN KEY ("product_tag_id") REFERENCES "product_tag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" ADD CONSTRAINT "FK_e0843930fbb8854fe36ca39dae1" FOREIGN KEY ("type_id") REFERENCES "product_type"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tags" DROP CONSTRAINT "FK_21683a063fe82dafdf681ecc9c4"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product_tags" DROP CONSTRAINT "FK_5b0c6fc53c574299ecc7f9ee22e"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" DROP CONSTRAINT "FK_49d419fc77d3aed46c835c558ac"`
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "type_id"`)
|
||||
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "collection_id"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" DROP CONSTRAINT "FK_e0843930fbb8854fe36ca39dae1"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "product" ADD "tags" character varying`
|
||||
)
|
||||
await queryRunner.query(`DROP INDEX "IDX_21683a063fe82dafdf681ecc9c"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_5b0c6fc53c574299ecc7f9ee22"`)
|
||||
await queryRunner.query(`DROP TABLE "product_tags"`)
|
||||
await queryRunner.query(`DROP TABLE "product_type"`)
|
||||
await queryRunner.query(`DROP TABLE "product_tag"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_6910923cb678fd6e99011a21cc"`)
|
||||
await queryRunner.query(`DROP TABLE "product_collection"`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Entity, Column, PrimaryColumn } from "typeorm"
|
||||
|
||||
@Entity()
|
||||
export class NotificationProvider {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ default: true })
|
||||
is_installed: boolean
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Index,
|
||||
PrimaryColumn,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { Customer } from "./customer"
|
||||
import { NotificationProvider } from "./notification-provider"
|
||||
|
||||
@Entity()
|
||||
export class Notification {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
event_name: string
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
resource_type: string
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
resource_id: string
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
customer_id: string
|
||||
|
||||
@ManyToOne(() => Customer)
|
||||
@JoinColumn({ name: "customer_id" })
|
||||
customer: Customer
|
||||
|
||||
@Column()
|
||||
to: string
|
||||
|
||||
@Column({ type: "jsonb" })
|
||||
data: any
|
||||
|
||||
@Column({ nullable: true })
|
||||
parent_id: string
|
||||
|
||||
@ManyToOne(() => Notification)
|
||||
@JoinColumn({ name: "parent_id" })
|
||||
parent_notification: Notification
|
||||
|
||||
@OneToMany(
|
||||
() => Notification,
|
||||
noti => noti.parent_notification
|
||||
)
|
||||
resends: Notification[]
|
||||
|
||||
@Column({ nullable: true })
|
||||
provider_id: string
|
||||
|
||||
@ManyToOne(() => NotificationProvider)
|
||||
@JoinColumn({ name: "provider_id" })
|
||||
provider: NotificationProvider
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: "timestamptz" })
|
||||
updated_at: Date
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `noti_${id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
ManyToMany,
|
||||
Index,
|
||||
OneToMany,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { Product } from "./product"
|
||||
import _ from "lodash"
|
||||
|
||||
@Entity()
|
||||
export class ProductCollection {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
title: string
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column({ nullable: true })
|
||||
handle: string
|
||||
|
||||
@OneToMany(
|
||||
() => Product,
|
||||
product => product.collection
|
||||
)
|
||||
products: Product[]
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: "timestamptz" })
|
||||
updated_at: Date
|
||||
|
||||
@DeleteDateColumn({ type: "timestamptz" })
|
||||
deleted_at: Date
|
||||
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `pcol_${id}`
|
||||
|
||||
if (!this.handle) {
|
||||
this.handle = _.kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@ export class ProductOption {
|
||||
)
|
||||
values: ProductOptionValue
|
||||
|
||||
@Column()
|
||||
product_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => Product,
|
||||
product => product.options
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
@Entity()
|
||||
export class ProductTag {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
value: string
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: "timestamptz" })
|
||||
updated_at: Date
|
||||
|
||||
@DeleteDateColumn({ type: "timestamptz" })
|
||||
deleted_at: Date
|
||||
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `ptag_${id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
@Entity()
|
||||
export class ProductType {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
value: string
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: "timestamptz" })
|
||||
updated_at: Date
|
||||
|
||||
@DeleteDateColumn({ type: "timestamptz" })
|
||||
deleted_at: Date
|
||||
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `ptyp_${id}`
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,13 @@ import {
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { Image } from "./image"
|
||||
import { ProductCollection } from "./product-collection"
|
||||
import { ProductOption } from "./product-option"
|
||||
import { ProductTag } from "./product-tag"
|
||||
import { ProductType } from "./product-type"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { ShippingProfile } from "./shipping-profile"
|
||||
import _ from "lodash"
|
||||
|
||||
@Entity()
|
||||
export class Product {
|
||||
@@ -35,9 +39,6 @@ export class Product {
|
||||
@Column({ nullable: true })
|
||||
description: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
tags: string
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column({ nullable: true })
|
||||
handle: string
|
||||
@@ -107,6 +108,34 @@ export class Product {
|
||||
@Column({ nullable: true })
|
||||
material: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
collection_id: string
|
||||
|
||||
@ManyToOne(() => ProductCollection)
|
||||
@JoinColumn({ name: "collection_id" })
|
||||
collection: ProductCollection
|
||||
|
||||
@Column({ nullable: true })
|
||||
type_id: string
|
||||
|
||||
@ManyToOne(() => ProductType)
|
||||
@JoinColumn({ name: "type_id" })
|
||||
type: ProductType
|
||||
|
||||
@ManyToMany(() => ProductTag)
|
||||
@JoinTable({
|
||||
name: "product_tags",
|
||||
joinColumn: {
|
||||
name: "product_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "product_tag_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
tags: ProductTag[]
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@@ -124,5 +153,9 @@ export class Product {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `prod_${id}`
|
||||
|
||||
if (!this.handle) {
|
||||
this.handle = _.kebabCase(this.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { NotificationProvider } from "../models/notification-provider"
|
||||
|
||||
@EntityRepository(NotificationProvider)
|
||||
export class NotificationProviderRepository extends Repository<
|
||||
NotificationProvider
|
||||
> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { Notification } from "../models/notification"
|
||||
|
||||
@EntityRepository(Notification)
|
||||
export class NotificationRepository extends Repository<Notification> {}
|
||||
@@ -1,5 +1,56 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { flatten, groupBy, map, merge } from "lodash"
|
||||
import { EntityRepository, Repository, FindManyOptions } from "typeorm"
|
||||
import { Order } from "../models/order"
|
||||
|
||||
@EntityRepository(Order)
|
||||
export class OrderRepository extends Repository<Order> {}
|
||||
export class OrderRepository extends Repository<Order> {
|
||||
public async findWithRelations(
|
||||
relations: Array<keyof Order> = [],
|
||||
optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {}
|
||||
): Promise<Order[]> {
|
||||
const entities = await this.find(optionsWithoutRelations)
|
||||
const entitiesIds = entities.map(({ id }) => id)
|
||||
const entitiesIdsWithRelations = await Promise.all(
|
||||
relations.map(relation => {
|
||||
const relationParts = relation.split(".") as string[]
|
||||
|
||||
const partialRelations = relationParts.reduce(
|
||||
(acc: string[], _: string, index: number) => {
|
||||
const toPush = []
|
||||
|
||||
for (let i = 0; i <= index; i++) {
|
||||
toPush.push(relationParts[i])
|
||||
}
|
||||
|
||||
acc.push(toPush.join("."))
|
||||
|
||||
return acc
|
||||
},
|
||||
[] as string[]
|
||||
)
|
||||
|
||||
return this.findByIds(entitiesIds, {
|
||||
select: ["id"],
|
||||
relations: partialRelations,
|
||||
})
|
||||
})
|
||||
).then(flatten)
|
||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||
|
||||
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
||||
return map(entitiesAndRelationsById, entityAndRelations =>
|
||||
merge({}, ...entityAndRelations)
|
||||
)
|
||||
}
|
||||
|
||||
public async findOneWithRelations(
|
||||
relations: Array<keyof Order> = [],
|
||||
optionsWithoutRelations: Omit<FindManyOptions<Order>, "relations"> = {}
|
||||
): Promise<Order> {
|
||||
const result = await this.findWithRelations(
|
||||
relations,
|
||||
optionsWithoutRelations
|
||||
)
|
||||
return result[0]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ProductCollection } from "../models/product-collection"
|
||||
|
||||
@EntityRepository(ProductCollection)
|
||||
export class ProductCollectionRepository extends Repository<
|
||||
ProductCollection
|
||||
> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ProductTag } from "../models/product-tag"
|
||||
|
||||
@EntityRepository(ProductTag)
|
||||
export class ProductTagRepository extends Repository<ProductTag> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ProductType } from "../models/product-type"
|
||||
|
||||
@EntityRepository(ProductType)
|
||||
export class ProductTypeRepository extends Repository<ProductType> {}
|
||||
@@ -1,6 +1,9 @@
|
||||
export const EventBusServiceMock = {
|
||||
emit: jest.fn(),
|
||||
subscribe: jest.fn(),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { IdMap } from "medusa-test-utils"
|
||||
|
||||
export const ProductCollectionServiceMock = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
create: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve({ id: IdMap.getId("col"), ...data })
|
||||
}),
|
||||
retrieve: jest.fn().mockImplementation(id => {
|
||||
if (id === IdMap.getId("col")) {
|
||||
return Promise.resolve({ id: IdMap.getId("col"), title: "Suits" })
|
||||
}
|
||||
}),
|
||||
delete: jest.fn().mockReturnValue(Promise.resolve()),
|
||||
update: jest.fn().mockImplementation((id, value) => {
|
||||
return Promise.resolve({ id, title: value })
|
||||
}),
|
||||
list: jest.fn().mockImplementation(data => {
|
||||
return Promise.resolve([{ id: IdMap.getId("col"), title: "Suits" }])
|
||||
}),
|
||||
}
|
||||
|
||||
const mock = jest.fn().mockImplementation(() => {
|
||||
return ProductCollectionServiceMock
|
||||
})
|
||||
|
||||
export default mock
|
||||
@@ -0,0 +1,54 @@
|
||||
import NotificationService from "../notification"
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
|
||||
describe("NotificationService", () => {
|
||||
describe("send", () => {
|
||||
const notificationRepository = MockRepository({ create: c => c })
|
||||
|
||||
const container = {
|
||||
manager: MockManager,
|
||||
notificationRepository,
|
||||
noti_test: {
|
||||
sendNotification: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
to: "test@mail.com",
|
||||
data: { id: "something" },
|
||||
})
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
const notificationService = new NotificationService(container)
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully calls provider and saves noti", async () => {
|
||||
await notificationService.send("event.test", { id: "test" }, "test")
|
||||
|
||||
expect(container.noti_test.sendNotification).toHaveBeenCalledTimes(1)
|
||||
expect(container.noti_test.sendNotification).toHaveBeenCalledWith(
|
||||
"event.test",
|
||||
{ id: "test" },
|
||||
null
|
||||
)
|
||||
|
||||
const constructed = {
|
||||
resource_type: "event",
|
||||
resource_id: "test",
|
||||
customer_id: null,
|
||||
to: "test@mail.com",
|
||||
data: { id: "something" },
|
||||
event_name: "event.test",
|
||||
provider_id: "test",
|
||||
}
|
||||
|
||||
expect(notificationRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(notificationRepository.create).toHaveBeenCalledWith(constructed)
|
||||
|
||||
expect(notificationRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(notificationRepository.save).toHaveBeenCalledWith(constructed)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,23 @@ describe("OrderService", () => {
|
||||
getRefundedTotal: o => {
|
||||
return o.refunded_total || 0
|
||||
},
|
||||
getShippingTotal: o => {
|
||||
return o.shipping_total || 0
|
||||
},
|
||||
getGiftCardTotal: o => {
|
||||
return o.gift_card_total || 0
|
||||
},
|
||||
getDiscountTotal: o => {
|
||||
return o.discount_total || 0
|
||||
},
|
||||
getTaxTotal: o => {
|
||||
return o.tax_total || 0
|
||||
},
|
||||
getSubtotal: o => {
|
||||
return o.subtotal || 0
|
||||
},
|
||||
}
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
@@ -380,7 +396,7 @@ describe("OrderService", () => {
|
||||
|
||||
describe("retrieve", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: q => {
|
||||
return Promise.resolve({})
|
||||
},
|
||||
})
|
||||
@@ -396,8 +412,8 @@ describe("OrderService", () => {
|
||||
|
||||
it("calls order model functions", async () => {
|
||||
await orderService.retrieve(IdMap.getId("test-order"))
|
||||
expect(orderRepo.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(orderRepo.findOne).toHaveBeenCalledWith({
|
||||
expect(orderRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
|
||||
expect(orderRepo.findOneWithRelations).toHaveBeenCalledWith(undefined, {
|
||||
where: { id: IdMap.getId("test-order") },
|
||||
})
|
||||
})
|
||||
@@ -430,7 +446,7 @@ describe("OrderService", () => {
|
||||
|
||||
describe("update", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
case IdMap.getId("fulfilled-order"):
|
||||
return Promise.resolve({
|
||||
@@ -511,7 +527,7 @@ describe("OrderService", () => {
|
||||
|
||||
describe("cancel", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
case IdMap.getId("paid-order"):
|
||||
return Promise.resolve({
|
||||
@@ -590,7 +606,7 @@ describe("OrderService", () => {
|
||||
|
||||
describe("capturePayment", () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
case IdMap.getId("fail"):
|
||||
return Promise.resolve({
|
||||
@@ -702,7 +718,7 @@ describe("OrderService", () => {
|
||||
}
|
||||
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
case "partial":
|
||||
return Promise.resolve(partialOrder)
|
||||
@@ -854,7 +870,7 @@ describe("OrderService", () => {
|
||||
payments: [{ id: "payment_test" }],
|
||||
}
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
default:
|
||||
return Promise.resolve(order)
|
||||
@@ -1027,7 +1043,7 @@ describe("OrderService", () => {
|
||||
payments: [{ id: "payment_test" }],
|
||||
}
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
default:
|
||||
return Promise.resolve(order)
|
||||
@@ -1113,7 +1129,7 @@ describe("OrderService", () => {
|
||||
}
|
||||
|
||||
const orderRepo = MockRepository({
|
||||
findOne: q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
switch (q.where.id) {
|
||||
case IdMap.getId("partial"):
|
||||
return Promise.resolve(partialOrder)
|
||||
@@ -1190,9 +1206,7 @@ describe("OrderService", () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
const orderRepo = MockRepository({
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockReturnValue(Promise.resolve({ id: IdMap.getId("order") })),
|
||||
findOneWithRelations: () => Promise.resolve({ id: IdMap.getId("order") }),
|
||||
})
|
||||
|
||||
it("fails if order/swap relationship not satisfied", async () => {
|
||||
@@ -1252,7 +1266,7 @@ describe("OrderService", () => {
|
||||
|
||||
it("registers a swap as received", async () => {
|
||||
const orderRepo = MockRepository({
|
||||
findOne: jest.fn().mockReturnValue(
|
||||
findOneWithRelations: () =>
|
||||
Promise.resolve({
|
||||
id: IdMap.getId("order_123"),
|
||||
items: [
|
||||
@@ -1262,8 +1276,7 @@ describe("OrderService", () => {
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
const swapService = {
|
||||
@@ -1313,7 +1326,7 @@ describe("OrderService", () => {
|
||||
})
|
||||
|
||||
const orderRepo = MockRepository({
|
||||
findOne: jest.fn().mockImplementation(q => {
|
||||
findOneWithRelations: (rel, q) => {
|
||||
if (q.where.id === IdMap.getId("cannot")) {
|
||||
return Promise.resolve({
|
||||
id: IdMap.getId("order"),
|
||||
@@ -1337,7 +1350,7 @@ describe("OrderService", () => {
|
||||
total: 100,
|
||||
refunded_total: 0,
|
||||
})
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const paymentProviderService = {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import ProductCollectionService from "../product-collection"
|
||||
|
||||
describe("ProductCollectionService", () => {
|
||||
describe("retrieve", () => {
|
||||
const productCollectionRepository = MockRepository({
|
||||
findOne: query => {
|
||||
if (query.where.id === "non-existing") {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.resolve({ id: IdMap.getId("bathrobe") })
|
||||
},
|
||||
})
|
||||
|
||||
const productCollectionService = new ProductCollectionService({
|
||||
manager: MockManager,
|
||||
productCollectionRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully retrieves a product collection", async () => {
|
||||
const result = await productCollectionService.retrieve(
|
||||
IdMap.getId("bathrobe")
|
||||
)
|
||||
|
||||
expect(productCollectionRepository.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(productCollectionRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: IdMap.getId("bathrobe") },
|
||||
})
|
||||
|
||||
expect(result.id).toEqual(IdMap.getId("bathrobe"))
|
||||
})
|
||||
|
||||
it("fails on non-existing product collection id", async () => {
|
||||
try {
|
||||
await productCollectionService.retrieve("non-existing")
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(
|
||||
`Product collection with id: non-existing was not found`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("create", () => {
|
||||
const productCollectionRepository = MockRepository({
|
||||
findOne: query => Promise.resolve({ id: IdMap.getId("bathrobe") }),
|
||||
})
|
||||
|
||||
const productCollectionService = new ProductCollectionService({
|
||||
manager: MockManager,
|
||||
productCollectionRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates a product collection", async () => {
|
||||
await productCollectionService.create({ title: "bathrobe" })
|
||||
|
||||
expect(productCollectionRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(productCollectionRepository.create).toHaveBeenCalledWith({
|
||||
title: "bathrobe",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("update", () => {
|
||||
const productCollectionRepository = MockRepository({
|
||||
findOne: query => {
|
||||
if (query.where.id === "non-existing") {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.resolve({ id: IdMap.getId("bathrobe") })
|
||||
},
|
||||
})
|
||||
|
||||
const productCollectionService = new ProductCollectionService({
|
||||
manager: MockManager,
|
||||
productCollectionRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully updates a product collection", async () => {
|
||||
await productCollectionService.update(IdMap.getId("bathrobe"), {
|
||||
title: "bathrobes",
|
||||
})
|
||||
|
||||
expect(productCollectionRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(productCollectionRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("bathrobe"),
|
||||
title: "bathrobes",
|
||||
})
|
||||
})
|
||||
|
||||
it("fails on non-existing product collection", async () => {
|
||||
try {
|
||||
await productCollectionService.update(IdMap.getId("test"), {
|
||||
title: "bathrobes",
|
||||
})
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(
|
||||
`Product collection with id: ${IdMap.getId("test")} was not found`
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe("delete", () => {
|
||||
const productCollectionRepository = MockRepository({
|
||||
findOne: query => {
|
||||
if (query.where.id === "non-existing") {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.resolve({ id: IdMap.getId("bathrobe") })
|
||||
},
|
||||
})
|
||||
|
||||
const productCollectionService = new ProductCollectionService({
|
||||
manager: MockManager,
|
||||
productCollectionRepository,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully removes a product collection", async () => {
|
||||
await productCollectionService.delete(IdMap.getId("bathrobe"))
|
||||
|
||||
expect(productCollectionRepository.remove).toHaveBeenCalledTimes(1)
|
||||
expect(productCollectionRepository.remove).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("bathrobe"),
|
||||
})
|
||||
})
|
||||
|
||||
it("succeeds idempotently", async () => {
|
||||
const result = await productCollectionService.delete(IdMap.getId("test"))
|
||||
expect(result).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -36,13 +36,54 @@ describe("ProductService", () => {
|
||||
|
||||
describe("create", () => {
|
||||
const productRepository = MockRepository({
|
||||
create: () =>
|
||||
Promise.resolve({ id: IdMap.getId("ironman"), title: "Suit" }),
|
||||
create: () => ({
|
||||
id: IdMap.getId("ironman"),
|
||||
title: "Suit",
|
||||
options: [],
|
||||
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
||||
}),
|
||||
findOne: () => ({
|
||||
id: IdMap.getId("ironman"),
|
||||
title: "Suit",
|
||||
options: [],
|
||||
collection: { id: IdMap.getId("cat"), title: "Suits" },
|
||||
}),
|
||||
})
|
||||
|
||||
const productTagRepository = MockRepository({
|
||||
findOne: () => Promise.resolve(undefined),
|
||||
create: data => {
|
||||
if (data.value === "title") {
|
||||
return { id: "tag-1", value: "title" }
|
||||
}
|
||||
|
||||
if (data.value === "title2") {
|
||||
return { id: "tag-2", value: "title2" }
|
||||
}
|
||||
},
|
||||
})
|
||||
const productTypeRepository = MockRepository({
|
||||
findOne: () => Promise.resolve(undefined),
|
||||
create: data => {
|
||||
return { id: "type", value: "type1" }
|
||||
},
|
||||
})
|
||||
|
||||
const productCollectionService = {
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
retrieve: id =>
|
||||
Promise.resolve({ id: IdMap.getId("cat"), title: "Suits" }),
|
||||
}
|
||||
|
||||
const productService = new ProductService({
|
||||
manager: MockManager,
|
||||
productRepository,
|
||||
eventBusService,
|
||||
productCollectionService,
|
||||
productTagRepository,
|
||||
productTypeRepository,
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -50,9 +91,11 @@ describe("ProductService", () => {
|
||||
})
|
||||
|
||||
it("successfully create a product", async () => {
|
||||
const result = await productService.create({
|
||||
await productService.create({
|
||||
title: "Suit",
|
||||
options: [],
|
||||
tags: [{ value: "title" }, { value: "title2" }],
|
||||
type: "type-1",
|
||||
})
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
@@ -64,15 +107,30 @@ describe("ProductService", () => {
|
||||
expect(productRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(productRepository.create).toHaveBeenCalledWith({
|
||||
title: "Suit",
|
||||
options: [],
|
||||
})
|
||||
|
||||
expect(productRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(productTagRepository.findOne).toHaveBeenCalledTimes(2)
|
||||
// We add two tags, that does not exist therefore we make sure
|
||||
// that create is also called
|
||||
expect(productTagRepository.create).toHaveBeenCalledTimes(2)
|
||||
|
||||
expect(result).toEqual({
|
||||
expect(productTypeRepository.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(productTypeRepository.create).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(productRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(productRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("ironman"),
|
||||
title: "Suit",
|
||||
options: [],
|
||||
tags: [
|
||||
{ id: "tag-1", value: "title" },
|
||||
{ id: "tag-2", value: "title2" },
|
||||
],
|
||||
type_id: "type",
|
||||
collection: {
|
||||
id: IdMap.getId("cat"),
|
||||
title: "Suits",
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -93,6 +151,13 @@ describe("ProductService", () => {
|
||||
},
|
||||
})
|
||||
|
||||
const productTypeRepository = MockRepository({
|
||||
findOne: () => Promise.resolve(undefined),
|
||||
create: data => {
|
||||
return { id: "type", value: "type1" }
|
||||
},
|
||||
})
|
||||
|
||||
const productVariantRepository = MockRepository()
|
||||
|
||||
const productVariantService = {
|
||||
@@ -102,11 +167,24 @@ describe("ProductService", () => {
|
||||
update: () => Promise.resolve(),
|
||||
}
|
||||
|
||||
const productTagRepository = MockRepository({
|
||||
findOne: data => {
|
||||
if (data.where.value === "test") {
|
||||
return Promise.resolve({ id: IdMap.getId("test"), value: "test" })
|
||||
}
|
||||
if (data.where.value === "test2") {
|
||||
return Promise.resolve({ id: IdMap.getId("test2"), value: "test2" })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const productService = new ProductService({
|
||||
manager: MockManager,
|
||||
productRepository,
|
||||
productVariantService,
|
||||
productVariantRepository,
|
||||
productTagRepository,
|
||||
productTypeRepository,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
@@ -146,6 +224,10 @@ describe("ProductService", () => {
|
||||
it("successfully updates product", async () => {
|
||||
await productService.update(IdMap.getId("ironman"), {
|
||||
title: "Full suit",
|
||||
collection: {
|
||||
id: IdMap.getId("test"),
|
||||
value: "test",
|
||||
},
|
||||
})
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
@@ -158,6 +240,34 @@ describe("ProductService", () => {
|
||||
expect(productRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("ironman"),
|
||||
title: "Full suit",
|
||||
collection: {
|
||||
id: IdMap.getId("test"),
|
||||
value: "test",
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it("successfully updates tags", async () => {
|
||||
await productService.update(IdMap.getId("ironman"), {
|
||||
tags: [
|
||||
{ id: IdMap.getId("test"), value: "test" },
|
||||
{ id: IdMap.getId("test2"), value: "test2" },
|
||||
],
|
||||
})
|
||||
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
||||
"product.updated",
|
||||
expect.any(Object)
|
||||
)
|
||||
|
||||
expect(productRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(productRepository.save).toHaveBeenCalledWith({
|
||||
id: IdMap.getId("ironman"),
|
||||
tags: [
|
||||
{ id: IdMap.getId("test"), value: "test" },
|
||||
{ id: IdMap.getId("test2"), value: "test2" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import SwapService from "../swap"
|
||||
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const generateOrder = (orderId, items, additional = {}) => {
|
||||
return {
|
||||
id: IdMap.getId(orderId),
|
||||
@@ -70,7 +77,9 @@ describe("SwapService", () => {
|
||||
})
|
||||
|
||||
it("fails if item is returned", async () => {
|
||||
const swapService = new SwapService({})
|
||||
const swapService = new SwapService({
|
||||
eventBusService,
|
||||
})
|
||||
const res = () =>
|
||||
swapService.validateReturnItems_(
|
||||
{
|
||||
@@ -168,6 +177,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
cartService,
|
||||
lineItemService,
|
||||
@@ -236,6 +246,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
})
|
||||
const res = swapService.createCart(IdMap.getId("swap-1"))
|
||||
@@ -274,6 +285,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
lineItemService,
|
||||
@@ -358,6 +370,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
})
|
||||
@@ -401,6 +414,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
returnService,
|
||||
})
|
||||
@@ -475,6 +489,7 @@ describe("SwapService", () => {
|
||||
})
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
fulfillmentService,
|
||||
lineItemService,
|
||||
@@ -595,6 +610,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
@@ -691,6 +707,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
totalsService,
|
||||
paymentProviderService,
|
||||
@@ -770,6 +787,7 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
|
||||
@@ -99,7 +99,7 @@ class ClaimService extends BaseService {
|
||||
const { claim_items, shipping_methods, metadata } = data
|
||||
|
||||
if (metadata) {
|
||||
claim.metadata = this.setMetadata_(claim, update.metadata)
|
||||
claim.metadata = this.setMetadata_(claim, metadata)
|
||||
await claimRepo.save(claim)
|
||||
}
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ class CustomerService extends BaseService {
|
||||
const token = jwt.sign(payload, secret)
|
||||
// Notify subscribers
|
||||
this.eventBus_.emit(CustomerService.Events.PASSWORD_RESET, {
|
||||
id: customerId,
|
||||
email: customer.email,
|
||||
first_name: customer.first_name,
|
||||
last_name: customer.last_name,
|
||||
@@ -320,6 +321,7 @@ class CustomerService extends BaseService {
|
||||
|
||||
const {
|
||||
email,
|
||||
password,
|
||||
password_hash,
|
||||
billing_address,
|
||||
metadata,
|
||||
@@ -342,6 +344,10 @@ class CustomerService extends BaseService {
|
||||
customer[key] = value
|
||||
}
|
||||
|
||||
if (password) {
|
||||
customer.password_hash = await this.hashPassword_(password)
|
||||
}
|
||||
|
||||
const updated = await customerRepository.save(customer)
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
|
||||
@@ -217,14 +217,18 @@ class EventBusService {
|
||||
*/
|
||||
worker_ = job => {
|
||||
const { eventName, data } = job.data
|
||||
const observers = this.observers_[eventName] || []
|
||||
const eventObservers = this.observers_[eventName] || []
|
||||
const wildcardObservers = this.observers_["*"] || []
|
||||
|
||||
const observers = eventObservers.concat(wildcardObservers)
|
||||
|
||||
this.logger_.info(
|
||||
`Processing ${eventName} which has ${observers.length} subscribers`
|
||||
`Processing ${eventName} which has ${eventObservers.length} subscribers`
|
||||
)
|
||||
|
||||
return Promise.all(
|
||||
observers.map(subscriber => {
|
||||
return subscriber(data).catch(err => {
|
||||
return subscriber(data, eventName).catch(err => {
|
||||
this.logger_.warn(
|
||||
`An error occured while processing ${eventName}: ${err}`
|
||||
)
|
||||
@@ -242,7 +246,7 @@ class EventBusService {
|
||||
|
||||
return Promise.all(
|
||||
observers.map(subscriber => {
|
||||
return subscriber(data).catch(err => {
|
||||
return subscriber(data, eventName).catch(err => {
|
||||
this.logger_.warn(
|
||||
`An error occured while processing ${eventName}: ${err}`
|
||||
)
|
||||
|
||||
@@ -89,6 +89,18 @@ class FulfillmentProviderService {
|
||||
const provider = this.retrieveProvider(option.provider_id)
|
||||
return provider.createReturn(returnOrder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches documents from the fulfillment provider
|
||||
* @param {string} providerId - the id of the provider
|
||||
* @param {object} fulfillmentData - the data relating to the fulfillment
|
||||
* @param {"invoice" | "label"} documentType - the typ of
|
||||
* document to fetch
|
||||
*/
|
||||
async retrieveDocuments(providerId, fulfillmentData, documentType) {
|
||||
const provider = this.retrieveProvider(providerId)
|
||||
return provider.retrieveDocuments(fulfillmentData, documentType)
|
||||
}
|
||||
}
|
||||
|
||||
export default FulfillmentProviderService
|
||||
@@ -0,0 +1,254 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import _ from "lodash"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate orchestrate notifications.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class NotificationService extends BaseService {
|
||||
constructor(container) {
|
||||
super()
|
||||
|
||||
const {
|
||||
manager,
|
||||
notificationProviderRepository,
|
||||
notificationRepository,
|
||||
logger,
|
||||
} = container
|
||||
|
||||
this.container_ = container
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
this.logger_ = logger
|
||||
|
||||
/** @private @const {NotificationRepository} */
|
||||
this.notificationRepository_ = notificationRepository
|
||||
this.notificationProviderRepository_ = notificationProviderRepository
|
||||
|
||||
this.subscribers_ = {}
|
||||
this.attachmentGenerator_ = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an attachment generator to the service. The generator can be
|
||||
* used to generate on demand invoices or other documents.
|
||||
*/
|
||||
registerAttachmentGenerator(service) {
|
||||
this.attachmentGenerator_ = service
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the service's manager to a given transaction manager.
|
||||
* @parma {EntityManager} transactionManager - the manager to use
|
||||
* return {NotificationService} a cloned notification service
|
||||
*/
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new LineItemService({
|
||||
manager: transactionManager,
|
||||
notificationRepository: this.notificationRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a list of notification provider ids and persists them in the database.
|
||||
* @param {Array<string>} providers - a list of provider ids
|
||||
*/
|
||||
async registerInstalledProviders(providers) {
|
||||
const { manager, notificationProviderRepository } = this.container_
|
||||
const model = manager.getCustomRepository(notificationProviderRepository)
|
||||
model.update({}, { is_installed: false })
|
||||
for (const p of providers) {
|
||||
const n = model.create({ id: p, is_installed: true })
|
||||
await model.save(n)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of notifications.
|
||||
* @param {object} selector - the params to select the notifications by.
|
||||
* @param {object} config - the configuration to apply to the query
|
||||
* @return {Array<Notification>} the notifications that satisfy the query.
|
||||
*/
|
||||
async list(
|
||||
selector,
|
||||
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
|
||||
) {
|
||||
const notiRepo = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return notiRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a notification with a given id
|
||||
* @param {string} id - the id of the notification
|
||||
* @return {Notification} the notification
|
||||
*/
|
||||
async retrieve(id, config = {}) {
|
||||
const notiRepository = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
|
||||
const validatedId = this.validateId_(id)
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
|
||||
const notification = await notiRepository.findOne(query)
|
||||
|
||||
if (!notification) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Notification with id: ${id} was not found.`
|
||||
)
|
||||
}
|
||||
|
||||
return notification
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a given provider to an event.
|
||||
* @param {string} eventName - the event to subscribe to
|
||||
* @param {string} providerId - the provider that the event will be sent to
|
||||
*/
|
||||
subscribe(eventName, providerId) {
|
||||
if (typeof providerId !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"providerId must be a string"
|
||||
)
|
||||
}
|
||||
|
||||
if (this.subscribers_[eventName]) {
|
||||
this.subscribers_[eventName].push(providerId)
|
||||
} else {
|
||||
this.subscribers_[eventName] = [providerId]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a provider with a given id. Will throw a NOT_FOUND error if the
|
||||
* resolution fails.
|
||||
* @param {string} id - the id of the provider
|
||||
* @return {NotificationProvider} the notification provider
|
||||
*/
|
||||
retrieveProvider_(id) {
|
||||
try {
|
||||
return this.container_[`noti_${id}`]
|
||||
} catch (err) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Could not find a notification provider with id: ${id}.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an event by relaying the event data to the subscribing providers.
|
||||
* The result of the notification send will be persisted in the database in
|
||||
* order to allow for resends. Will log any errors that are encountered.
|
||||
* @param {string} eventName - the event to handle
|
||||
* @param {object} data - the data the event was sent with
|
||||
*/
|
||||
handleEvent(eventName, data) {
|
||||
const subs = this.subscribers_[eventName]
|
||||
if (!subs) {
|
||||
return
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
subs.map(async providerId => {
|
||||
return this.send(eventName, data, providerId).catch(err => {
|
||||
console.log(err)
|
||||
this.logger_.warn(
|
||||
`An error occured while ${providerId} was processing a notification for ${eventName}: ${err.message}`
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a notification, by calling the given provider's sendNotification
|
||||
* method. Persists the Notification in the database.
|
||||
* @param {string} event - the name of the event
|
||||
* @param {object} eventData - the data the event was sent with
|
||||
* @param {string} providerId - the provider that should hande the event.
|
||||
* @return {Notification} the created notification
|
||||
*/
|
||||
async send(event, eventData, providerId) {
|
||||
const provider = this.retrieveProvider_(providerId)
|
||||
const result = await provider.sendNotification(
|
||||
event,
|
||||
eventData,
|
||||
this.attachmentGenerator_
|
||||
)
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
const { to, data } = result
|
||||
const notiRepo = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
|
||||
const [resource_type] = event.split(".")
|
||||
const resource_id = eventData.id
|
||||
const customer_id = eventData.customer_id || null
|
||||
|
||||
const created = notiRepo.create({
|
||||
resource_type,
|
||||
resource_id,
|
||||
customer_id,
|
||||
to,
|
||||
data,
|
||||
event_name: event,
|
||||
provider_id: providerId,
|
||||
})
|
||||
|
||||
return notiRepo.save(created)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resends a notification by retrieving a prior notification and calling the
|
||||
* underlying provider's resendNotification method.
|
||||
* @param {string} id - the id of the notification
|
||||
* @param {object} config - any configuration that might override the previous
|
||||
* send
|
||||
* @return {Notification} the newly created notification
|
||||
*/
|
||||
async resend(id, config = {}) {
|
||||
const notification = await this.retrieve(id)
|
||||
|
||||
const provider = this.retrieveProvider_(notification.provider_id)
|
||||
const { to, data } = await provider.resendNotification(
|
||||
notification,
|
||||
config,
|
||||
this.attachmentGenerator_
|
||||
)
|
||||
|
||||
const notiRepo = this.manager_.getCustomRepository(
|
||||
this.notificationRepository_
|
||||
)
|
||||
const created = notiRepo.create({
|
||||
...notification,
|
||||
to,
|
||||
data,
|
||||
parent_id: id,
|
||||
})
|
||||
|
||||
return notiRepo.save(created)
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationService
|
||||
@@ -328,7 +328,7 @@ class OrderService extends BaseService {
|
||||
query.select = select
|
||||
}
|
||||
|
||||
const raw = await orderRepo.findOne(query)
|
||||
const raw = await orderRepo.findOneWithRelations(query.relations, query)
|
||||
|
||||
if (!raw) {
|
||||
throw new MedusaError(
|
||||
@@ -904,11 +904,13 @@ class OrderService extends BaseService {
|
||||
|
||||
const result = await orderRepo.save(order)
|
||||
|
||||
this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(OrderService.Events.PAYMENT_CAPTURED, {
|
||||
id: result.id,
|
||||
})
|
||||
if (order.payment_status === "captured") {
|
||||
this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(OrderService.Events.PAYMENT_CAPTURED, {
|
||||
id: result.id,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
@@ -956,7 +958,16 @@ class OrderService extends BaseService {
|
||||
async createFulfillment(orderId, itemsToFulfill, metadata = {}) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const order = await this.retrieve(orderId, {
|
||||
select: [
|
||||
"subtotal",
|
||||
"shipping_total",
|
||||
"discount_total",
|
||||
"tax_total",
|
||||
"gift_card_total",
|
||||
"total",
|
||||
],
|
||||
relations: [
|
||||
"discounts",
|
||||
"region",
|
||||
"fulfillments",
|
||||
"shipping_address",
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Provides layer to manipulate product collections.
|
||||
* @implements BaseService
|
||||
*/
|
||||
class ProductCollectionService extends BaseService {
|
||||
constructor({
|
||||
manager,
|
||||
productCollectionRepository,
|
||||
productRepository,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @const {ProductCollectionRepository} */
|
||||
this.productCollectionRepository_ = productCollectionRepository
|
||||
|
||||
/** @private @const {ProductRepository} */
|
||||
this.productRepository_ = productRepository
|
||||
|
||||
/** @private @const {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ProductCollectionService({
|
||||
manager: transactionManager,
|
||||
productCollectionRepository: this.productCollectionRepository_,
|
||||
productRepository: this.productRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a product collection by id.
|
||||
* @param {string} collectionId - the id of the collection to retrieve.
|
||||
* @return {Promise<ProductCollection>} the collection.
|
||||
*/
|
||||
async retrieve(collectionId, config = {}) {
|
||||
const collectionRepo = this.manager_.getCustomRepository(
|
||||
this.productCollectionRepository_
|
||||
)
|
||||
|
||||
const validatedId = this.validateId_(collectionId)
|
||||
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
const collection = await collectionRepo.findOne(query)
|
||||
|
||||
if (!collection) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Product collection with id: ${collectionId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a product collection
|
||||
* @param {object} collection - the collection to create
|
||||
* @return {Promise<ProductCollection>} created collection
|
||||
*/
|
||||
async create(collection) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const collectionRepo = manager.getCustomRepository(
|
||||
this.productCollectionRepository_
|
||||
)
|
||||
|
||||
const productCollection = collectionRepo.create(collection)
|
||||
return collectionRepo.save(productCollection)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a product collection
|
||||
* @param {string} collectionId - id of collection to update
|
||||
* @param {object} update - update object
|
||||
* @return {Promise<ProductCollection>} update collection
|
||||
*/
|
||||
async update(collectionId, update) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const collectionRepo = manager.getCustomRepository(
|
||||
this.productCollectionRepository_
|
||||
)
|
||||
|
||||
const collection = await this.retrieve(collectionId)
|
||||
|
||||
const { metadata, ...rest } = update
|
||||
|
||||
if (metadata) {
|
||||
collection.metadata = this.setMetadata_(collection, metadata)
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(rest)) {
|
||||
collection[key] = value
|
||||
}
|
||||
|
||||
return collectionRepo.save(collection)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a product collection idempotently
|
||||
* @param {string} collectionId - id of collection to delete
|
||||
* @return {Promise} empty promise
|
||||
*/
|
||||
async delete(collectionId) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const productCollectionRepo = manager.getCustomRepository(
|
||||
this.productCollectionRepository_
|
||||
)
|
||||
|
||||
const collection = await this.retrieve(collectionId)
|
||||
|
||||
if (!collection) return Promise.resolve()
|
||||
|
||||
await productCollectionRepo.remove(collection)
|
||||
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists product collections
|
||||
* @param {Object} selector - the query object for find
|
||||
* @return {Promise} the result of the find operation
|
||||
*/
|
||||
async list(selector = {}, config = { skip: 0, take: 20 }) {
|
||||
const productCollectionRepo = this.manager_.getCustomRepository(
|
||||
this.productCollectionRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return productCollectionRepo.find(query)
|
||||
}
|
||||
}
|
||||
|
||||
export default ProductCollectionService
|
||||
@@ -20,6 +20,9 @@ class ProductService extends BaseService {
|
||||
productOptionRepository,
|
||||
eventBusService,
|
||||
productVariantService,
|
||||
productCollectionService,
|
||||
productTypeRepository,
|
||||
productTagRepository,
|
||||
}) {
|
||||
super()
|
||||
|
||||
@@ -40,6 +43,15 @@ class ProductService extends BaseService {
|
||||
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.productCollectionService_ = productCollectionService
|
||||
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.productTypeRepository_ = productTypeRepository
|
||||
|
||||
/** @private @const {ProductCollectionService} */
|
||||
this.productTagRepository_ = productTagRepository
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
@@ -54,6 +66,9 @@ class ProductService extends BaseService {
|
||||
productOptionRepository: this.productOptionRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
productVariantService: this.productVariantService_,
|
||||
productCollectionService: this.productCollectionService_,
|
||||
productTagRepository: this.productTagRepository_,
|
||||
productTypeRepository: this.productTypeRepository_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
@@ -88,6 +103,7 @@ class ProductService extends BaseService {
|
||||
alias: "product",
|
||||
leftJoinAndSelect: {
|
||||
variant: "product.variants",
|
||||
collection: "product.collection",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -100,6 +116,7 @@ class ProductService extends BaseService {
|
||||
.orWhere(`product.description ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` })
|
||||
.orWhere(`collection.title ILIKE :q`, { q: `%${q}%` })
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -152,6 +169,78 @@ class ProductService extends BaseService {
|
||||
return product.variants
|
||||
}
|
||||
|
||||
async listTypes() {
|
||||
const productTypeRepository = this.manager_.getCustomRepository(
|
||||
this.productTypeRepository_
|
||||
)
|
||||
|
||||
return await productTypeRepository.find({})
|
||||
}
|
||||
|
||||
async listTagsByUsage(count = 10) {
|
||||
const tags = await this.manager_.query(
|
||||
`
|
||||
SELECT ID, O.USAGE_COUNT, PT.VALUE
|
||||
FROM PRODUCT_TAG PT
|
||||
LEFT JOIN
|
||||
(SELECT COUNT(*) AS USAGE_COUNT,
|
||||
PRODUCT_TAG_ID
|
||||
FROM PRODUCT_TAGS
|
||||
GROUP BY PRODUCT_TAG_ID) O ON O.PRODUCT_TAG_ID = PT.ID
|
||||
ORDER BY O.USAGE_COUNT DESC
|
||||
LIMIT $1`,
|
||||
[count]
|
||||
)
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
async upsertProductType_(type) {
|
||||
const productTypeRepository = this.manager_.getCustomRepository(
|
||||
this.productTypeRepository_
|
||||
)
|
||||
|
||||
if (type === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
const existing = await productTypeRepository.findOne({
|
||||
where: { value: type.value },
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
const created = productTypeRepository.create(type)
|
||||
const result = await productTypeRepository.save(created)
|
||||
|
||||
return result.id
|
||||
}
|
||||
|
||||
async upsertProductTags_(tags) {
|
||||
const productTagRepository = this.manager_.getCustomRepository(
|
||||
this.productTagRepository_
|
||||
)
|
||||
|
||||
let newTags = []
|
||||
for (const tag of tags) {
|
||||
const existing = await productTagRepository.findOne({
|
||||
where: { value: tag.value },
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
newTags.push(existing)
|
||||
} else {
|
||||
const created = productTagRepository.create(tag)
|
||||
const result = await productTagRepository.save(created)
|
||||
newTags.push(result)
|
||||
}
|
||||
}
|
||||
|
||||
return newTags
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a product.
|
||||
* @param {object} productObject - the product to create
|
||||
@@ -164,17 +253,29 @@ class ProductService extends BaseService {
|
||||
this.productOptionRepository_
|
||||
)
|
||||
|
||||
const product = await productRepo.create(productObject)
|
||||
const { options, tags, type, ...rest } = productObject
|
||||
|
||||
let product = productRepo.create(rest)
|
||||
|
||||
if (tags) {
|
||||
product.tags = await this.upsertProductTags_(tags)
|
||||
}
|
||||
|
||||
if (typeof type !== `undefined`) {
|
||||
product.type_id = await this.upsertProductType_(type)
|
||||
}
|
||||
|
||||
product = await productRepo.save(product)
|
||||
|
||||
product.options = await Promise.all(
|
||||
productObject.options.map(async o => {
|
||||
const res = await optionRepo.create({ ...o, product_id: product.id })
|
||||
options.map(async o => {
|
||||
const res = optionRepo.create({ ...o, product_id: product.id })
|
||||
await optionRepo.save(res)
|
||||
return res
|
||||
})
|
||||
)
|
||||
|
||||
const result = await productRepo.save(product)
|
||||
const result = await this.retrieve(product.id, { relations: ["options"] })
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
@@ -202,10 +303,18 @@ class ProductService extends BaseService {
|
||||
)
|
||||
|
||||
const product = await this.retrieve(productId, {
|
||||
relations: ["variants"],
|
||||
relations: ["variants", "tags"],
|
||||
})
|
||||
|
||||
const { variants, metadata, options, images, ...rest } = update
|
||||
const {
|
||||
variants,
|
||||
metadata,
|
||||
options,
|
||||
images,
|
||||
tags,
|
||||
type,
|
||||
...rest
|
||||
} = update
|
||||
|
||||
if (!product.thumbnail && !update.thumbnail && images && images.length) {
|
||||
product.thumbnail = images[0]
|
||||
@@ -215,6 +324,14 @@ class ProductService extends BaseService {
|
||||
product.metadata = this.setMetadata_(product, metadata)
|
||||
}
|
||||
|
||||
if (typeof type !== `undefined`) {
|
||||
product.type_id = await this.upsertProductType_(type)
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
product.tags = await this.upsertProductTags_(tags)
|
||||
}
|
||||
|
||||
if (variants) {
|
||||
// Iterate product variants and update their properties accordingly
|
||||
for (const variant of product.variants) {
|
||||
@@ -321,7 +438,7 @@ class ProductService extends BaseService {
|
||||
product_id: productId,
|
||||
})
|
||||
|
||||
const result = await productOptionRepo.save(option)
|
||||
await productOptionRepo.save(option)
|
||||
|
||||
for (const variant of product.variants) {
|
||||
this.productVariantService_
|
||||
@@ -329,6 +446,8 @@ class ProductService extends BaseService {
|
||||
.addOptionValue(variant.id, option.id, "Default Value")
|
||||
}
|
||||
|
||||
const result = await this.retrieve(productId)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ProductService.Events.UPDATED, result)
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MedusaError } from "medusa-core-utils"
|
||||
*/
|
||||
class SwapService extends BaseService {
|
||||
static Events = {
|
||||
CREATED: "swap.created",
|
||||
SHIPMENT_CREATED: "swap.shipment_created",
|
||||
PAYMENT_COMPLETED: "swap.payment_completed",
|
||||
PAYMENT_CAPTURED: "swap.payment_captured",
|
||||
@@ -247,6 +248,12 @@ class SwapService extends BaseService {
|
||||
order
|
||||
)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(SwapService.Events.CREATED, {
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,7 +94,10 @@ class TotalsService extends BaseService {
|
||||
|
||||
getLineItemRefund(object, lineItem) {
|
||||
const { discounts } = object
|
||||
const tax_rate = object.tax_rate || object.region.tax_rate
|
||||
const tax_rate =
|
||||
typeof object.tax_rate !== "undefined"
|
||||
? object.tax_rate
|
||||
: object.region.tax_rate
|
||||
const taxRate = (tax_rate || 0) / 100
|
||||
|
||||
const discount = discounts.find(({ rule }) => rule.type !== "free_shipping")
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class NotificationSubscriber {
|
||||
constructor({ eventBusService, notificationService }) {
|
||||
this.notificationService_ = notificationService
|
||||
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
this.eventBus_.subscribe("*", this.onEvent)
|
||||
}
|
||||
|
||||
onEvent = (data, eventName) => {
|
||||
return this.notificationService_.handleEvent(eventName, data)
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationSubscriber
|
||||
Reference in new issue
Block a user