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/integration-tests/api/src/services/test-not.js b/integration-tests/api/src/services/test-not.js new file mode 100644 index 0000000000..77f3d9340e --- /dev/null +++ b/integration-tests/api/src/services/test-not.js @@ -0,0 +1,19 @@ +import { NotificationService } from "medusa-interfaces"; + +class TestNotiService extends NotificationService { + static identifier = "test-not"; + + constructor() { + super(); + } + + async sendNotification() { + return Promise.resolve(); + } + + async resendNotification() { + return Promise.resolve(); + } +} + +export default TestNotiService; 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-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js b/packages/medusa-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js new file mode 100644 index 0000000000..71cbe37e95 --- /dev/null +++ b/packages/medusa-fulfillment-webshipper/src/services/__tests__/webshipper-fulfillment.js @@ -0,0 +1,211 @@ +import WebshipperFulfillmentService from "../webshipper-fulfillment" + +describe("WebshipperFulfillmentService", () => { + const orderService = { + createShipment: jest.fn(), + } + const swapService = { + createShipment: jest.fn(), + } + const claimService = { + createShipment: jest.fn(), + } + + describe("handleWebhook", () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it("creates an order shipment", async () => { + const webshipper = new WebshipperFulfillmentService( + { + orderService, + claimService, + swapService, + }, + {} + ) + + webshipper.retrieveRelationship = () => { + return { + data: { + attributes: { + ext_ref: "order_test.ful_test", + }, + }, + } + } + + const body = { + data: { + attributes: { + tracking_links: [ + { + url: "https://test/1134", + number: "12324245345", + }, + { + url: "https://test/1234", + number: "12324245345", + }, + ], + }, + relationships: { + order: { + id: "order", + }, + }, + }, + } + + await webshipper.handleWebhook("", body) + + expect(claimService.createShipment).toHaveBeenCalledTimes(0) + expect(swapService.createShipment).toHaveBeenCalledTimes(0) + + expect(orderService.createShipment).toHaveBeenCalledTimes(1) + expect(orderService.createShipment).toHaveBeenCalledWith( + "order_test", + "ful_test", + [ + { + url: "https://test/1134", + tracking_number: "12324245345", + }, + { + url: "https://test/1234", + tracking_number: "12324245345", + }, + ] + ) + }) + + it("creates a claim shipment", async () => { + const webshipper = new WebshipperFulfillmentService( + { + orderService, + claimService, + swapService, + }, + {} + ) + + webshipper.retrieveRelationship = () => { + return { + data: { + attributes: { + ext_ref: "claim_test.ful_test", + }, + }, + } + } + + const body = { + data: { + attributes: { + tracking_links: [ + { + url: "https://test/1134", + number: "12324245345", + }, + { + url: "https://test/1234", + number: "12324245345", + }, + ], + }, + relationships: { + order: { + id: "order", + }, + }, + }, + } + + await webshipper.handleWebhook("", body) + + expect(orderService.createShipment).toHaveBeenCalledTimes(0) + expect(swapService.createShipment).toHaveBeenCalledTimes(0) + + expect(claimService.createShipment).toHaveBeenCalledTimes(1) + expect(claimService.createShipment).toHaveBeenCalledWith( + "claim_test", + "ful_test", + [ + { + url: "https://test/1134", + tracking_number: "12324245345", + }, + { + url: "https://test/1234", + tracking_number: "12324245345", + }, + ] + ) + }) + + it("creates a swap shipment", async () => { + const webshipper = new WebshipperFulfillmentService( + { + orderService, + claimService, + swapService, + }, + {} + ) + + webshipper.retrieveRelationship = () => { + return { + data: { + attributes: { + ext_ref: "swap_test.ful_test", + }, + }, + } + } + + const body = { + data: { + attributes: { + tracking_links: [ + { + url: "https://test/1134", + number: "12324245345", + }, + { + url: "https://test/1234", + number: "12324245345", + }, + ], + }, + relationships: { + order: { + id: "order", + }, + }, + }, + } + + await webshipper.handleWebhook("", body) + + expect(orderService.createShipment).toHaveBeenCalledTimes(0) + expect(claimService.createShipment).toHaveBeenCalledTimes(0) + + expect(swapService.createShipment).toHaveBeenCalledTimes(1) + expect(swapService.createShipment).toHaveBeenCalledWith( + "swap_test", + "ful_test", + [ + { + url: "https://test/1134", + tracking_number: "12324245345", + }, + { + url: "https://test/1234", + tracking_number: "12324245345", + }, + ] + ) + }) + }) +}) diff --git a/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js b/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js index 43d96fd573..3504b906bc 100644 --- a/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js +++ b/packages/medusa-fulfillment-webshipper/src/services/webshipper-fulfillment.js @@ -363,9 +363,10 @@ class WebshipperFulfillmentService extends FulfillmentService { body.data.relationships.order ) if (wsOrder.data && wsOrder.data.attributes.ext_ref) { - const trackingNumbers = body.data.attributes.tracking_links.map( - (l) => l.number - ) + const trackingLinks = body.data.attributes.tracking_links.map((l) => ({ + url: l.url, + tracking_number: l.number, + })) const [orderId, fulfillmentIndex] = wsOrder.data.attributes.ext_ref.split( "." ) @@ -375,7 +376,7 @@ class WebshipperFulfillmentService extends FulfillmentService { return this.swapService_.createShipment( orderId, fulfillmentIndex, - trackingNumbers + trackingLinks ) } else { const swap = await this.swapService_.retrieve(orderId.substring(1), { @@ -385,21 +386,21 @@ class WebshipperFulfillmentService extends FulfillmentService { return this.swapService_.createShipment( swap.id, fulfillment.id, - trackingNumbers + trackingLinks ) } } else if (orderId.charAt(0).toLowerCase() === "c") { return this.claimService_.createShipment( orderId, fulfillmentIndex, - trackingNumbers + trackingLinks ) } else { if (fulfillmentIndex.startsWith("ful")) { return this.orderService_.createShipment( orderId, fulfillmentIndex, - trackingNumbers + trackingLinks ) } else { const order = await this.orderService_.retrieve(orderId, { @@ -411,7 +412,7 @@ class WebshipperFulfillmentService extends FulfillmentService { return this.orderService_.createShipment( order.id, fulfillment.id, - trackingNumbers + trackingLinks ) } } 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 6683f80e06..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,18 +2,32 @@ 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 = {} + + if ("q" in req.query) { + 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({}, 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/orders/__tests__/get-order.js b/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js index 743e5c4f40..eaa0fc0136 100644 --- a/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js +++ b/packages/medusa/src/api/routes/admin/orders/__tests__/get-order.js @@ -10,6 +10,8 @@ const defaultRelations = [ "shipping_methods", "payments", "fulfillments", + "fulfillments.tracking_links", + "fulfillments.items", "returns", "gift_cards", "gift_card_transactions", diff --git a/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js index 840270e91a..d4c6c36a0c 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-claim-shipment.js @@ -23,7 +23,7 @@ export default async (req, res) => { await claimService.createShipment( claim_id, value.fulfillment_id, - value.tracking_numbers + value.tracking_numbers.map(n => ({ tracking_number: n })) ) const order = await orderService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/admin/orders/create-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-shipment.js index 24e0ebe199..2a5a097641 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-shipment.js @@ -22,7 +22,7 @@ export default async (req, res) => { await orderService.createShipment( id, value.fulfillment_id, - value.tracking_numbers + value.tracking_numbers.map(n => ({ tracking_number: n })) ) const order = await orderService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js b/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js index 700e1599e0..a407570f81 100644 --- a/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js +++ b/packages/medusa/src/api/routes/admin/orders/create-swap-shipment.js @@ -23,7 +23,7 @@ export default async (req, res) => { await swapService.createShipment( swap_id, value.fulfillment_id, - value.tracking_numbers + value.tracking_numbers.map(n => ({ tracking_number: n })) ) const order = await orderService.retrieve(id, { diff --git a/packages/medusa/src/api/routes/admin/orders/index.js b/packages/medusa/src/api/routes/admin/orders/index.js index ffbc5023d9..89278a310a 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.js +++ b/packages/medusa/src/api/routes/admin/orders/index.js @@ -188,6 +188,8 @@ export const defaultRelations = [ "shipping_methods", "payments", "fulfillments", + "fulfillments.tracking_links", + "fulfillments.items", "returns", "gift_cards", "gift_card_transactions", @@ -271,6 +273,7 @@ export const allowedRelations = [ "shipping_methods", "payments", "fulfillments", + "fulfillments.tracking_links", "returns", "claims", "swaps", 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 94a7510e43..ecb614cc12 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/migrations/1613656135167-tracking_links.ts b/packages/medusa/src/migrations/1613656135167-tracking_links.ts new file mode 100644 index 0000000000..4755a24c04 --- /dev/null +++ b/packages/medusa/src/migrations/1613656135167-tracking_links.ts @@ -0,0 +1,16 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class trackingLinks1613656135167 implements MigrationInterface { + name = 'trackingLinks1613656135167' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "tracking_link" ("id" character varying NOT NULL, "url" character varying, "tracking_number" character varying NOT NULL, "fulfillment_id" character varying NOT NULL, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deleted_at" TIMESTAMP WITH TIME ZONE, "metadata" jsonb, "idempotency_key" character varying, CONSTRAINT "PK_fcfd77feb9012ec2126d7c0bfb6" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "tracking_link" ADD CONSTRAINT "FK_471e9e4c96e02ba209a307db32b" FOREIGN KEY ("fulfillment_id") REFERENCES "fulfillment"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tracking_link" DROP CONSTRAINT "FK_471e9e4c96e02ba209a307db32b"`); + await queryRunner.query(`DROP TABLE "tracking_link"`); + } + +} diff --git a/packages/medusa/src/models/fulfillment.ts b/packages/medusa/src/models/fulfillment.ts index e071feef3b..be6f4bb8f3 100644 --- a/packages/medusa/src/models/fulfillment.ts +++ b/packages/medusa/src/models/fulfillment.ts @@ -22,6 +22,7 @@ import { FulfillmentProvider } from "./fulfillment-provider" import { FulfillmentItem } from "./fulfillment-item" import { Swap } from "./swap" import { ClaimOrder } from "./claim-order" +import { TrackingLink } from "./tracking-link" @Entity() export class Fulfillment { @@ -76,6 +77,13 @@ export class Fulfillment { ) items: FulfillmentItem[] + @OneToMany( + () => TrackingLink, + tl => tl.fulfillment, + { cascade: ["insert"] } + ) + tracking_links: TrackingLink[] + @Column({ type: "jsonb", default: [] }) tracking_numbers: string[] diff --git a/packages/medusa/src/models/tracking-link.ts b/packages/medusa/src/models/tracking-link.ts new file mode 100644 index 0000000000..ded8e9bc1e --- /dev/null +++ b/packages/medusa/src/models/tracking-link.ts @@ -0,0 +1,63 @@ +import { + Entity, + Index, + BeforeInsert, + Column, + DeleteDateColumn, + CreateDateColumn, + UpdateDateColumn, + PrimaryColumn, + OneToOne, + OneToMany, + ManyToOne, + ManyToMany, + JoinColumn, + JoinTable, +} from "typeorm" +import { ulid } from "ulid" + +import { Fulfillment } from "./fulfillment" + +@Entity() +export class TrackingLink { + @PrimaryColumn() + id: string + + @Column({ nullable: true }) + url: string + + @Column() + tracking_number: string + + @Column() + fulfillment_id: string + + @ManyToOne( + () => Fulfillment, + ful => ful.tracking_links + ) + @JoinColumn({ name: "fulfillment_id" }) + fulfillment: Fulfillment + + @CreateDateColumn({ type: "timestamptz" }) + created_at: Date + + @UpdateDateColumn({ type: "timestamptz" }) + updated_at: Date + + @DeleteDateColumn({ type: "timestamptz" }) + deleted_at: Date + + @Column({ type: "jsonb", nullable: true }) + metadata: any + + @Column({ nullable: true }) + idempotency_key: string + + @BeforeInsert() + private beforeInsert() { + if (this.id) return + const id = ulid() + this.id = `tlink_${id}` + } +} 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/repositories/tracking-link.ts b/packages/medusa/src/repositories/tracking-link.ts new file mode 100644 index 0000000000..785321a045 --- /dev/null +++ b/packages/medusa/src/repositories/tracking-link.ts @@ -0,0 +1,5 @@ +import { EntityRepository, Repository } from "typeorm" +import { TrackingLink } from "../models/tracking-link" + +@EntityRepository(TrackingLink) +export class TrackingLinkRepository extends Repository {} diff --git a/packages/medusa/src/services/__tests__/fulfillment.js b/packages/medusa/src/services/__tests__/fulfillment.js index 2d20deadf0..2c178ad705 100644 --- a/packages/medusa/src/services/__tests__/fulfillment.js +++ b/packages/medusa/src/services/__tests__/fulfillment.js @@ -95,6 +95,7 @@ describe("FulfillmentService", () => { }) describe("createShipment", () => { + const trackingLinkRepository = MockRepository({ create: c => c }) const fulfillmentRepository = MockRepository({ findOne: () => Promise.resolve({ id: IdMap.getId("fulfillment") }), }) @@ -102,6 +103,7 @@ describe("FulfillmentService", () => { const fulfillmentService = new FulfillmentService({ manager: MockManager, fulfillmentRepository, + trackingLinkRepository, }) const now = new Date() @@ -113,14 +115,17 @@ describe("FulfillmentService", () => { it("calls order model functions", async () => { await fulfillmentService.createShipment( IdMap.getId("fulfillment"), - ["1234", "2345"], + [{ tracking_number: "1234" }, { tracking_number: "2345" }], {} ) expect(fulfillmentRepository.save).toHaveBeenCalledTimes(1) expect(fulfillmentRepository.save).toHaveBeenCalledWith({ id: IdMap.getId("fulfillment"), - tracking_numbers: ["1234", "2345"], + tracking_links: [ + { tracking_number: "1234" }, + { tracking_number: "2345" }, + ], metadata: {}, shipped_at: now, }) diff --git a/packages/medusa/src/services/__tests__/order.js b/packages/medusa/src/services/__tests__/order.js index d2c270fdb0..262504ea33 100644 --- a/packages/medusa/src/services/__tests__/order.js +++ b/packages/medusa/src/services/__tests__/order.js @@ -1182,14 +1182,14 @@ describe("OrderService", () => { await orderService.createShipment( IdMap.getId("test"), IdMap.getId("fulfillment"), - ["1234", "2345"], + [{ tracking_number: "1234" }, { tracking_number: "2345" }], {} ) expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1) expect(fulfillmentService.createShipment).toHaveBeenCalledWith( IdMap.getId("fulfillment"), - ["1234", "2345"], + [{ tracking_number: "1234" }, { tracking_number: "2345" }], {} ) 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/claim.js b/packages/medusa/src/services/claim.js index f8aa1db3ae..ccc2b663e5 100644 --- a/packages/medusa/src/services/claim.js +++ b/packages/medusa/src/services/claim.js @@ -418,7 +418,7 @@ class ClaimService extends BaseService { }) } - async createShipment(id, fulfillmentId, trackingNumbers, metadata = []) { + async createShipment(id, fulfillmentId, trackingLinks, metadata = []) { return this.atomicPhase_(async manager => { const claim = await this.retrieve(id, { relations: ["additional_items"], @@ -426,7 +426,7 @@ class ClaimService extends BaseService { const shipment = await this.fulfillmentService_ .withTransaction(manager) - .createShipment(fulfillmentId, trackingNumbers, metadata) + .createShipment(fulfillmentId, trackingLinks, metadata) claim.fulfillment_status = "shipped" diff --git a/packages/medusa/src/services/customer.js b/packages/medusa/src/services/customer.js index 6db5092f03..486e89c210 100644 --- a/packages/medusa/src/services/customer.js +++ b/packages/medusa/src/services/customer.js @@ -3,6 +3,7 @@ import Scrypt from "scrypt-kdf" import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { Brackets } from "typeorm" /** * Provides layer to manipulate customers. @@ -132,6 +133,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/fulfillment.js b/packages/medusa/src/services/fulfillment.js index 0548ff2510..b05363978d 100644 --- a/packages/medusa/src/services/fulfillment.js +++ b/packages/medusa/src/services/fulfillment.js @@ -11,6 +11,7 @@ class FulfillmentService extends BaseService { manager, totalsService, fulfillmentRepository, + trackingLinkRepository, shippingProfileService, lineItemService, fulfillmentProviderService, @@ -26,6 +27,9 @@ class FulfillmentService extends BaseService { /** @private @const {FulfillmentRepository} */ this.fulfillmentRepository_ = fulfillmentRepository + /** @private @const {TrackingLinkRepository} */ + this.trackingLinkRepository_ = trackingLinkRepository + /** @private @const {ShippingProfileService} */ this.shippingProfileService_ = shippingProfileService @@ -44,6 +48,7 @@ class FulfillmentService extends BaseService { const cloned = new FulfillmentService({ manager: transactionManager, totalsService: this.totalsService_, + trackingLinkRepository: this.trackingLinkRepository_, fulfillmentRepository: this.fulfillmentRepository_, shippingProfileService: this.shippingProfileService_, lineItemService: this.lineItemService_, @@ -235,15 +240,18 @@ class FulfillmentService extends BaseService { * Creates a shipment by marking a fulfillment as shipped. Adds * tracking numbers and potentially more metadata. * @param {Order} fulfillmentId - the fulfillment to ship - * @param {string[]} trackingNumbers - tracking numbers for the shipment + * @param {TrackingLink[]} trackingNumbers - tracking numbers for the shipment * @param {object} metadata - potential metadata to add * @return {Fulfillment} the shipped fulfillment */ - async createShipment(fulfillmentId, trackingNumbers, metadata) { + async createShipment(fulfillmentId, trackingLinks, metadata) { return this.atomicPhase_(async manager => { const fulfillmentRepository = manager.getCustomRepository( this.fulfillmentRepository_ ) + const trackingLinkRepo = manager.getCustomRepository( + this.trackingLinkRepository_ + ) const fulfillment = await this.retrieve(fulfillmentId, { relations: ["items"], @@ -251,7 +259,11 @@ class FulfillmentService extends BaseService { const now = new Date() fulfillment.shipped_at = now - fulfillment.tracking_numbers = trackingNumbers + + fulfillment.tracking_links = trackingLinks.map(tl => + trackingLinkRepo.create(tl) + ) + fulfillment.metadata = { ...fulfillment.metadata, ...metadata, diff --git a/packages/medusa/src/services/order.js b/packages/medusa/src/services/order.js index 4f37c5a030..aee1438081 100644 --- a/packages/medusa/src/services/order.js +++ b/packages/medusa/src/services/order.js @@ -553,13 +553,13 @@ class OrderService extends BaseService { * have been created in regards to the shipment. * @param {string} orderId - the id of the order that has been shipped * @param {string} fulfillmentId - the fulfillment that has now been shipped - * @param {Array} trackingNumbers - array of tracking numebers + * @param {TrackingLink[]} trackingLinks - array of tracking numebers * associated with the shipment * @param {Dictionary} metadata - optional metadata to add to * the fulfillment * @return {order} the resulting order following the update. */ - async createShipment(orderId, fulfillmentId, trackingNumbers, metadata = {}) { + async createShipment(orderId, fulfillmentId, trackingLinks, metadata = {}) { return this.atomicPhase_(async manager => { const order = await this.retrieve(orderId, { relations: ["items"] }) const shipment = await this.fulfillmentService_.retrieve(fulfillmentId) @@ -573,7 +573,7 @@ class OrderService extends BaseService { const shipmentRes = await this.fulfillmentService_ .withTransaction(manager) - .createShipment(fulfillmentId, trackingNumbers, metadata) + .createShipment(fulfillmentId, trackingLinks, metadata) order.fulfillment_status = "shipped" for (const item of order.items) { diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index 1c9e32d6c3..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) } /** @@ -147,8 +158,21 @@ class ProductService extends BaseService { this.productRepository_ ) 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) { throw new MedusaError( MedusaError.Types.NOT_FOUND, diff --git a/packages/medusa/src/services/swap.js b/packages/medusa/src/services/swap.js index bc97b3aff4..5d06abfd1e 100644 --- a/packages/medusa/src/services/swap.js +++ b/packages/medusa/src/services/swap.js @@ -671,12 +671,12 @@ class SwapService extends BaseService { * @param {string} swapId - the id of the swap that has been shipped. * @param {string} fulfillmentId - the id of the specific fulfillment that * has been shipped - * @param {Array} trackingNumbers - the tracking numbers associated + * @param {TrackingLink[]} trackingLinks - the tracking numbers associated * with the shipment * @param {object} metadata - optional metadata to attach to the shipment. * @returns {Promise} the updated swap with new fulfillments and status. */ - async createShipment(swapId, fulfillmentId, trackingNumbers, metadata = {}) { + async createShipment(swapId, fulfillmentId, trackingLinks, metadata = {}) { return this.atomicPhase_(async manager => { const swap = await this.retrieve(swapId, { relations: ["additional_items"], @@ -685,7 +685,7 @@ class SwapService extends BaseService { // Update the fulfillment to register const shipment = await this.fulfillmentService_ .withTransaction(manager) - .createShipment(fulfillmentId, trackingNumbers, metadata) + .createShipment(fulfillmentId, trackingLinks, metadata) swap.fulfillment_status = "shipped"