fix merge conflicts

This commit is contained in:
olivermrbl committed 2021-02-17 17:24:34 +01:00
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",
]
+6 -3
View File
@@ -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"
+6
View File
@@ -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")
+15
View File
@@ -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
+38
View File
@@ -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}`
}
}
+36 -3
View File
@@ -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> {}
+53 -2
View File
@@ -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)
})
})
})
+31 -18
View File
@@ -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" },
],
})
})
+19 -1
View File
@@ -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,
+1 -1
View File
@@ -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)
}
+6
View File
@@ -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)
+8 -4
View File
@@ -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
+17 -6
View File
@@ -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
+126 -7
View File
@@ -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)
+7
View File
@@ -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
})
}
+4 -1
View File
@@ -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