feat(medusa): cart context (#201)
- Adds a context field to Cart - context is automatically populated with ip + user agent - context can be updated via POST /store/cart/:id or set when creating via POST /store/cart
This commit is contained in:
@@ -72,6 +72,26 @@ describe("/store/carts", () => {
|
|||||||
const getRes = await api.post(`/store/carts/${response.data.cart.id}`);
|
const getRes = await api.post(`/store/carts/${response.data.cart.id}`);
|
||||||
expect(getRes.status).toEqual(200);
|
expect(getRes.status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates a cart with context", async () => {
|
||||||
|
const api = useApi();
|
||||||
|
const response = await api.post("/store/carts", {
|
||||||
|
context: {
|
||||||
|
test_id: "test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
|
||||||
|
const getRes = await api.post(`/store/carts/${response.data.cart.id}`);
|
||||||
|
expect(getRes.status).toEqual(200);
|
||||||
|
|
||||||
|
const cart = getRes.data.cart;
|
||||||
|
expect(cart.context).toEqual({
|
||||||
|
ip: "::ffff:127.0.0.1",
|
||||||
|
user_agent: "axios/0.21.1",
|
||||||
|
test_id: "test",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("POST /store/carts/:id", () => {
|
describe("POST /store/carts/:id", () => {
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
const glob = require(`glob`);
|
// API
|
||||||
|
|
||||||
const pkgs = glob
|
|
||||||
.sync(`${__dirname}/*/`)
|
|
||||||
.map((p) => p.replace(__dirname, `<rootDir>/integration-tests`));
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
rootDir: `../`,
|
|
||||||
roots: pkgs,
|
|
||||||
testPathIgnorePatterns: [
|
testPathIgnorePatterns: [
|
||||||
`/examples/`,
|
`/examples/`,
|
||||||
`/www/`,
|
`/www/`,
|
||||||
@@ -17,6 +11,6 @@ module.exports = {
|
|||||||
`__testfixtures__`,
|
`__testfixtures__`,
|
||||||
`.cache`,
|
`.cache`,
|
||||||
],
|
],
|
||||||
transform: { "^.+\\.[jt]s$": `<rootDir>/jest-transformer.js` },
|
transform: { "^.+\\.[jt]s$": `../../jest-transformer.js` },
|
||||||
setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],
|
setupFilesAfterEnv: ["../setup.js"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"test": "jest --runInBand",
|
||||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/medusa": "^1.1.3",
|
"@medusajs/medusa": "1.1.11-dev-1615544779907",
|
||||||
"medusa-interfaces": "^1.1.0",
|
"medusa-interfaces": "1.1.1-dev-1615544779907",
|
||||||
"typeorm": "^0.2.31"
|
"typeorm": "^0.2.31"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.12.10",
|
"@babel/cli": "^7.12.10",
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
"@babel/node": "^7.12.10",
|
"@babel/node": "^7.12.10",
|
||||||
"babel-preset-medusa-package": "^1.1.0"
|
"babel-preset-medusa-package": "1.1.0-dev-1615544779907",
|
||||||
|
"jest": "^26.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const express = require("express");
|
const express = require("express");
|
||||||
const getPort = require("get-port");
|
const getPort = require("get-port");
|
||||||
|
const importFrom = require("import-from");
|
||||||
const loaders = require("@medusajs/medusa/dist/loaders").default;
|
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
|
const loaders = importFrom(process.cwd(), "@medusajs/medusa/dist/loaders")
|
||||||
|
.default;
|
||||||
|
|
||||||
const { dbConnection } = await loaders({
|
const { dbConnection } = await loaders({
|
||||||
directory: path.resolve(process.cwd()),
|
directory: path.resolve(process.cwd()),
|
||||||
expressApp: app,
|
expressApp: app,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^5.1.1",
|
||||||
|
"import-from": "^3.0.0",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"lerna": "^3.22.1",
|
"lerna": "^3.22.1",
|
||||||
"mongoose": "^5.10.15",
|
"mongoose": "^5.10.15",
|
||||||
|
|||||||
@@ -17,13 +17,13 @@
|
|||||||
"author": "Sebastian Rindom",
|
"author": "Sebastian Rindom",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.7.5",
|
"@babel/cli": "^7.13.0",
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "^7.13.8",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||||
"@babel/plugin-transform-classes": "^7.9.5",
|
"@babel/plugin-transform-classes": "^7.9.5",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||||
"@babel/preset-env": "^7.7.5",
|
"@babel/preset-env": "^7.13.9",
|
||||||
"@babel/runtime": "^7.9.6",
|
"@babel/runtime": "^7.13.9",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"jest": "^25.5.2",
|
"jest": "^25.5.2",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,9 @@ class SegmentService extends BaseService {
|
|||||||
* e.g.
|
* e.g.
|
||||||
* {
|
* {
|
||||||
* write_key: Segment write key given in Segment dashboard
|
* write_key: Segment write key given in Segment dashboard
|
||||||
|
* use_ga_id: If set to true the plugin will look for a ga_id in the cart
|
||||||
|
* context if present this id will be used as the Google Analytics
|
||||||
|
* client id.
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
constructor({ totalsService, productService }, options) {
|
constructor({ totalsService, productService }, options) {
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ class OrderSubscriber {
|
|||||||
segmentService,
|
segmentService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
orderService,
|
orderService,
|
||||||
|
cartService,
|
||||||
claimService,
|
claimService,
|
||||||
returnService,
|
returnService,
|
||||||
fulfillmentService,
|
fulfillmentService,
|
||||||
}) {
|
}) {
|
||||||
this.orderService_ = orderService
|
this.orderService_ = orderService
|
||||||
|
|
||||||
|
this.cartService_ = cartService
|
||||||
|
|
||||||
this.returnService_ = returnService
|
this.returnService_ = returnService
|
||||||
|
|
||||||
this.claimService_ = claimService
|
this.claimService_ = claimService
|
||||||
|
|
||||||
this.fulfillmentService_ = fulfillmentService
|
this.fulfillmentService_ = fulfillmentService
|
||||||
|
|
||||||
eventBusService.subscribe(
|
eventBusService.subscribe(
|
||||||
@@ -239,12 +243,46 @@ class OrderSubscriber {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const eventContext = {}
|
||||||
|
const integrations = {}
|
||||||
|
|
||||||
|
if (order.cart_id) {
|
||||||
|
try {
|
||||||
|
const cart = await this.cartService_.retrieve(order.cart_id, {
|
||||||
|
select: ["context"],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (cart.context) {
|
||||||
|
if (cart.context.ip) {
|
||||||
|
eventContext.ip = cart.context.ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cart.context.user_agent) {
|
||||||
|
eventContext.user_agent = cart.context.user_agent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentService.options_ && segmentService.options_.use_ga_id) {
|
||||||
|
if (cart.context.ga_id) {
|
||||||
|
integrations["Google Analytics"] = {
|
||||||
|
clientId: cart.context.ga_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err)
|
||||||
|
console.warn("Failed to gather context for order")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const orderData = await segmentService.buildOrder(order)
|
const orderData = await segmentService.buildOrder(order)
|
||||||
const orderEvent = {
|
const orderEvent = {
|
||||||
event: "Order Completed",
|
event: "Order Completed",
|
||||||
userId: order.customer_id,
|
userId: order.customer_id,
|
||||||
properties: orderData,
|
properties: orderData,
|
||||||
timestamp: order.created_at,
|
timestamp: order.created_at,
|
||||||
|
context: eventContext,
|
||||||
|
integrations,
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentService.track(orderEvent)
|
segmentService.track(orderEvent)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"medusa-interfaces": "1.x",
|
"medusa-interfaces": "1.x",
|
||||||
"mongoose": "5.x"
|
"typeorm": "0.2.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/plugin-transform-classes": "^7.9.5",
|
"@babel/plugin-transform-classes": "^7.9.5",
|
||||||
@@ -82,7 +82,6 @@
|
|||||||
"request-ip": "^2.1.3",
|
"request-ip": "^2.1.3",
|
||||||
"resolve-cwd": "^3.0.0",
|
"resolve-cwd": "^3.0.0",
|
||||||
"scrypt-kdf": "^2.0.1",
|
"scrypt-kdf": "^2.0.1",
|
||||||
"typeorm": "^0.2.29",
|
|
||||||
"ulid": "^2.3.0",
|
"ulid": "^2.3.0",
|
||||||
"uuid": "^8.3.1",
|
"uuid": "^8.3.1",
|
||||||
"winston": "^3.2.1"
|
"winston": "^3.2.1"
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ describe("POST /store/carts", () => {
|
|||||||
subject = await request("POST", `/store/carts`, {
|
subject = await request("POST", `/store/carts`, {
|
||||||
payload: {
|
payload: {
|
||||||
region_id: IdMap.getId("testRegion"),
|
region_id: IdMap.getId("testRegion"),
|
||||||
|
context: {
|
||||||
|
clientId: "test",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -23,6 +26,11 @@ describe("POST /store/carts", () => {
|
|||||||
expect(CartServiceMock.create).toHaveBeenCalledTimes(1)
|
expect(CartServiceMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(CartServiceMock.create).toHaveBeenCalledWith({
|
expect(CartServiceMock.create).toHaveBeenCalledWith({
|
||||||
region_id: IdMap.getId("testRegion"),
|
region_id: IdMap.getId("testRegion"),
|
||||||
|
context: {
|
||||||
|
ip: "::ffff:127.0.0.1",
|
||||||
|
user_agent: "node-superagent/3.8.3",
|
||||||
|
clientId: "test",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import reqIp from "request-ip"
|
||||||
import { Validator, MedusaError } from "medusa-core-utils"
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
import { defaultFields, defaultRelations } from "./"
|
import { defaultFields, defaultRelations } from "./"
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ import { defaultFields, defaultRelations } from "./"
|
|||||||
* quantity:
|
* quantity:
|
||||||
* description: The quantity of the Product Variant to add
|
* description: The quantity of the Product Variant to add
|
||||||
* type: integer
|
* type: integer
|
||||||
|
* context:
|
||||||
|
* description: "An optional object to provide context to the Cart. The `context` field is automatically populated with `ip` and `user_agent`"
|
||||||
|
* type: object
|
||||||
* tags:
|
* tags:
|
||||||
* - Cart
|
* - Cart
|
||||||
* responses:
|
* responses:
|
||||||
@@ -53,6 +57,7 @@ export default async (req, res) => {
|
|||||||
quantity: Validator.number().required(),
|
quantity: Validator.number().required(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
context: Validator.object().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { value, error } = schema.validate(req.body)
|
const { value, error } = schema.validate(req.body)
|
||||||
@@ -60,6 +65,11 @@ export default async (req, res) => {
|
|||||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reqContext = {
|
||||||
|
ip: reqIp.getClientIp(req),
|
||||||
|
user_agent: req.get("user-agent"),
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const lineItemService = req.scope.resolve("lineItemService")
|
const lineItemService = req.scope.resolve("lineItemService")
|
||||||
const cartService = req.scope.resolve("cartService")
|
const cartService = req.scope.resolve("cartService")
|
||||||
@@ -77,6 +87,10 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const toCreate = {
|
const toCreate = {
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
|
context: {
|
||||||
|
...reqContext,
|
||||||
|
...value.context,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.user && req.user.customer_id) {
|
if (req.user && req.user.customer_id) {
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ import { defaultFields, defaultRelations } from "./"
|
|||||||
* customer_id:
|
* customer_id:
|
||||||
* description: "The id of the Customer to associate the Cart with."
|
* description: "The id of the Customer to associate the Cart with."
|
||||||
* type: string
|
* type: string
|
||||||
|
* context:
|
||||||
|
* description: "An optional object to provide context to the Cart."
|
||||||
|
* type: object
|
||||||
* tags:
|
* tags:
|
||||||
* - Cart
|
* - Cart
|
||||||
* responses:
|
* responses:
|
||||||
@@ -85,6 +88,7 @@ export default async (req, res) => {
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
customer_id: Validator.string().optional(),
|
customer_id: Validator.string().optional(),
|
||||||
|
context: Validator.object().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { value, error } = schema.validate(req.body)
|
const { value, error } = schema.validate(req.body)
|
||||||
|
|||||||
13
packages/medusa/src/migrations/1614684597235-cart_context.ts
Normal file
13
packages/medusa/src/migrations/1614684597235-cart_context.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||||
|
|
||||||
|
export class cartContext1614684597235 implements MigrationInterface {
|
||||||
|
name = "cartContext1614684597235"
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "cart" ADD "context" jsonb`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "cart" DROP COLUMN "context"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -243,6 +243,9 @@ export class Cart {
|
|||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
idempotency_key: string
|
idempotency_key: string
|
||||||
|
|
||||||
|
@Column({ type: "jsonb", nullable: true })
|
||||||
|
context: any
|
||||||
|
|
||||||
// Total fields
|
// Total fields
|
||||||
shipping_total: number
|
shipping_total: number
|
||||||
discount_total: number
|
discount_total: number
|
||||||
|
|||||||
@@ -641,6 +641,14 @@ class CartService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("context" in update) {
|
||||||
|
const prevContext = cart.context || {}
|
||||||
|
cart.context = {
|
||||||
|
...prevContext,
|
||||||
|
...update.context,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = await cartRepo.save(cart)
|
const result = await cartRepo.save(cart)
|
||||||
|
|
||||||
if ("email" in update || "customer_id" in update) {
|
if ("email" in update || "customer_id" in update) {
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ FIXTURE_PATTERN=$1
|
|||||||
|
|
||||||
lerna run build
|
lerna run build
|
||||||
|
|
||||||
|
medusa-dev --set-path-to-repo .
|
||||||
|
|
||||||
cd docs-util/fixture-gen
|
cd docs-util/fixture-gen
|
||||||
|
|
||||||
yarn
|
medusa-dev --force-install --scan-once
|
||||||
yarn link @medusajs/medusa medusa-interfaces
|
|
||||||
|
|
||||||
cd ../..
|
cd ../..
|
||||||
|
|
||||||
|
|||||||
10
scripts/integration-tests.sh
Executable file
10
scripts/integration-tests.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
medusa-dev --set-path-to-repo .
|
||||||
|
|
||||||
|
cd integration-tests/api
|
||||||
|
|
||||||
|
medusa-dev --force-install --scan-once
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
|
||||||
@@ -5392,6 +5392,13 @@ import-fresh@^2.0.0:
|
|||||||
caller-path "^2.0.0"
|
caller-path "^2.0.0"
|
||||||
resolve-from "^3.0.0"
|
resolve-from "^3.0.0"
|
||||||
|
|
||||||
|
import-from@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966"
|
||||||
|
integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==
|
||||||
|
dependencies:
|
||||||
|
resolve-from "^5.0.0"
|
||||||
|
|
||||||
import-local@^2.0.0:
|
import-local@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
|
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
|
||||||
|
|||||||
Reference in New Issue
Block a user