fix merge conflicts

This commit is contained in:
olivermrbl
2021-02-27 10:15:42 +01:00
49 changed files with 1212 additions and 133 deletions

126
README.md
View File

@@ -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!

View File

@@ -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",
}),
]),
}),
])
);
});
});
});

View File

@@ -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",
});
};

View File

@@ -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;

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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",
},
]
)
})
})
})

View File

@@ -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
)
}
}

View File

@@ -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")

View File

@@ -206,7 +206,7 @@ class PayPalProviderService extends PaymentService {
return sessionData
} catch (error) {
throw error
return this.createPayment(cart)
}
}

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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": {

View File

@@ -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,
})
})
}
}

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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

View File

@@ -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"
],
}
)

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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,
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -1,6 +1,6 @@
{
"name": "medusa-plugin-sendgrid",
"version": "1.1.3",
"version": "1.1.4",
"description": "SendGrid transactional emails",
"main": "index.js",
"repository": {

View File

@@ -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(", "),
}
}

View File

@@ -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)

View File

@@ -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": {

View File

@@ -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
}

View File

@@ -10,6 +10,8 @@ const defaultRelations = [
"shipping_methods",
"payments",
"fulfillments",
"fulfillments.tracking_links",
"fulfillments.items",
"returns",
"gift_cards",
"gift_card_transactions",

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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",

View File

@@ -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,
}

View File

@@ -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({

View File

@@ -0,0 +1,16 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class trackingLinks1613656135167 implements MigrationInterface {
name = 'trackingLinks1613656135167'
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`ALTER TABLE "tracking_link" DROP CONSTRAINT "FK_471e9e4c96e02ba209a307db32b"`);
await queryRunner.query(`DROP TABLE "tracking_link"`);
}
}

View File

@@ -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[]

View File

@@ -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}`
}
}

View File

@@ -1,5 +1,53 @@
import { EntityRepository, Repository } from "typeorm"
import { flatten, groupBy, map, merge } from "lodash"
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
import { Product } from "../models/product"
@EntityRepository(Product)
export class ProductRepository extends Repository<Product> {}
export class ProductRepository extends Repository<Product> {
public async findWithRelations(
relations: Array<keyof Product> = [],
optionsWithoutRelations: Omit<FindManyOptions<Product>, "relations"> = {}
): Promise<Product[]> {
const entities = await this.find(optionsWithoutRelations)
const entitiesIds = entities.map(({ id }) => id)
const groupedRelations = {}
for (const rel of relations) {
const [topLevel] = rel.split(".")
if (groupedRelations[topLevel]) {
groupedRelations[topLevel].push(rel)
} else {
groupedRelations[topLevel] = [rel]
}
}
const entitiesIdsWithRelations = await Promise.all(
Object.entries(groupedRelations).map(([_, rels]) => {
return this.findByIds(entitiesIds, {
select: ["id"],
relations: rels as string[],
})
})
).then(flatten)
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entitiesAndRelationsById, entityAndRelations =>
merge({}, ...entityAndRelations)
)
}
public async findOneWithRelations(
relations: Array<keyof Product> = [],
optionsWithoutRelations: Omit<FindManyOptions<Product>, "relations"> = {}
): Promise<Product> {
// Limit 1
optionsWithoutRelations.take = 1
const result = await this.findWithRelations(
relations,
optionsWithoutRelations
)
return result[0]
}
}

View File

@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { TrackingLink } from "../models/tracking-link"
@EntityRepository(TrackingLink)
export class TrackingLinkRepository extends Repository<TrackingLink> {}

View File

@@ -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,
})

View File

@@ -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" }],
{}
)

View File

@@ -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: [

View File

@@ -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"

View File

@@ -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

View File

@@ -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,

View File

@@ -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<String>} trackingNumbers - array of tracking numebers
* @param {TrackingLink[]} trackingLinks - array of tracking numebers
* associated with the shipment
* @param {Dictionary<String, String>} 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) {

View File

@@ -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,

View File

@@ -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<string>} 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<Swap>} 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"