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}`);
|
||||
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", () => {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
const glob = require(`glob`);
|
||||
|
||||
const pkgs = glob
|
||||
.sync(`${__dirname}/*/`)
|
||||
.map((p) => p.replace(__dirname, `<rootDir>/integration-tests`));
|
||||
// API
|
||||
|
||||
module.exports = {
|
||||
testEnvironment: `node`,
|
||||
rootDir: `../`,
|
||||
roots: pkgs,
|
||||
testPathIgnorePatterns: [
|
||||
`/examples/`,
|
||||
`/www/`,
|
||||
@@ -17,6 +11,6 @@ module.exports = {
|
||||
`__testfixtures__`,
|
||||
`.cache`,
|
||||
],
|
||||
transform: { "^.+\\.[jt]s$": `<rootDir>/jest-transformer.js` },
|
||||
setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],
|
||||
transform: { "^.+\\.[jt]s$": `../../jest-transformer.js` },
|
||||
setupFilesAfterEnv: ["../setup.js"],
|
||||
};
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "jest --runInBand",
|
||||
"build": "babel src -d dist --extensions \".ts,.js\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/medusa": "^1.1.3",
|
||||
"medusa-interfaces": "^1.1.0",
|
||||
"@medusajs/medusa": "1.1.11-dev-1615544779907",
|
||||
"medusa-interfaces": "1.1.1-dev-1615544779907",
|
||||
"typeorm": "^0.2.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^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 express = require("express");
|
||||
const getPort = require("get-port");
|
||||
|
||||
const loaders = require("@medusajs/medusa/dist/loaders").default;
|
||||
const importFrom = require("import-from");
|
||||
|
||||
const initialize = async () => {
|
||||
const app = express();
|
||||
|
||||
const loaders = importFrom(process.cwd(), "@medusajs/medusa/dist/loaders")
|
||||
.default;
|
||||
|
||||
const { dbConnection } = await loaders({
|
||||
directory: path.resolve(process.cwd()),
|
||||
expressApp: app,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"cross-env": "^7.0.2",
|
||||
"express": "^4.17.1",
|
||||
"get-port": "^5.1.1",
|
||||
"import-from": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"lerna": "^3.22.1",
|
||||
"mongoose": "^5.10.15",
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
"author": "Sebastian Rindom",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/cli": "^7.13.0",
|
||||
"@babel/core": "^7.13.8",
|
||||
"@babel/plugin-proposal-class-properties": "^7.13.0",
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"@babel/preset-env": "^7.13.9",
|
||||
"@babel/runtime": "^7.13.9",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.2",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,9 @@ class SegmentService extends BaseService {
|
||||
* e.g.
|
||||
* {
|
||||
* 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) {
|
||||
|
||||
@@ -3,15 +3,19 @@ class OrderSubscriber {
|
||||
segmentService,
|
||||
eventBusService,
|
||||
orderService,
|
||||
cartService,
|
||||
claimService,
|
||||
returnService,
|
||||
fulfillmentService,
|
||||
}) {
|
||||
this.orderService_ = orderService
|
||||
|
||||
this.cartService_ = cartService
|
||||
|
||||
this.returnService_ = returnService
|
||||
|
||||
this.claimService_ = claimService
|
||||
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
|
||||
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 orderEvent = {
|
||||
event: "Order Completed",
|
||||
userId: order.customer_id,
|
||||
properties: orderData,
|
||||
timestamp: order.created_at,
|
||||
context: eventContext,
|
||||
integrations,
|
||||
}
|
||||
|
||||
segmentService.track(orderEvent)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"medusa-interfaces": "1.x",
|
||||
"mongoose": "5.x"
|
||||
"typeorm": "0.2.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-classes": "^7.9.5",
|
||||
@@ -82,7 +82,6 @@
|
||||
"request-ip": "^2.1.3",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"scrypt-kdf": "^2.0.1",
|
||||
"typeorm": "^0.2.29",
|
||||
"ulid": "^2.3.0",
|
||||
"uuid": "^8.3.1",
|
||||
"winston": "^3.2.1"
|
||||
|
||||
@@ -11,6 +11,9 @@ describe("POST /store/carts", () => {
|
||||
subject = await request("POST", `/store/carts`, {
|
||||
payload: {
|
||||
region_id: IdMap.getId("testRegion"),
|
||||
context: {
|
||||
clientId: "test",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -23,6 +26,11 @@ describe("POST /store/carts", () => {
|
||||
expect(CartServiceMock.create).toHaveBeenCalledTimes(1)
|
||||
expect(CartServiceMock.create).toHaveBeenCalledWith({
|
||||
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 { defaultFields, defaultRelations } from "./"
|
||||
|
||||
@@ -31,6 +32,9 @@ import { defaultFields, defaultRelations } from "./"
|
||||
* quantity:
|
||||
* description: The quantity of the Product Variant to add
|
||||
* 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:
|
||||
* - Cart
|
||||
* responses:
|
||||
@@ -53,6 +57,7 @@ export default async (req, res) => {
|
||||
quantity: Validator.number().required(),
|
||||
})
|
||||
.optional(),
|
||||
context: Validator.object().optional(),
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const reqContext = {
|
||||
ip: reqIp.getClientIp(req),
|
||||
user_agent: req.get("user-agent"),
|
||||
}
|
||||
|
||||
try {
|
||||
const lineItemService = req.scope.resolve("lineItemService")
|
||||
const cartService = req.scope.resolve("cartService")
|
||||
@@ -77,6 +87,10 @@ export default async (req, res) => {
|
||||
|
||||
const toCreate = {
|
||||
region_id: regionId,
|
||||
context: {
|
||||
...reqContext,
|
||||
...value.context,
|
||||
},
|
||||
}
|
||||
|
||||
if (req.user && req.user.customer_id) {
|
||||
|
||||
@@ -51,6 +51,9 @@ import { defaultFields, defaultRelations } from "./"
|
||||
* customer_id:
|
||||
* description: "The id of the Customer to associate the Cart with."
|
||||
* type: string
|
||||
* context:
|
||||
* description: "An optional object to provide context to the Cart."
|
||||
* type: object
|
||||
* tags:
|
||||
* - Cart
|
||||
* responses:
|
||||
@@ -85,6 +88,7 @@ export default async (req, res) => {
|
||||
})
|
||||
.optional(),
|
||||
customer_id: Validator.string().optional(),
|
||||
context: Validator.object().optional(),
|
||||
})
|
||||
|
||||
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 })
|
||||
idempotency_key: string
|
||||
|
||||
@Column({ type: "jsonb", nullable: true })
|
||||
context: any
|
||||
|
||||
// Total fields
|
||||
shipping_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)
|
||||
|
||||
if ("email" in update || "customer_id" in update) {
|
||||
|
||||
@@ -4,10 +4,11 @@ FIXTURE_PATTERN=$1
|
||||
|
||||
lerna run build
|
||||
|
||||
medusa-dev --set-path-to-repo .
|
||||
|
||||
cd docs-util/fixture-gen
|
||||
|
||||
yarn
|
||||
yarn link @medusajs/medusa medusa-interfaces
|
||||
medusa-dev --force-install --scan-once
|
||||
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d"
|
||||
|
||||
Reference in New Issue
Block a user