fix merge conflicts

This commit is contained in:
olivermrbl
2021-02-27 10:22:33 +01:00
31 changed files with 845 additions and 118 deletions
+93 -33
View File
@@ -1,36 +1,96 @@
# medusa # Medusa
Medusa Monorepo
Medusa is a headless commerce engine built with Node.js using Express with a Postgresql database.
# MVP Roadmap (!) ## Documentation
- Finish core See [Medusa Commerce API docs](https://docs.medusa-commerce.com/api/store/) for Node.js.
- Services
- [x] Auth ## Get started in less than 5 minutes
- [x] Product
- [x] Product Variant You can get a Medusa engine up and running in your local development environment within a couple of minutes. Perform the following steps:
- [x] User
- [ ] Region 1. Install Medusa, the Medusa CLI, Medusa babel preset and Medusa interfaces
- [ ] Cart
- [x] Customer ```bash
- [ ] Order # core medusa
- [ ] Promo Code npm install @medusajs/medusa
- [ ] Gift cards yarn add @medusajs/medusa
- REST API controllers
- Admin # CLI
- Store npm install -g @medusa/medusa-cli
- Core plugins yarn add global @medusajs/medusa-cli
- Payment Providers
- [ ] Stripe # babel preset
- [ ] Klarna npm install babel-preset-medusa-package
- [ ] PayPal yarn add babel-preset-medusa-package
- [ ] QuickPay
- Fulfillment providers # interfaces
- [ ] E-conomic npm install medusa-interfaces
- [ ] Brightpearl yarn add medusa-interfaces
- [ ] Shipmondo ```
- [ ] Webshipper 2. Create a file `medusa-config.js` at the root level of your Node.js project and fill in required settings
- Transactional Emails ```
- [ ] Twilio SendGrid // CORS to avoid issues when consuming Medusa from a client
- Role Based Permission const STORE_CORS = "http://localhost:8000";
- Contentful sync plugin (to send SKUs to Contentful for content enrichment)
// Database URL (here we use a local database called medusa-development)
const DATABASE_URL = "postgres://localhost/medusa-development";
// Medusa uses Redis, so this needs configuration as well
const REDIS_URL = "redis://localhost:6379"
// This is the place to include plugins. See API documentation for a thorough guide on plugins.
const plugins = [];
module.exports = {
projectConfig: {
redis_url: REDIS_URL,
database_url: DATABASE_URL,
database_logging: true,
database_extra:
process.env.NODE_ENV === "production" ||
process.env.NODE_ENV === "staging"
? {
ssl: { rejectUnauthorized: false },
}
: {},
database_type: "postgres",
store_cors: STORE_CORS,
},
plugins,
};
```
3. Create a Medusa user, such that you can perform authenticated calls
```bash
# provide email and password to the command
medusa user -e lebron@james.com -p lebronjames123
```
4. Start your Medusa engine in your local environment
```bash
medusa develop
```
5. Open any client or API tool to start using your Medusa engine
Medusa is running at `http://localhost:4000`. You should now investigate our [API docs](https://docs.medusa-commerce.com/api/store/) to start playing around with your new headless commerce engine.
After these four steps and only a couple of minutes, you now have a complete commerce engine running locally.
## Contribution
Medusa is all about the community. Therefore, we would love for you to help us build the most robust and powerful commerce engine on the market. Whether its fixing bugs, improving our documentation or simply spreading the word, please feel free to join in.
## Repository structure
The Medusa repository is a mono-repository managed using Lerna. Lerna allows us to have all Medusa packages in one place, and still distribute them as separate NPM packages.
## Licensed
Licended under the [MIT License](https://github.com/medusajs/medusa/blob/master/LICENSE)
## Thank you!
@@ -0,0 +1,135 @@
const { dropDatabase } = require("pg-god");
const path = require("path");
const setupServer = require("../../../helpers/setup-server");
const { useApi } = require("../../../helpers/use-api");
const { initDb } = require("../../../helpers/use-db");
const customerSeeder = require("../../helpers/customer-seeder");
const adminSeeder = require("../../helpers/admin-seeder");
jest.setTimeout(30000);
describe("/admin/customers", () => {
let medusaProcess;
let dbConnection;
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."));
dbConnection = await initDb({ cwd });
medusaProcess = await setupServer({ cwd });
});
afterAll(async () => {
await dbConnection.close();
await dropDatabase({ databaseName: "medusa-integration" });
medusaProcess.kill();
});
describe("GET /admin/customers", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection);
await customerSeeder(dbConnection);
} catch (err) {
console.log(err);
throw err;
}
});
afterEach(async () => {
const manager = dbConnection.manager;
await manager.query(`DELETE FROM "address"`);
await manager.query(`DELETE FROM "customer"`);
await manager.query(`DELETE FROM "user"`);
});
it("lists customers and query count", async () => {
const api = useApi();
const response = await api
.get("/admin/customers", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
expect(response.data.count).toEqual(3);
expect(response.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "test-customer-1",
}),
expect.objectContaining({
id: "test-customer-2",
}),
expect.objectContaining({
id: "test-customer-3",
}),
])
);
});
it("lists customers with specific query", async () => {
const api = useApi();
const response = await api
.get("/admin/customers?q=test2@email.com", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
expect(response.data.count).toEqual(1);
expect(response.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "test-customer-2",
email: "test2@email.com",
}),
])
);
});
it("lists customers with expand query", async () => {
const api = useApi();
const response = await api
.get("/admin/customers?q=test1@email.com&expand=shipping_addresses", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err);
});
expect(response.status).toEqual(200);
expect(response.data.count).toEqual(1);
console.log(response.data.customers);
expect(response.data.customers).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "test-customer-1",
shipping_addresses: expect.arrayContaining([
expect.objectContaining({
id: "test-address",
first_name: "Lebron",
last_name: "James",
}),
]),
}),
])
);
});
});
});
@@ -0,0 +1,27 @@
const { Customer, Address } = require("@medusajs/medusa");
module.exports = async (connection, data = {}) => {
const manager = connection.manager;
await manager.insert(Customer, {
id: "test-customer-1",
email: "test1@email.com",
});
await manager.insert(Customer, {
id: "test-customer-2",
email: "test2@email.com",
});
await manager.insert(Customer, {
id: "test-customer-3",
email: "test3@email.com",
});
await manager.insert(Address, {
id: "test-address",
first_name: "Lebron",
last_name: "James",
customer_id: "test-customer-1",
});
};
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.3](https://github.com/medusajs/medusa/compare/medusa-fulfillment-webshipper@1.1.3-next.0...medusa-fulfillment-webshipper@1.1.3) (2021-02-25)
**Note:** Version bump only for package medusa-fulfillment-webshipper
## [1.1.3-next.0](https://github.com/medusajs/medusa/compare/medusa-fulfillment-webshipper@1.1.2...medusa-fulfillment-webshipper@1.1.3-next.0) (2021-02-22)
### Features
* **medusa:** tracking links ([#177](https://github.com/medusajs/medusa/issues/177)) ([99ad43b](https://github.com/medusajs/medusa/commit/99ad43bf47c3922f391d433448b1c4affd88f457))
## [1.1.2](https://github.com/medusajs/medusa/compare/medusa-fulfillment-webshipper@1.1.1...medusa-fulfillment-webshipper@1.1.2) (2021-02-17) ## [1.1.2](https://github.com/medusajs/medusa/compare/medusa-fulfillment-webshipper@1.1.1...medusa-fulfillment-webshipper@1.1.2) (2021-02-17)
@@ -1,6 +1,6 @@
{ {
"name": "medusa-fulfillment-webshipper", "name": "medusa-fulfillment-webshipper",
"version": "1.1.2", "version": "1.1.3",
"description": "Webshipper Fulfillment provider for Medusa", "description": "Webshipper Fulfillment provider for Medusa",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -30,6 +30,11 @@ export default async (req, res) => {
const purchaseUnit = order.purchase_units[0] const purchaseUnit = order.purchase_units[0]
const cartId = purchaseUnit.custom_id const cartId = purchaseUnit.custom_id
if (!cartId) {
res.sendStatus(200)
return
}
const manager = req.scope.resolve("manager") const manager = req.scope.resolve("manager")
const cartService = req.scope.resolve("cartService") const cartService = req.scope.resolve("cartService")
const orderService = req.scope.resolve("orderService") const orderService = req.scope.resolve("orderService")
@@ -206,7 +206,7 @@ class PayPalProviderService extends PaymentService {
return sessionData return sessionData
} catch (error) { } catch (error) {
throw error return this.createPayment(cart)
} }
} }
@@ -141,7 +141,7 @@ class StripeProviderService extends PaymentService {
const amount = await this.totalsService_.getTotal(cart) const amount = await this.totalsService_.getTotal(cart)
const intentRequest = { const intentRequest = {
amount: amount, amount: Math.round(amount),
currency: currency_code, currency: currency_code,
setup_future_usage: "on_session", setup_future_usage: "on_session",
capture_method: this.options_.capture ? "automatic" : "manual", capture_method: this.options_.capture ? "automatic" : "manual",
@@ -242,12 +242,12 @@ class StripeProviderService extends PaymentService {
if (stripeId !== sessionData.customer) { if (stripeId !== sessionData.customer) {
return this.createPayment(cart) return this.createPayment(cart)
} else { } else {
if (cart.total && sessionData.amount === cart.total) { if (cart.total && sessionData.amount === Math.round(cart.total)) {
return sessionData return sessionData
} }
return this.stripe_.paymentIntents.update(sessionData.id, { return this.stripe_.paymentIntents.update(sessionData.id, {
amount: cart.total, amount: Math.round(cart.total),
}) })
} }
} catch (error) { } catch (error) {
@@ -309,7 +309,7 @@ class StripeProviderService extends PaymentService {
const { id } = payment.data const { id } = payment.data
try { try {
await this.stripe_.refunds.create({ await this.stripe_.refunds.create({
amount: amountToRefund, amount: Math.round(amountToRefund),
payment_intent: id, payment_intent: id,
}) })
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.5](https://github.com/medusajs/medusa/compare/medusa-plugin-brightpearl@1.1.4...medusa-plugin-brightpearl@1.1.5) (2021-02-25)
**Note:** Version bump only for package medusa-plugin-brightpearl
## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-plugin-brightpearl@1.1.3...medusa-plugin-brightpearl@1.1.4) (2021-02-17) ## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-plugin-brightpearl@1.1.3...medusa-plugin-brightpearl@1.1.4) (2021-02-17)
**Note:** Version bump only for package medusa-plugin-brightpearl **Note:** Version bump only for package medusa-plugin-brightpearl
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-brightpearl", "name": "medusa-plugin-brightpearl",
"version": "1.1.4", "version": "1.1.5",
"description": "Brightpearl plugin for Medusa Commerce", "description": "Brightpearl plugin for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -5,6 +5,7 @@ import Brightpearl from "../utils/brightpearl"
class BrightpearlService extends BaseService { class BrightpearlService extends BaseService {
constructor( constructor(
{ {
manager,
oauthService, oauthService,
totalsService, totalsService,
productVariantService, productVariantService,
@@ -18,6 +19,7 @@ class BrightpearlService extends BaseService {
) { ) {
super() super()
this.manager_ = manager
this.options = options this.options = options
this.productVariantService_ = productVariantService this.productVariantService_ = productVariantService
this.regionService_ = regionService this.regionService_ = regionService
@@ -189,9 +191,13 @@ class BrightpearlService extends BaseService {
.retrieveBySKU(sku) .retrieveBySKU(sku)
.catch((_) => undefined) .catch((_) => undefined)
if (variant && variant.manage_inventory) { if (variant && variant.manage_inventory) {
await this.productVariantService_.update(variant.id, { await this.manager_.transaction((m) => {
return this.productVariantService_
.withTransaction(m)
.update(variant.id, {
inventory_quantity: onHand, inventory_quantity: onHand,
}) })
})
} }
} }
} }
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-plugin-contentful@1.1.3...medusa-plugin-contentful@1.1.4) (2021-02-25)
### Bug Fixes
* **medusa-plugin-contentful:** Allow custom fields in plugin options ([#180](https://github.com/medusajs/medusa/issues/180)) ([587a464](https://github.com/medusajs/medusa/commit/587a464e83576833ff616bde7bb26b1bb48472fe))
## [1.1.3](https://github.com/medusajs/medusa/compare/medusa-plugin-contentful@1.1.2...medusa-plugin-contentful@1.1.3) (2021-02-17) ## [1.1.3](https://github.com/medusajs/medusa/compare/medusa-plugin-contentful@1.1.2...medusa-plugin-contentful@1.1.3) (2021-02-17)
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-contentful", "name": "medusa-plugin-contentful",
"version": "1.1.3", "version": "1.1.4",
"description": "Contentful plugin for Medusa Commerce", "description": "Contentful plugin for Medusa Commerce",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -19,8 +19,16 @@ const checkContentTypes = async (container) => {
if (product && product.fields) { if (product && product.fields) {
const productFields = product.fields const productFields = product.fields
const customProductFields = Object.keys(
contentfulService.options_.custom_product_fields || {}
)
const keys = Object.values(productFields).map((f) => f.id) const keys = Object.values(productFields).map((f) => f.id)
if (!requiredProductFields.every((f) => keys.includes(f))) {
const missingKeys = requiredProductFields.filter(
(rpf) => !keys.includes(rpf) && !customProductFields.includes(rpf)
)
if (missingKeys.length) {
throw Error( throw Error(
`Contentful: Content type ${`product`} is missing some required key(s). Required: ${requiredProductFields.join( `Contentful: Content type ${`product`} is missing some required key(s). Required: ${requiredProductFields.join(
", " ", "
@@ -32,8 +40,16 @@ const checkContentTypes = async (container) => {
if (variant && variant.fields) { if (variant && variant.fields) {
const variantFields = variant.fields const variantFields = variant.fields
const customVariantFields = Object.keys(
contentfulService.options_.custom_variant_fields || {}
)
const keys = Object.values(variantFields).map((f) => f.id) const keys = Object.values(variantFields).map((f) => f.id)
if (!requiredVariantFields.every((f) => keys.includes(f))) {
const missingKeys = requiredVariantFields.filter(
(rpf) => !keys.includes(rpf) && !customVariantFields.includes(rpf)
)
if (missingKeys.length) {
throw Error( throw Error(
`Contentful: Content type ${`productVariant`} is missing some required key(s). Required: ${requiredVariantFields.join( `Contentful: Content type ${`productVariant`} is missing some required key(s). Required: ${requiredVariantFields.join(
", " ", "
@@ -47,13 +63,13 @@ const requiredProductFields = [
"title", "title",
"variants", "variants",
"options", "options",
"objectId", "medusaId",
"type", "type",
"collection", "collection",
"tags", "tags",
"handle", "handle",
] ]
const requiredVariantFields = ["title", "sku", "prices", "options", "objectId"] const requiredVariantFields = ["title", "sku", "prices", "options", "medusaId"]
export default checkContentTypes export default checkContentTypes
@@ -111,10 +111,27 @@ class ContentfulService extends BaseService {
return assets return assets
} }
getCustomField(field, type) {
const customOptions = this.options_[`custom_${type}_fields`]
if (customOptions) {
return customOptions[field] || field
} else {
return field
}
}
async createProductInContentful(product) { async createProductInContentful(product) {
try { try {
const p = await this.productService_.retrieve(product.id, { const p = await this.productService_.retrieve(product.id, {
relations: ["variants", "options", "tags", "type", "collection"], relations: [
"variants",
"options",
"tags",
"type",
"collection",
"images",
],
}) })
const environment = await this.getContentfulEnvironment_() const environment = await this.getContentfulEnvironment_()
@@ -122,46 +139,92 @@ class ContentfulService extends BaseService {
const variantLinks = this.getVariantLinks_(variantEntries) const variantLinks = this.getVariantLinks_(variantEntries)
const fields = { const fields = {
title: { [this.getCustomField("title", "product")]: {
"en-US": p.title, "en-US": p.title,
}, },
variants: { [this.getCustomField("variants", "product")]: {
"en-US": variantLinks, "en-US": variantLinks,
}, },
options: { [this.getCustomField("options", "product")]: {
"en-US": p.options, "en-US": p.options,
}, },
objectId: { [this.getCustomField("medusaId", "product")]: {
"en-US": p.id, "en-US": p.id,
}, },
} }
if (p.images.length > 0) {
const imageLinks = await this.createImageAssets(product)
const thumbnailAsset = await environment.createAsset({
fields: {
title: {
"en-US": `${p.title}`,
},
description: {
"en-US": "",
},
file: {
"en-US": {
contentType: "image/xyz",
fileName: p.thumbnail,
upload: p.thumbnail,
},
},
},
})
await thumbnailAsset.processForAllLocales()
const thumbnailLink = {
sys: {
type: "Link",
linkType: "Asset",
id: thumbnailAsset.sys.id,
},
}
fields.thumbnail = {
"en-US": thumbnailLink,
}
if (imageLinks) {
fields.images = {
"en-US": imageLinks,
}
}
}
if (p.type) { if (p.type) {
const type = { const type = {
"en-US": p.type.value, "en-US": p.type.value,
} }
fields.type = type
fields[this.getCustomField("type", "product")] = type
} }
if (p.collection) { if (p.collection) {
const collection = { const collection = {
"en-US": p.collection.title, "en-US": p.collection.title,
} }
fields.collection = collection
fields[this.getCustomField("collection", "product")] = collection
} }
if (p.tags) { if (p.tags) {
const tags = { const tags = {
"en-US": p.tags, "en-US": p.tags,
} }
fields.tags = tags
fields[this.getCustomField("tags", "product")] = tags
} }
if (p.handle) { if (p.handle) {
const handle = { const handle = {
"en-US": p.handle, "en-US": p.handle,
} }
fields.handle = handle
fields[this.getCustomField("handle", "product")] = handle
} }
const result = await environment.createEntryWithId("product", p.id, { const result = await environment.createEntryWithId("product", p.id, {
@@ -189,19 +252,19 @@ class ContentfulService extends BaseService {
v.id, v.id,
{ {
fields: { fields: {
title: { [this.getCustomField("title", "variant")]: {
"en-US": v.title, "en-US": v.title,
}, },
sku: { [this.getCustomField("sku", "variant")]: {
"en-US": v.sku, "en-US": v.sku,
}, },
prices: { [this.getCustomField("prices", "variant")]: {
"en-US": v.prices, "en-US": v.prices,
}, },
options: { [this.getCustomField("options", "variant")]: {
"en-US": v.options, "en-US": v.options,
}, },
objectId: { [this.getCustomField("medusaId", "variant")]: {
"en-US": v.id, "en-US": v.id,
}, },
}, },
@@ -240,7 +303,14 @@ class ContentfulService extends BaseService {
} }
const p = await this.productService_.retrieve(product.id, { const p = await this.productService_.retrieve(product.id, {
relations: ["options", "variants", "type", "collection", "tags"], relations: [
"options",
"variants",
"type",
"collection",
"tags",
"images",
],
}) })
const variantEntries = await this.getVariantEntries_(p.variants) const variantEntries = await this.getVariantEntries_(p.variants)
@@ -248,46 +318,86 @@ class ContentfulService extends BaseService {
const productEntryFields = { const productEntryFields = {
...productEntry.fields, ...productEntry.fields,
title: { [this.getCustomField("title", "product")]: {
"en-US": p.title, "en-US": p.title,
}, },
options: { [this.getCustomField("options", "product")]: {
"en-US": p.options, "en-US": p.options,
}, },
variants: { [this.getCustomField("variants", "product")]: {
"en-US": variantLinks, "en-US": variantLinks,
}, },
objectId: { [this.getCustomField("medusaId", "product")]: {
"en-US": p.id, "en-US": p.id,
}, },
} }
if (p.thumbnail) {
const thumbnailAsset = await environment.createAsset({
fields: {
title: {
"en-US": `${p.title}`,
},
description: {
"en-US": "",
},
file: {
"en-US": {
contentType: "image/xyz",
fileName: p.thumbnail,
upload: p.thumbnail,
},
},
},
})
await thumbnailAsset.processForAllLocales()
const thumbnailLink = {
sys: {
type: "Link",
linkType: "Asset",
id: thumbnailAsset.sys.id,
},
}
productEntryFields.thumbnail = {
"en-US": thumbnailLink,
}
}
if (p.type) { if (p.type) {
const type = { const type = {
"en-US": p.type.value, "en-US": p.type.value,
} }
productEntryFields.type = type
productEntryFields[this.getCustomField("type", "product")] = type
} }
if (p.collection) { if (p.collection) {
const collection = { const collection = {
"en-US": p.collection.title, "en-US": p.collection.title,
} }
productEntryFields.collection = collection
productEntryFields[
this.getCustomField("collection", "product")
] = collection
} }
if (p.tags) { if (p.tags) {
const tags = { const tags = {
"en-US": p.tags, "en-US": p.tags,
} }
productEntryFields.tags = tags
productEntryFields[this.getCustomField("tags", "product")] = tags
} }
if (p.handle) { if (p.handle) {
const handle = { const handle = {
"en-US": p.handle, "en-US": p.handle,
} }
productEntryFields.handle = handle
productEntryFields[this.getCustomField("handle", "product")] = handle
} }
productEntry.fields = productEntryFields productEntry.fields = productEntryFields
@@ -333,19 +443,19 @@ class ContentfulService extends BaseService {
const variantEntryFields = { const variantEntryFields = {
...variantEntry.fields, ...variantEntry.fields,
title: { [this.getCustomField("title", "variant")]: {
"en-US": v.title, "en-US": v.title,
}, },
sku: { [this.getCustomField("sku", "variant")]: {
"en-US": v.sku, "en-US": v.sku,
}, },
options: { [this.getCustomField("options", "variant")]: {
"en-US": v.options, "en-US": v.options,
}, },
prices: { [this.getCustomField("prices", "variant")]: {
"en-US": v.prices, "en-US": v.prices,
}, },
objectId: { [this.getCustomField("medusaId", "variant")]: {
"en-US": v.id, "en-US": v.id,
}, },
} }
@@ -377,7 +487,8 @@ class ContentfulService extends BaseService {
} }
let update = { let update = {
title: productEntry.fields.title["en-US"], title:
productEntry.fields[this.getCustomField("title", "product")]["en-US"],
} }
// Get the thumbnail, if present // Get the thumbnail, if present
@@ -421,7 +532,10 @@ class ContentfulService extends BaseService {
const updatedVariant = await this.productVariantService_.update( const updatedVariant = await this.productVariantService_.update(
variantId, variantId,
{ {
title: variantEntry.fields.title["en-US"], title:
variantEntry.fields[this.getCustomField("title", "variant")][
"en-US"
],
} }
) )
@@ -3,6 +3,59 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.5](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.5-next.3...medusa-plugin-segment@1.1.5) (2021-02-25)
**Note:** Version bump only for package medusa-plugin-segment
## [1.1.5-next.3](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.5-next.2...medusa-plugin-segment@1.1.5-next.3) (2021-02-25)
### Bug Fixes
* normalize currency code ([98aa404](https://github.com/medusajs/medusa/commit/98aa404306d55f0818d48e56c51146351ebfe306))
## [1.1.5-next.2](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.5-next.1...medusa-plugin-segment@1.1.5-next.2) (2021-02-25)
### Features
* **segment:** track shipments ([d156911](https://github.com/medusajs/medusa/commit/d15691188348c19fc22806d8cf7584fc5f249ce9))
## [1.1.5-next.1](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.5-next.0...medusa-plugin-segment@1.1.5-next.1) (2021-02-25)
### Bug Fixes
* add subtitle to tracks ([0c294b7](https://github.com/medusajs/medusa/commit/0c294b7b3acbc1b873aab7e90a8e596bdac48899))
* versioning ([262af34](https://github.com/medusajs/medusa/commit/262af34125543d9a80bf469b5d380019b9bc8d3f))
## [1.1.5-next.0](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.4...medusa-plugin-segment@1.1.5-next.0) (2021-02-22)
### Features
* **medusa-plugin-segment:** adds category and type to segment events ([#179](https://github.com/medusajs/medusa/issues/179)) ([e27cf72](https://github.com/medusajs/medusa/commit/e27cf72a8ca49a6586a82dde964d559c40a4415f))
## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.3...medusa-plugin-segment@1.1.4) (2021-02-17) ## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-plugin-segment@1.1.3...medusa-plugin-segment@1.1.4) (2021-02-17)
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-segment", "name": "medusa-plugin-segment",
"version": "1.1.4", "version": "1.1.5",
"description": "Segment Analytics", "description": "Segment Analytics",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -42,5 +42,5 @@
"medusa-core-utils": "^1.1.0", "medusa-core-utils": "^1.1.0",
"medusa-test-utils": "^1.1.3" "medusa-test-utils": "^1.1.3"
}, },
"gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" "gitHead": "0c294b7b3acbc1b873aab7e90a8e596bdac48899"
} }
@@ -10,11 +10,12 @@ class SegmentService extends BaseService {
* write_key: Segment write key given in Segment dashboard * write_key: Segment write key given in Segment dashboard
* } * }
*/ */
constructor({ totalsService }, options) { constructor({ totalsService, productService }, options) {
super() super()
this.totalsService_ = totalsService this.totalsService_ = totalsService
this.options_ = options this.options_ = options
this.productService_ = productService
this.analytics_ = new Analytics(options.write_key) this.analytics_ = new Analytics(options.write_key)
} }
@@ -102,7 +103,7 @@ class SegmentService extends BaseService {
tax, tax,
discount, discount,
coupon, coupon,
currency: order.currency_code, currency: order.currency_code.toUpperCase(),
products: await Promise.all( products: await Promise.all(
order.items.map(async (item) => { order.items.map(async (item) => {
let name = item.title let name = item.title
@@ -129,12 +130,20 @@ class SegmentService extends BaseService {
variant = item.variant.sku variant = item.variant.sku
} }
const product = await this.productService_.retrieve(
item.variant.product_id,
{ relations: ["collection", "type"] }
)
return { return {
name, name,
variant, variant,
price: lineTotal / 100 / item.quantity, price: lineTotal / 100 / item.quantity,
reporting_revenue: revenue, reporting_revenue: revenue,
product_id: item.variant.product_id, product_id: item.variant.product_id,
category: product.collection?.title,
subtitle: product.subtitle,
type: product.type?.value,
sku, sku,
quantity: item.quantity, quantity: item.quantity,
} }
@@ -5,12 +5,76 @@ class OrderSubscriber {
orderService, orderService,
claimService, claimService,
returnService, returnService,
fulfillmentService,
}) { }) {
this.orderService_ = orderService this.orderService_ = orderService
this.returnService_ = returnService this.returnService_ = returnService
this.claimService_ = claimService this.claimService_ = claimService
this.fulfillmentService_ = fulfillmentService
eventBusService.subscribe(
"order.shipment_created",
async ({ id, fulfillment_id }) => {
const order = await this.orderService_.retrieve(id, {
select: [
"shipping_total",
"discount_total",
"tax_total",
"refunded_total",
"gift_card_total",
"subtotal",
"total",
],
relations: [
"customer",
"billing_address",
"shipping_address",
"discounts",
"shipping_methods",
"payments",
"fulfillments",
"returns",
"items",
"gift_cards",
"gift_card_transactions",
"swaps",
"swaps.return_order",
"swaps.payment",
"swaps.shipping_methods",
"swaps.shipping_address",
"swaps.additional_items",
"swaps.fulfillments",
],
})
const fulfillment = await this.fulfillmentService_.retrieve(
fulfillment_id,
{
relations: ["items"],
}
)
const toBuildFrom = {
...order,
provider_id: fulfillment.provider,
items: fulfillment.items.map((i) =>
order.items.find((l) => l.id === i.item_id)
),
}
const orderData = await segmentService.buildOrder(toBuildFrom)
const orderEvent = {
event: "Order Shipped",
userId: order.customer_id,
properties: orderData,
timestamp: fulfillment.shipped_at,
}
segmentService.track(orderEvent)
}
)
eventBusService.subscribe("claim.created", async ({ id }) => { eventBusService.subscribe("claim.created", async ({ id }) => {
const claim = await this.claimService_.retrieve(id, { const claim = await this.claimService_.retrieve(id, {
@@ -74,6 +138,7 @@ class OrderSubscriber {
"payments", "payments",
"fulfillments", "fulfillments",
"returns", "returns",
"items",
"gift_cards", "gift_cards",
"gift_card_transactions", "gift_card_transactions",
"swaps", "swaps",
@@ -160,6 +225,7 @@ class OrderSubscriber {
"shipping_methods", "shipping_methods",
"payments", "payments",
"fulfillments", "fulfillments",
"items",
"returns", "returns",
"gift_cards", "gift_cards",
"gift_card_transactions", "gift_card_transactions",
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.4](https://github.com/medusajs/medusa/compare/medusa-plugin-sendgrid@1.1.3...medusa-plugin-sendgrid@1.1.4) (2021-02-25)
### Bug Fixes
* add tracking links to shipments ([7be4bb5](https://github.com/medusajs/medusa/commit/7be4bb5f2daa0aad805abe0f97278f53cf3af402))
* sendgrid tracking links ([5cfc8d8](https://github.com/medusajs/medusa/commit/5cfc8d80bd3eaee93595027d0cc3ce67ae98d275))
## [1.1.3](https://github.com/medusajs/medusa/compare/medusa-plugin-sendgrid@1.1.2...medusa-plugin-sendgrid@1.1.3) (2021-02-17) ## [1.1.3](https://github.com/medusajs/medusa/compare/medusa-plugin-sendgrid@1.1.2...medusa-plugin-sendgrid@1.1.3) (2021-02-17)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "medusa-plugin-sendgrid", "name": "medusa-plugin-sendgrid",
"version": "1.1.3", "version": "1.1.4",
"description": "SendGrid transactional emails", "description": "SendGrid transactional emails",
"main": "index.js", "main": "index.js",
"repository": { "repository": {
@@ -275,7 +275,7 @@ class SendGridService extends NotificationService {
}) })
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, { const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
relations: ["items"], relations: ["items", "tracking_links"],
}) })
return { return {
@@ -283,6 +283,7 @@ class SendGridService extends NotificationService {
date: shipment.shipped_at.toDateString(), date: shipment.shipped_at.toDateString(),
email: order.email, email: order.email,
fulfillment: shipment, fulfillment: shipment,
tracking_links: shipment.tracking_links,
tracking_number: shipment.tracking_numbers.join(", "), tracking_number: shipment.tracking_numbers.join(", "),
} }
} }
@@ -586,7 +587,9 @@ class SendGridService extends NotificationService {
const refundAmount = swap.return_order.refund_amount const refundAmount = swap.return_order.refund_amount
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id) const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
relations: ["tracking_links"],
})
return { return {
swap, swap,
@@ -602,6 +605,7 @@ class SendGridService extends NotificationService {
refund_amount: `${this.humanPrice_(refundAmount)} ${currencyCode}`, refund_amount: `${this.humanPrice_(refundAmount)} ${currencyCode}`,
additional_total: `${this.humanPrice_(additionalTotal)} ${currencyCode}`, additional_total: `${this.humanPrice_(additionalTotal)} ${currencyCode}`,
fulfillment: shipment, fulfillment: shipment,
tracking_links: shipment.tracking_links,
tracking_number: shipment.tracking_numbers.join(", "), tracking_number: shipment.tracking_numbers.join(", "),
} }
} }
@@ -611,13 +615,16 @@ class SendGridService extends NotificationService {
relations: ["order", "order.items", "order.shipping_address"], relations: ["order", "order.items", "order.shipping_address"],
}) })
const shipment = await this.fulfillmentService_.retrieve(fulfillment_id) const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, {
relations: ["tracking_links"],
})
return { return {
email: claim.order.email, email: claim.order.email,
claim, claim,
order: claim.order, order: claim.order,
fulfillment: shipment, fulfillment: shipment,
tracking_links: shipment.tracking_links,
tracking_number: shipment.tracking_numbers.join(", "), tracking_number: shipment.tracking_numbers.join(", "),
} }
} }
+41
View File
@@ -3,6 +3,47 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.11](https://github.com/medusajs/medusa/compare/@medusajs/medusa@1.1.10...@medusajs/medusa@1.1.11) (2021-02-25)
### Bug Fixes
* **medusa:** Add querying func. on customer retrievals ([#181](https://github.com/medusajs/medusa/issues/181)) ([22be418](https://github.com/medusajs/medusa/commit/22be418ec132944afe469106ba4b3b92f634d240))
## [1.1.10](https://github.com/medusajs/medusa/compare/@medusajs/medusa@1.1.10-next.1...@medusajs/medusa@1.1.10) (2021-02-25)
**Note:** Version bump only for package @medusajs/medusa
## [1.1.10-next.1](https://github.com/medusajs/medusa/compare/@medusajs/medusa@1.1.10-next.0...@medusajs/medusa@1.1.10-next.1) (2021-02-25)
### Bug Fixes
* update-product ([0320788](https://github.com/medusajs/medusa/commit/0320788aacf93da8a8951c6a540656da1772dba4))
## [1.1.10-next.0](https://github.com/medusajs/medusa/compare/@medusajs/medusa@1.1.9...@medusajs/medusa@1.1.10-next.0) (2021-02-22)
### Features
* **medusa:** tracking links ([#177](https://github.com/medusajs/medusa/issues/177)) ([99ad43b](https://github.com/medusajs/medusa/commit/99ad43bf47c3922f391d433448b1c4affd88f457))
## [1.1.9](https://github.com/medusajs/medusa/compare/@medusajs/medusa@1.1.8...@medusajs/medusa@1.1.9) (2021-02-18) ## [1.1.9](https://github.com/medusajs/medusa/compare/@medusajs/medusa@1.1.8...@medusajs/medusa@1.1.9) (2021-02-18)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@medusajs/medusa", "name": "@medusajs/medusa",
"version": "1.1.9", "version": "1.1.11",
"description": "E-commerce for JAMstack", "description": "E-commerce for JAMstack",
"main": "dist/index.js", "main": "dist/index.js",
"repository": { "repository": {
@@ -2,7 +2,7 @@ export default async (req, res) => {
try { try {
const customerService = req.scope.resolve("customerService") const customerService = req.scope.resolve("customerService")
const limit = parseInt(req.query.limit) || 10 const limit = parseInt(req.query.limit) || 50
const offset = parseInt(req.query.offset) || 0 const offset = parseInt(req.query.offset) || 0
const selector = {} const selector = {}
@@ -11,15 +11,23 @@ export default async (req, res) => {
selector.q = req.query.q selector.q = req.query.q
} }
let expandFields = []
if ("expand" in req.query) {
expandFields = req.query.expand.split(",")
}
const listConfig = { const listConfig = {
relations: [], relations: expandFields.length ? expandFields : [],
skip: offset, skip: offset,
take: limit, take: limit,
} }
const customers = await customerService.list(selector, listConfig) const [customers, count] = await customerService.listAndCount(
selector,
listConfig
)
res.json({ customers, count: customers.length, offset, limit }) res.json({ customers, count, offset, limit })
} catch (error) { } catch (error) {
throw error throw error
} }
@@ -14,13 +14,23 @@ export default async (req, res) => {
selector.q = req.query.q selector.q = req.query.q
} }
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 ("is_giftcard" in req.query) { if ("is_giftcard" in req.query) {
selector.is_giftcard = req.query.is_giftcard === "true" selector.is_giftcard = req.query.is_giftcard === "true"
} }
const listConfig = { const listConfig = {
select: defaultFields, select: includeFields.length ? includeFields : defaultFields,
relations: defaultRelations, relations: expandFields.length ? expandFields : defaultRelations,
skip: offset, skip: offset,
take: limit, take: limit,
} }
@@ -6,6 +6,9 @@ export default async (req, res) => {
const schema = Validator.object().keys({ const schema = Validator.object().keys({
title: Validator.string().optional(), title: Validator.string().optional(),
subtitle: Validator.string()
.optional()
.allow(null, ""),
description: Validator.string().optional(), description: Validator.string().optional(),
type: Validator.object() type: Validator.object()
.keys({ .keys({
+50 -2
View File
@@ -1,5 +1,53 @@
import { EntityRepository, Repository } from "typeorm" import { flatten, groupBy, map, merge } from "lodash"
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
import { Product } from "../models/product" import { Product } from "../models/product"
@EntityRepository(Product) @EntityRepository(Product)
export class ProductRepository extends Repository<Product> {} export class ProductRepository extends Repository<Product> {
public async findWithRelations(
relations: Array<keyof Product> = [],
optionsWithoutRelations: Omit<FindManyOptions<Product>, "relations"> = {}
): Promise<Product[]> {
const entities = await this.find(optionsWithoutRelations)
const entitiesIds = entities.map(({ id }) => id)
const groupedRelations = {}
for (const rel of relations) {
const [topLevel] = rel.split(".")
if (groupedRelations[topLevel]) {
groupedRelations[topLevel].push(rel)
} else {
groupedRelations[topLevel] = [rel]
}
}
const entitiesIdsWithRelations = await Promise.all(
Object.entries(groupedRelations).map(([_, rels]) => {
return this.findByIds(entitiesIds, {
select: ["id"],
relations: rels as string[],
})
})
).then(flatten)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entitiesAndRelationsById, entityAndRelations =>
merge({}, ...entityAndRelations)
)
}
public async findOneWithRelations(
relations: Array<keyof Product> = [],
optionsWithoutRelations: Omit<FindManyOptions<Product>, "relations"> = {}
): Promise<Product> {
// Limit 1
optionsWithoutRelations.take = 1
const result = await this.findWithRelations(
relations,
optionsWithoutRelations
)
return result[0]
}
}
@@ -11,7 +11,8 @@ const eventBusService = {
describe("ProductService", () => { describe("ProductService", () => {
describe("retrieve", () => { describe("retrieve", () => {
const productRepo = MockRepository({ const productRepo = MockRepository({
findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), findOneWithRelations: () =>
Promise.resolve({ id: IdMap.getId("ironman") }),
}) })
const productService = new ProductService({ const productService = new ProductService({
manager: MockManager, manager: MockManager,
@@ -25,8 +26,8 @@ describe("ProductService", () => {
it("successfully retrieves a product", async () => { it("successfully retrieves a product", async () => {
const result = await productService.retrieve(IdMap.getId("ironman")) const result = await productService.retrieve(IdMap.getId("ironman"))
expect(productRepo.findOne).toHaveBeenCalledTimes(1) expect(productRepo.findOneWithRelations).toHaveBeenCalledTimes(1)
expect(productRepo.findOne).toHaveBeenCalledWith({ expect(productRepo.findOneWithRelations).toHaveBeenCalledWith(undefined, {
where: { id: IdMap.getId("ironman") }, where: { id: IdMap.getId("ironman") },
}) })
@@ -42,7 +43,7 @@ describe("ProductService", () => {
options: [], options: [],
collection: { id: IdMap.getId("cat"), title: "Suits" }, collection: { id: IdMap.getId("cat"), title: "Suits" },
}), }),
findOne: () => ({ findOneWithRelations: () => ({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
title: "Suit", title: "Suit",
options: [], options: [],
@@ -137,7 +138,7 @@ describe("ProductService", () => {
describe("update", () => { describe("update", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => { findOneWithRelations: (rels, query) => {
if (query.where.id === IdMap.getId("ironman&co")) { if (query.where.id === IdMap.getId("ironman&co")) {
return Promise.resolve({ return Promise.resolve({
id: IdMap.getId("ironman&co"), id: IdMap.getId("ironman&co"),
@@ -322,7 +323,7 @@ describe("ProductService", () => {
describe("addOption", () => { describe("addOption", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => findOneWithRelations: query =>
Promise.resolve({ Promise.resolve({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
options: [{ title: "Color" }], options: [{ title: "Color" }],
@@ -395,7 +396,7 @@ describe("ProductService", () => {
describe("reorderVariants", () => { describe("reorderVariants", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => findOneWithRelations: query =>
Promise.resolve({ Promise.resolve({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }], variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }],
@@ -453,7 +454,7 @@ describe("ProductService", () => {
describe("reorderOptions", () => { describe("reorderOptions", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => findOneWithRelations: query =>
Promise.resolve({ Promise.resolve({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
options: [ options: [
@@ -519,7 +520,7 @@ describe("ProductService", () => {
describe("updateOption", () => { describe("updateOption", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => findOneWithRelations: query =>
Promise.resolve({ Promise.resolve({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
options: [ options: [
@@ -594,7 +595,7 @@ describe("ProductService", () => {
describe("deleteOption", () => { describe("deleteOption", () => {
const productRepository = MockRepository({ const productRepository = MockRepository({
findOne: query => findOneWithRelations: query =>
Promise.resolve({ Promise.resolve({
id: IdMap.getId("ironman"), id: IdMap.getId("ironman"),
variants: [ variants: [
+44
View File
@@ -160,6 +160,50 @@ class CustomerService extends BaseService {
return customerRepo.find(query) return customerRepo.find(query)
} }
async listAndCount(
selector,
config = { relations: [], skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const customerRepo = this.manager_.getCustomRepository(
this.customerRepository_
)
let q
if ("q" in selector) {
q = selector.q
delete selector.q
}
const query = this.buildQuery_(selector, config)
if (q) {
const where = query.where
delete where.email
delete where.first_name
delete where.last_name
query.join = {
alias: "customer",
}
query.where = qb => {
qb.where(where)
qb.andWhere(
new Brackets(qb => {
qb.where(`customer.first_name ILIKE :q`, { q: `%${q}%` })
.orWhere(`customer.last_name ILIKE :q`, { q: `%${q}%` })
.orWhere(`customer.email ILIKE :q`, { q: `%${q}%` })
})
)
}
}
const [customers, count] = await customerRepo.findAndCount(query)
return [customers, count]
}
/** /**
* Return the total number of documents in database * Return the total number of documents in database
* @return {Promise} the result of the count operation * @return {Promise} the result of the count operation
+30 -6
View File
@@ -93,6 +93,17 @@ class ProductService extends BaseService {
const query = this.buildQuery_(selector, config) const query = this.buildQuery_(selector, config)
if (config.relations && config.relations.length > 0) {
query.relations = config.relations
}
if (config.select && config.select.length > 0) {
query.select = config.select
}
const rels = query.relations
delete query.relations
if (q) { if (q) {
const where = query.where const where = query.where
@@ -122,7 +133,7 @@ class ProductService extends BaseService {
} }
} }
return productRepo.find(query) return productRepo.findWithRelations(rels, query)
} }
/** /**
@@ -143,11 +154,25 @@ class ProductService extends BaseService {
* @return {Promise<Product>} the result of the find one operation. * @return {Promise<Product>} the result of the find one operation.
*/ */
async retrieve(productId, config = {}) { async retrieve(productId, config = {}) {
return this.atomicPhase_(async manager => { const productRepo = this.manager_.getCustomRepository(
const productRepo = manager.getCustomRepository(this.productRepository_) this.productRepository_
)
const validatedId = this.validateId_(productId) const validatedId = this.validateId_(productId)
const query = this.buildQuery_({ id: validatedId }, config)
const product = await productRepo.findOne(query) const query = { where: { id: validatedId } }
if (config.relations && config.relations.length > 0) {
query.relations = config.relations
}
if (config.select && config.select.length > 0) {
query.select = config.select
}
const rels = query.relations
delete query.relations
const product = await productRepo.findOneWithRelations(rels, query)
if (!product) { if (!product) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
@@ -156,7 +181,6 @@ class ProductService extends BaseService {
} }
return product return product
})
} }
/** /**