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:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user