diff --git a/README.md b/README.md index c75bba9654..fbfee350b6 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,96 @@ -# medusa -Medusa Monorepo +# Medusa +Medusa is a headless commerce engine built with Node.js using Express with a Postgresql database. -# MVP Roadmap (!) +## Documentation -- Finish core - - Services - - [x] Auth - - [x] Product - - [x] Product Variant - - [x] User - - [ ] Region - - [ ] Cart - - [x] Customer - - [ ] Order - - [ ] Promo Code - - [ ] Gift cards - - REST API controllers - - Admin - - Store -- Core plugins - - Payment Providers - - [ ] Stripe - - [ ] Klarna - - [ ] PayPal - - [ ] QuickPay - - Fulfillment providers - - [ ] E-conomic - - [ ] Brightpearl - - [ ] Shipmondo - - [ ] Webshipper - - Transactional Emails - - [ ] Twilio SendGrid - - Role Based Permission - - Contentful sync plugin (to send SKUs to Contentful for content enrichment) +See [Medusa Commerce API docs](https://docs.medusa-commerce.com/api/store/) for Node.js. + +## Get started in less than 5 minutes + +You can get a Medusa engine up and running in your local development environment within a couple of minutes. Perform the following steps: + +1. Install Medusa, the Medusa CLI, Medusa babel preset and Medusa interfaces + + ```bash + # core medusa + npm install @medusajs/medusa + yarn add @medusajs/medusa + + # CLI + npm install -g @medusa/medusa-cli + yarn add global @medusajs/medusa-cli + + # babel preset + npm install babel-preset-medusa-package + yarn add babel-preset-medusa-package + + # interfaces + npm install medusa-interfaces + yarn add medusa-interfaces + ``` +2. Create a file `medusa-config.js` at the root level of your Node.js project and fill in required settings + ``` + // CORS to avoid issues when consuming Medusa from a client + const STORE_CORS = "http://localhost:8000"; + + // 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! diff --git a/integration-tests/api/__tests__/admin/customer.js b/integration-tests/api/__tests__/admin/customer.js new file mode 100644 index 0000000000..14c4222977 --- /dev/null +++ b/integration-tests/api/__tests__/admin/customer.js @@ -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", + }), + ]), + }), + ]) + ); + }); + }); +}); diff --git a/integration-tests/api/helpers/customer-seeder.js b/integration-tests/api/helpers/customer-seeder.js new file mode 100644 index 0000000000..78b3e22ec5 --- /dev/null +++ b/integration-tests/api/helpers/customer-seeder.js @@ -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", + }); +}; diff --git a/packages/medusa-fulfillment-webshipper/CHANGELOG.md b/packages/medusa-fulfillment-webshipper/CHANGELOG.md index ec89b0aae2..2faa0faa67 100644 --- a/packages/medusa-fulfillment-webshipper/CHANGELOG.md +++ b/packages/medusa-fulfillment-webshipper/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. 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) diff --git a/packages/medusa-fulfillment-webshipper/package.json b/packages/medusa-fulfillment-webshipper/package.json index ef1762db12..0d644f4859 100644 --- a/packages/medusa-fulfillment-webshipper/package.json +++ b/packages/medusa-fulfillment-webshipper/package.json @@ -1,6 +1,6 @@ { "name": "medusa-fulfillment-webshipper", - "version": "1.1.2", + "version": "1.1.3", "description": "Webshipper Fulfillment provider for Medusa", "main": "index.js", "repository": { diff --git a/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js b/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js index e0ef09b680..03fa6d3276 100644 --- a/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js +++ b/packages/medusa-payment-paypal/src/api/routes/hooks/paypal.js @@ -30,6 +30,11 @@ export default async (req, res) => { const purchaseUnit = order.purchase_units[0] const cartId = purchaseUnit.custom_id + if (!cartId) { + res.sendStatus(200) + return + } + const manager = req.scope.resolve("manager") const cartService = req.scope.resolve("cartService") const orderService = req.scope.resolve("orderService") diff --git a/packages/medusa-payment-paypal/src/services/paypal-provider.js b/packages/medusa-payment-paypal/src/services/paypal-provider.js index bf78dbf388..ed868da3c8 100644 --- a/packages/medusa-payment-paypal/src/services/paypal-provider.js +++ b/packages/medusa-payment-paypal/src/services/paypal-provider.js @@ -206,7 +206,7 @@ class PayPalProviderService extends PaymentService { return sessionData } catch (error) { - throw error + return this.createPayment(cart) } } diff --git a/packages/medusa-payment-stripe/src/services/stripe-provider.js b/packages/medusa-payment-stripe/src/services/stripe-provider.js index b55b921bd2..34406cd467 100644 --- a/packages/medusa-payment-stripe/src/services/stripe-provider.js +++ b/packages/medusa-payment-stripe/src/services/stripe-provider.js @@ -141,7 +141,7 @@ class StripeProviderService extends PaymentService { const amount = await this.totalsService_.getTotal(cart) const intentRequest = { - amount: amount, + amount: Math.round(amount), currency: currency_code, setup_future_usage: "on_session", capture_method: this.options_.capture ? "automatic" : "manual", @@ -242,12 +242,12 @@ class StripeProviderService extends PaymentService { if (stripeId !== sessionData.customer) { return this.createPayment(cart) } else { - if (cart.total && sessionData.amount === cart.total) { + if (cart.total && sessionData.amount === Math.round(cart.total)) { return sessionData } return this.stripe_.paymentIntents.update(sessionData.id, { - amount: cart.total, + amount: Math.round(cart.total), }) } } catch (error) { @@ -309,7 +309,7 @@ class StripeProviderService extends PaymentService { const { id } = payment.data try { await this.stripe_.refunds.create({ - amount: amountToRefund, + amount: Math.round(amountToRefund), payment_intent: id, }) diff --git a/packages/medusa-plugin-brightpearl/CHANGELOG.md b/packages/medusa-plugin-brightpearl/CHANGELOG.md index acfde4efce..6ee2fb71aa 100644 --- a/packages/medusa-plugin-brightpearl/CHANGELOG.md +++ b/packages/medusa-plugin-brightpearl/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. 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) **Note:** Version bump only for package medusa-plugin-brightpearl diff --git a/packages/medusa-plugin-brightpearl/package.json b/packages/medusa-plugin-brightpearl/package.json index f765b8d772..bdd3b8470f 100644 --- a/packages/medusa-plugin-brightpearl/package.json +++ b/packages/medusa-plugin-brightpearl/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-brightpearl", - "version": "1.1.4", + "version": "1.1.5", "description": "Brightpearl plugin for Medusa Commerce", "main": "index.js", "repository": { diff --git a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js index a114000e52..2fd27359e8 100644 --- a/packages/medusa-plugin-brightpearl/src/services/brightpearl.js +++ b/packages/medusa-plugin-brightpearl/src/services/brightpearl.js @@ -5,6 +5,7 @@ import Brightpearl from "../utils/brightpearl" class BrightpearlService extends BaseService { constructor( { + manager, oauthService, totalsService, productVariantService, @@ -18,6 +19,7 @@ class BrightpearlService extends BaseService { ) { super() + this.manager_ = manager this.options = options this.productVariantService_ = productVariantService this.regionService_ = regionService @@ -189,8 +191,12 @@ class BrightpearlService extends BaseService { .retrieveBySKU(sku) .catch((_) => undefined) if (variant && variant.manage_inventory) { - await this.productVariantService_.update(variant.id, { - inventory_quantity: onHand, + await this.manager_.transaction((m) => { + return this.productVariantService_ + .withTransaction(m) + .update(variant.id, { + inventory_quantity: onHand, + }) }) } } diff --git a/packages/medusa-plugin-contentful/CHANGELOG.md b/packages/medusa-plugin-contentful/CHANGELOG.md index aada6dc0bf..0fa6c1d8bc 100644 --- a/packages/medusa-plugin-contentful/CHANGELOG.md +++ b/packages/medusa-plugin-contentful/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. 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) diff --git a/packages/medusa-plugin-contentful/package.json b/packages/medusa-plugin-contentful/package.json index f95bf1a087..0a093930de 100644 --- a/packages/medusa-plugin-contentful/package.json +++ b/packages/medusa-plugin-contentful/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-contentful", - "version": "1.1.3", + "version": "1.1.4", "description": "Contentful plugin for Medusa Commerce", "main": "index.js", "repository": { diff --git a/packages/medusa-plugin-contentful/src/loaders/check-types.js b/packages/medusa-plugin-contentful/src/loaders/check-types.js index 88f6ea78ef..9c224621dd 100644 --- a/packages/medusa-plugin-contentful/src/loaders/check-types.js +++ b/packages/medusa-plugin-contentful/src/loaders/check-types.js @@ -19,8 +19,16 @@ const checkContentTypes = async (container) => { if (product && product.fields) { const productFields = product.fields + const customProductFields = Object.keys( + contentfulService.options_.custom_product_fields || {} + ) 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( `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) { const variantFields = variant.fields + const customVariantFields = Object.keys( + contentfulService.options_.custom_variant_fields || {} + ) 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( `Contentful: Content type ${`productVariant`} is missing some required key(s). Required: ${requiredVariantFields.join( ", " @@ -47,13 +63,13 @@ const requiredProductFields = [ "title", "variants", "options", - "objectId", + "medusaId", "type", "collection", "tags", "handle", ] -const requiredVariantFields = ["title", "sku", "prices", "options", "objectId"] +const requiredVariantFields = ["title", "sku", "prices", "options", "medusaId"] export default checkContentTypes diff --git a/packages/medusa-plugin-contentful/src/services/contentful.js b/packages/medusa-plugin-contentful/src/services/contentful.js index 33f45acd32..b0ec7ce842 100644 --- a/packages/medusa-plugin-contentful/src/services/contentful.js +++ b/packages/medusa-plugin-contentful/src/services/contentful.js @@ -111,10 +111,27 @@ class ContentfulService extends BaseService { return assets } + getCustomField(field, type) { + const customOptions = this.options_[`custom_${type}_fields`] + + if (customOptions) { + return customOptions[field] || field + } else { + return field + } + } + async createProductInContentful(product) { try { 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_() @@ -122,46 +139,92 @@ class ContentfulService extends BaseService { const variantLinks = this.getVariantLinks_(variantEntries) const fields = { - title: { + [this.getCustomField("title", "product")]: { "en-US": p.title, }, - variants: { + [this.getCustomField("variants", "product")]: { "en-US": variantLinks, }, - options: { + [this.getCustomField("options", "product")]: { "en-US": p.options, }, - objectId: { + [this.getCustomField("medusaId", "product")]: { "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) { const type = { "en-US": p.type.value, } - fields.type = type + + fields[this.getCustomField("type", "product")] = type } if (p.collection) { const collection = { "en-US": p.collection.title, } - fields.collection = collection + + fields[this.getCustomField("collection", "product")] = collection } if (p.tags) { const tags = { "en-US": p.tags, } - fields.tags = tags + + fields[this.getCustomField("tags", "product")] = tags } if (p.handle) { const handle = { "en-US": p.handle, } - fields.handle = handle + + fields[this.getCustomField("handle", "product")] = handle } const result = await environment.createEntryWithId("product", p.id, { @@ -189,19 +252,19 @@ class ContentfulService extends BaseService { v.id, { fields: { - title: { + [this.getCustomField("title", "variant")]: { "en-US": v.title, }, - sku: { + [this.getCustomField("sku", "variant")]: { "en-US": v.sku, }, - prices: { + [this.getCustomField("prices", "variant")]: { "en-US": v.prices, }, - options: { + [this.getCustomField("options", "variant")]: { "en-US": v.options, }, - objectId: { + [this.getCustomField("medusaId", "variant")]: { "en-US": v.id, }, }, @@ -240,7 +303,14 @@ class ContentfulService extends BaseService { } 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) @@ -248,46 +318,86 @@ class ContentfulService extends BaseService { const productEntryFields = { ...productEntry.fields, - title: { + [this.getCustomField("title", "product")]: { "en-US": p.title, }, - options: { + [this.getCustomField("options", "product")]: { "en-US": p.options, }, - variants: { + [this.getCustomField("variants", "product")]: { "en-US": variantLinks, }, - objectId: { + [this.getCustomField("medusaId", "product")]: { "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) { const type = { "en-US": p.type.value, } - productEntryFields.type = type + + productEntryFields[this.getCustomField("type", "product")] = type } if (p.collection) { const collection = { "en-US": p.collection.title, } - productEntryFields.collection = collection + + productEntryFields[ + this.getCustomField("collection", "product") + ] = collection } if (p.tags) { const tags = { "en-US": p.tags, } - productEntryFields.tags = tags + + productEntryFields[this.getCustomField("tags", "product")] = tags } if (p.handle) { const handle = { "en-US": p.handle, } - productEntryFields.handle = handle + + productEntryFields[this.getCustomField("handle", "product")] = handle } productEntry.fields = productEntryFields @@ -333,19 +443,19 @@ class ContentfulService extends BaseService { const variantEntryFields = { ...variantEntry.fields, - title: { + [this.getCustomField("title", "variant")]: { "en-US": v.title, }, - sku: { + [this.getCustomField("sku", "variant")]: { "en-US": v.sku, }, - options: { + [this.getCustomField("options", "variant")]: { "en-US": v.options, }, - prices: { + [this.getCustomField("prices", "variant")]: { "en-US": v.prices, }, - objectId: { + [this.getCustomField("medusaId", "variant")]: { "en-US": v.id, }, } @@ -377,7 +487,8 @@ class ContentfulService extends BaseService { } let update = { - title: productEntry.fields.title["en-US"], + title: + productEntry.fields[this.getCustomField("title", "product")]["en-US"], } // Get the thumbnail, if present @@ -421,7 +532,10 @@ class ContentfulService extends BaseService { const updatedVariant = await this.productVariantService_.update( variantId, { - title: variantEntry.fields.title["en-US"], + title: + variantEntry.fields[this.getCustomField("title", "variant")][ + "en-US" + ], } ) diff --git a/packages/medusa-plugin-segment/CHANGELOG.md b/packages/medusa-plugin-segment/CHANGELOG.md index a2d98bcd21..87f9ecc0bc 100644 --- a/packages/medusa-plugin-segment/CHANGELOG.md +++ b/packages/medusa-plugin-segment/CHANGELOG.md @@ -3,6 +3,59 @@ All notable changes to this project will be documented in this file. 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) diff --git a/packages/medusa-plugin-segment/package.json b/packages/medusa-plugin-segment/package.json index 67817d6378..e3167bb4b4 100644 --- a/packages/medusa-plugin-segment/package.json +++ b/packages/medusa-plugin-segment/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-segment", - "version": "1.1.4", + "version": "1.1.5", "description": "Segment Analytics", "main": "index.js", "repository": { @@ -42,5 +42,5 @@ "medusa-core-utils": "^1.1.0", "medusa-test-utils": "^1.1.3" }, - "gitHead": "0646bd395a6056657cb0aa93c13699c4a9dbbcdd" + "gitHead": "0c294b7b3acbc1b873aab7e90a8e596bdac48899" } diff --git a/packages/medusa-plugin-segment/src/services/segment.js b/packages/medusa-plugin-segment/src/services/segment.js index eaeddcf5e8..fc4c653142 100644 --- a/packages/medusa-plugin-segment/src/services/segment.js +++ b/packages/medusa-plugin-segment/src/services/segment.js @@ -10,11 +10,12 @@ class SegmentService extends BaseService { * write_key: Segment write key given in Segment dashboard * } */ - constructor({ totalsService }, options) { + constructor({ totalsService, productService }, options) { super() this.totalsService_ = totalsService this.options_ = options + this.productService_ = productService this.analytics_ = new Analytics(options.write_key) } @@ -102,7 +103,7 @@ class SegmentService extends BaseService { tax, discount, coupon, - currency: order.currency_code, + currency: order.currency_code.toUpperCase(), products: await Promise.all( order.items.map(async (item) => { let name = item.title @@ -129,12 +130,20 @@ class SegmentService extends BaseService { variant = item.variant.sku } + const product = await this.productService_.retrieve( + item.variant.product_id, + { relations: ["collection", "type"] } + ) + return { name, variant, price: lineTotal / 100 / item.quantity, reporting_revenue: revenue, product_id: item.variant.product_id, + category: product.collection?.title, + subtitle: product.subtitle, + type: product.type?.value, sku, quantity: item.quantity, } diff --git a/packages/medusa-plugin-segment/src/subscribers/order.js b/packages/medusa-plugin-segment/src/subscribers/order.js index 3ee9ad40c7..c5eac08e03 100644 --- a/packages/medusa-plugin-segment/src/subscribers/order.js +++ b/packages/medusa-plugin-segment/src/subscribers/order.js @@ -5,12 +5,76 @@ class OrderSubscriber { orderService, claimService, returnService, + fulfillmentService, }) { this.orderService_ = orderService this.returnService_ = returnService 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 }) => { const claim = await this.claimService_.retrieve(id, { @@ -74,6 +138,7 @@ class OrderSubscriber { "payments", "fulfillments", "returns", + "items", "gift_cards", "gift_card_transactions", "swaps", @@ -160,6 +225,7 @@ class OrderSubscriber { "shipping_methods", "payments", "fulfillments", + "items", "returns", "gift_cards", "gift_card_transactions", diff --git a/packages/medusa-plugin-sendgrid/CHANGELOG.md b/packages/medusa-plugin-sendgrid/CHANGELOG.md index d8a161b95c..1b7107ad42 100644 --- a/packages/medusa-plugin-sendgrid/CHANGELOG.md +++ b/packages/medusa-plugin-sendgrid/CHANGELOG.md @@ -3,6 +3,18 @@ All notable changes to this project will be documented in this file. 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) diff --git a/packages/medusa-plugin-sendgrid/package.json b/packages/medusa-plugin-sendgrid/package.json index 762389382a..7df27324ba 100644 --- a/packages/medusa-plugin-sendgrid/package.json +++ b/packages/medusa-plugin-sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "medusa-plugin-sendgrid", - "version": "1.1.3", + "version": "1.1.4", "description": "SendGrid transactional emails", "main": "index.js", "repository": { diff --git a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js index 7dc39038c0..0d027700f8 100644 --- a/packages/medusa-plugin-sendgrid/src/services/sendgrid.js +++ b/packages/medusa-plugin-sendgrid/src/services/sendgrid.js @@ -275,7 +275,7 @@ class SendGridService extends NotificationService { }) const shipment = await this.fulfillmentService_.retrieve(fulfillment_id, { - relations: ["items"], + relations: ["items", "tracking_links"], }) return { @@ -283,6 +283,7 @@ class SendGridService extends NotificationService { date: shipment.shipped_at.toDateString(), email: order.email, fulfillment: shipment, + tracking_links: shipment.tracking_links, tracking_number: shipment.tracking_numbers.join(", "), } } @@ -586,7 +587,9 @@ class SendGridService extends NotificationService { 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 { swap, @@ -602,6 +605,7 @@ class SendGridService extends NotificationService { refund_amount: `${this.humanPrice_(refundAmount)} ${currencyCode}`, additional_total: `${this.humanPrice_(additionalTotal)} ${currencyCode}`, fulfillment: shipment, + tracking_links: shipment.tracking_links, tracking_number: shipment.tracking_numbers.join(", "), } } @@ -611,13 +615,16 @@ class SendGridService extends NotificationService { 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 { email: claim.order.email, claim, order: claim.order, fulfillment: shipment, + tracking_links: shipment.tracking_links, tracking_number: shipment.tracking_numbers.join(", "), } } diff --git a/packages/medusa/CHANGELOG.md b/packages/medusa/CHANGELOG.md index 2d5fba459c..890c4e0c76 100644 --- a/packages/medusa/CHANGELOG.md +++ b/packages/medusa/CHANGELOG.md @@ -3,6 +3,47 @@ All notable changes to this project will be documented in this file. 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) diff --git a/packages/medusa/package.json b/packages/medusa/package.json index 8ccd34dcdb..7fd7fd6f15 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -1,6 +1,6 @@ { "name": "@medusajs/medusa", - "version": "1.1.9", + "version": "1.1.11", "description": "E-commerce for JAMstack", "main": "dist/index.js", "repository": { diff --git a/packages/medusa/src/api/routes/admin/customers/list-customers.js b/packages/medusa/src/api/routes/admin/customers/list-customers.js index 566fa61c23..e0bcb9b9c8 100644 --- a/packages/medusa/src/api/routes/admin/customers/list-customers.js +++ b/packages/medusa/src/api/routes/admin/customers/list-customers.js @@ -2,7 +2,7 @@ export default async (req, res) => { try { 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 selector = {} @@ -11,15 +11,23 @@ export default async (req, res) => { selector.q = req.query.q } + let expandFields = [] + if ("expand" in req.query) { + expandFields = req.query.expand.split(",") + } + const listConfig = { - relations: [], + relations: expandFields.length ? expandFields : [], skip: offset, 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) { throw error } diff --git a/packages/medusa/src/api/routes/admin/products/list-products.js b/packages/medusa/src/api/routes/admin/products/list-products.js index e5190416fd..a1aa7b9982 100644 --- a/packages/medusa/src/api/routes/admin/products/list-products.js +++ b/packages/medusa/src/api/routes/admin/products/list-products.js @@ -14,13 +14,23 @@ export default async (req, res) => { 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) { selector.is_giftcard = req.query.is_giftcard === "true" } const listConfig = { - select: defaultFields, - relations: defaultRelations, + select: includeFields.length ? includeFields : defaultFields, + relations: expandFields.length ? expandFields : defaultRelations, skip: offset, take: limit, } diff --git a/packages/medusa/src/api/routes/admin/products/update-product.js b/packages/medusa/src/api/routes/admin/products/update-product.js index d017e36951..a90d759868 100644 --- a/packages/medusa/src/api/routes/admin/products/update-product.js +++ b/packages/medusa/src/api/routes/admin/products/update-product.js @@ -6,6 +6,9 @@ export default async (req, res) => { const schema = Validator.object().keys({ title: Validator.string().optional(), + subtitle: Validator.string() + .optional() + .allow(null, ""), description: Validator.string().optional(), type: Validator.object() .keys({ diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index 63659e200f..46e01c29bb 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -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" @EntityRepository(Product) -export class ProductRepository extends Repository {} +export class ProductRepository extends Repository { + public async findWithRelations( + relations: Array = [], + optionsWithoutRelations: Omit, "relations"> = {} + ): Promise { + 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 = [], + optionsWithoutRelations: Omit, "relations"> = {} + ): Promise { + // Limit 1 + optionsWithoutRelations.take = 1 + + const result = await this.findWithRelations( + relations, + optionsWithoutRelations + ) + return result[0] + } +} diff --git a/packages/medusa/src/services/__tests__/product.js b/packages/medusa/src/services/__tests__/product.js index de1eb7fe78..422bdb42e5 100644 --- a/packages/medusa/src/services/__tests__/product.js +++ b/packages/medusa/src/services/__tests__/product.js @@ -11,7 +11,8 @@ const eventBusService = { describe("ProductService", () => { describe("retrieve", () => { const productRepo = MockRepository({ - findOne: () => Promise.resolve({ id: IdMap.getId("ironman") }), + findOneWithRelations: () => + Promise.resolve({ id: IdMap.getId("ironman") }), }) const productService = new ProductService({ manager: MockManager, @@ -25,8 +26,8 @@ describe("ProductService", () => { it("successfully retrieves a product", async () => { const result = await productService.retrieve(IdMap.getId("ironman")) - expect(productRepo.findOne).toHaveBeenCalledTimes(1) - expect(productRepo.findOne).toHaveBeenCalledWith({ + expect(productRepo.findOneWithRelations).toHaveBeenCalledTimes(1) + expect(productRepo.findOneWithRelations).toHaveBeenCalledWith(undefined, { where: { id: IdMap.getId("ironman") }, }) @@ -42,7 +43,7 @@ describe("ProductService", () => { options: [], collection: { id: IdMap.getId("cat"), title: "Suits" }, }), - findOne: () => ({ + findOneWithRelations: () => ({ id: IdMap.getId("ironman"), title: "Suit", options: [], @@ -137,7 +138,7 @@ describe("ProductService", () => { describe("update", () => { const productRepository = MockRepository({ - findOne: query => { + findOneWithRelations: (rels, query) => { if (query.where.id === IdMap.getId("ironman&co")) { return Promise.resolve({ id: IdMap.getId("ironman&co"), @@ -322,7 +323,7 @@ describe("ProductService", () => { describe("addOption", () => { const productRepository = MockRepository({ - findOne: query => + findOneWithRelations: query => Promise.resolve({ id: IdMap.getId("ironman"), options: [{ title: "Color" }], @@ -395,7 +396,7 @@ describe("ProductService", () => { describe("reorderVariants", () => { const productRepository = MockRepository({ - findOne: query => + findOneWithRelations: query => Promise.resolve({ id: IdMap.getId("ironman"), variants: [{ id: IdMap.getId("green") }, { id: IdMap.getId("blue") }], @@ -453,7 +454,7 @@ describe("ProductService", () => { describe("reorderOptions", () => { const productRepository = MockRepository({ - findOne: query => + findOneWithRelations: query => Promise.resolve({ id: IdMap.getId("ironman"), options: [ @@ -519,7 +520,7 @@ describe("ProductService", () => { describe("updateOption", () => { const productRepository = MockRepository({ - findOne: query => + findOneWithRelations: query => Promise.resolve({ id: IdMap.getId("ironman"), options: [ @@ -594,7 +595,7 @@ describe("ProductService", () => { describe("deleteOption", () => { const productRepository = MockRepository({ - findOne: query => + findOneWithRelations: query => Promise.resolve({ id: IdMap.getId("ironman"), variants: [ diff --git a/packages/medusa/src/services/customer.js b/packages/medusa/src/services/customer.js index 66ffe16eed..cd9a3b47ca 100644 --- a/packages/medusa/src/services/customer.js +++ b/packages/medusa/src/services/customer.js @@ -160,6 +160,50 @@ class CustomerService extends BaseService { 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 {Promise} the result of the count operation diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 7e38385f2a..38b3fb4696 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -93,6 +93,17 @@ class ProductService extends BaseService { 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) { const where = query.where @@ -122,7 +133,7 @@ class ProductService extends BaseService { } } - return productRepo.find(query) + return productRepo.findWithRelations(rels, query) } /** @@ -143,20 +154,33 @@ class ProductService extends BaseService { * @return {Promise} the result of the find one operation. */ async retrieve(productId, config = {}) { - return this.atomicPhase_(async manager => { - const productRepo = manager.getCustomRepository(this.productRepository_) - const validatedId = this.validateId_(productId) - const query = this.buildQuery_({ id: validatedId }, config) - const product = await productRepo.findOne(query) - if (!product) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `Product with id: ${productId} was not found` - ) - } + const productRepo = this.manager_.getCustomRepository( + this.productRepository_ + ) + const validatedId = this.validateId_(productId) - return product - }) + 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) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Product with id: ${productId} was not found` + ) + } + + return product } /**