fix merge conflicts from develop

This commit is contained in:
olivermrbl committed 2021-05-25 15:44:26 +02:00
commit e3343f74a7
251 files changed
+25580 -4521

No files matched your search

@@ -1,3 +1,4 @@
import _ from "lodash"
import jwt from "jsonwebtoken"
import { Validator } from "medusa-core-utils"
import config from "../../../../config"
@@ -19,7 +20,7 @@ import config from "../../../../config"
* application/json:
* schema:
* properties:
* customer:
* user:
* $ref: "#/components/schemas/user"
*/
export default async (req, res) => {
@@ -46,5 +47,7 @@ export default async (req, res) => {
expiresIn: "24h",
})
res.json({ user: result.user })
const cleanRes = _.omit(result.user, ["password_hash"])
res.json({ user: cleanRes })
}
@@ -1,4 +1,4 @@
import passport from "passport"
import _ from "lodash"
/**
* @oas [get] /auth
@@ -14,11 +14,13 @@ import passport from "passport"
* application/json:
* schema:
* properties:
* customer:
* user:
* $ref: "#/components/schemas/user"
*/
export default async (req, res) => {
const userService = req.scope.resolve("userService")
const user = await userService.retrieve(req.user.userId)
res.status(200).json({ user })
const cleanRes = _.omit(user, ["password_hash"])
res.status(200).json({ user: cleanRes })
}
@@ -35,8 +35,11 @@ describe("POST /admin/discounts/:discount_id/regions/:region_id", () => {
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
@@ -35,8 +35,11 @@ describe("POST /admin/discounts/:discount_id/variants/:variant_id", () => {
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
@@ -11,6 +11,7 @@ describe("POST /admin/discounts", () => {
payload: {
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
@@ -33,6 +34,7 @@ describe("POST /admin/discounts", () => {
expect(DiscountServiceMock.create).toHaveBeenCalledWith({
code: "TEST",
rule: {
description: "Test",
type: "fixed",
value: 10,
allocation: "total",
@@ -51,6 +53,7 @@ describe("POST /admin/discounts", () => {
payload: {
code: "10%OFF",
rule: {
description: "Test",
value: 10,
allocation: "total",
},
@@ -6,8 +6,11 @@ const defaultFields = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
@@ -6,8 +6,11 @@ const defaultFields = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
@@ -6,8 +6,11 @@ const defaultFields = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
@@ -36,6 +36,9 @@ import { MedusaError, Validator } from "medusa-core-utils"
* type: array
* items:
* type: string
* usage_limit:
* type: number
* description: Maximum times the discount can be used
* metadata:
* description: An optional set of key-value pairs to hold additional information.
* type: object
@@ -64,12 +67,14 @@ export default async (req, res) => {
.required(),
allocation: Validator.string().required(),
valid_for: Validator.array().items(Validator.string()),
usage_limit: Validator.number().optional(),
})
.required(),
is_disabled: Validator.boolean().default(false),
starts_at: Validator.date().optional(),
ends_at: Validator.date().optional(),
usage_limit: Validator.number()
.positive()
.optional(),
regions: Validator.array()
.items(Validator.string())
.optional(),
@@ -26,6 +26,7 @@ export default async (req, res) => {
const schema = Validator.object().keys({
code: Validator.string().required(),
usage_limit: Validator.number().default(1),
metadata: Validator.object().optional(),
})
@@ -59,8 +59,11 @@ export const defaultFields = [
"id",
"code",
"is_dynamic",
"is_disabled",
"rule_id",
"parent_discount_id",
"usage_limit",
"usage_count",
"starts_at",
"ends_at",
"created_at",
@@ -69,6 +69,9 @@ export default async (req, res) => {
is_disabled: Validator.boolean().optional(),
starts_at: Validator.date().optional(),
ends_at: Validator.date().optional(),
usage_limit: Validator.number()
.positive()
.optional(),
regions: Validator.array()
.items(Validator.string())
.optional(),
@@ -11,6 +11,7 @@ import shippingProfileRoutes from "./shipping-profiles"
import discountRoutes from "./discounts"
import giftCardRoutes from "./gift-cards"
import orderRoutes from "./orders"
import returnReasonRoutes from "./return-reasons"
import storeRoutes from "./store"
import uploadRoutes from "./uploads"
import customerRoutes from "./customers"
@@ -66,6 +67,7 @@ export default (app, container, config) => {
draftOrderRoutes(route)
collectionRoutes(route)
notificationRoutes(route)
returnReasonRoutes(route)
return app
}
@@ -13,6 +13,8 @@ const defaultRelations = [
"fulfillments.tracking_links",
"fulfillments.items",
"returns",
"returns.items",
"returns.items.reason",
"gift_cards",
"gift_card_transactions",
"claims",
@@ -50,6 +52,7 @@ const defaultFields = [
"updated_at",
"metadata",
"items.refundable",
"swaps.additional_items.refundable",
"shipping_total",
"discount_total",
"tax_total",
@@ -57,6 +60,7 @@ const defaultFields = [
"gift_card_total",
"subtotal",
"total",
"paid_total",
"refundable_amount",
]
@@ -37,21 +37,18 @@ describe("POST /admin/orders/:id/return", () => {
it("calls OrderService return", () => {
expect(ReturnService.create).toHaveBeenCalledTimes(1)
expect(ReturnService.create).toHaveBeenCalledWith(
{
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 10,
shipping_method: undefined,
},
orders.testOrder
)
expect(ReturnService.create).toHaveBeenCalledWith({
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 10,
shipping_method: undefined,
})
})
})
@@ -88,21 +85,18 @@ describe("POST /admin/orders/:id/return", () => {
it("calls OrderService return", () => {
expect(ReturnService.create).toHaveBeenCalledTimes(1)
expect(ReturnService.create).toHaveBeenCalledWith(
{
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 0,
shipping_method: undefined,
},
orders.testOrder
)
expect(ReturnService.create).toHaveBeenCalledWith({
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 0,
shipping_method: undefined,
})
})
})
@@ -139,21 +133,18 @@ describe("POST /admin/orders/:id/return", () => {
it("calls OrderService return", () => {
expect(ReturnService.create).toHaveBeenCalledTimes(1)
expect(ReturnService.create).toHaveBeenCalledWith(
{
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 0,
shipping_method: undefined,
},
orders.testOrder
)
expect(ReturnService.create).toHaveBeenCalledWith({
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 0,
shipping_method: undefined,
})
})
})
@@ -194,24 +185,21 @@ describe("POST /admin/orders/:id/return", () => {
it("calls OrderService return", () => {
expect(ReturnService.create).toHaveBeenCalledTimes(1)
expect(ReturnService.create).toHaveBeenCalledWith(
{
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 100,
shipping_method: {
option_id: "opt_1234",
price: 12,
expect(ReturnService.create).toHaveBeenCalledWith({
order_id: IdMap.getId("test-order"),
idempotency_key: "testkey",
items: [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
],
refund_amount: 100,
shipping_method: {
option_id: "opt_1234",
price: 12,
},
orders.testOrder
)
})
expect(ReturnService.fulfill).toHaveBeenCalledTimes(1)
expect(ReturnService.fulfill).toHaveBeenCalledWith("return")
@@ -7,7 +7,7 @@ import { defaultFields, defaultRelations } from "./"
* summary: "Create a Swap"
* description: "Creates a Swap. Swaps are used to handle Return of previously purchased goods and Fulfillment of replacements simultaneously."
* parameters:
* - (path) id=* {string} The id of the Swap.
* - (path) id=* {string} The id of the Order.
* requestBody:
* content:
* application/json:
@@ -124,7 +124,7 @@ export default async (req, res) => {
.withTransaction(manager)
.retrieve(id, {
select: ["refunded_total", "total"],
relations: ["items", "swaps"],
relations: ["items", "swaps", "swaps.additional_items"],
})
const swap = await swapService
@@ -78,14 +78,6 @@ export default app => {
middlewares.wrap(require("./request-return").default)
)
/**
* Register a requested return
*/
route.post(
"/:id/return/:return_id/receive",
middlewares.wrap(require("./receive-return").default)
)
/**
* Cancel an order.
*/
@@ -195,6 +187,8 @@ export const defaultRelations = [
"fulfillments.tracking_links",
"fulfillments.items",
"returns",
"returns.items",
"returns.items.reason",
"gift_cards",
"gift_card_transactions",
"claims",
@@ -233,6 +227,7 @@ export const defaultFields = [
"updated_at",
"metadata",
"items.refundable",
"swaps.additional_items.refundable",
"shipping_total",
"discount_total",
"tax_total",
@@ -240,6 +235,7 @@ export const defaultFields = [
"gift_card_total",
"subtotal",
"total",
"paid_total",
"refundable_amount",
]
@@ -267,6 +263,7 @@ export const allowedFields = [
"subtotal",
"gift_card_total",
"total",
"paid_total",
"refundable_amount",
]
@@ -6,7 +6,7 @@ import { MedusaError, Validator } from "medusa-core-utils"
* @oas [get] /orders
* operationId: "GetOrders"
* summary: "List Orders"
* description: "Retrieves an list of Orders"
* description: "Retrieves a list of Orders"
* tags:
* - Order
* responses:
@@ -16,8 +16,10 @@ import { MedusaError, Validator } from "medusa-core-utils"
* application/json:
* schema:
* properties:
* order:
* $ref: "#/components/schemas/order"
* orders:
* type: array
* items:
* $ref: "#/components/schemas/order"
*/
export default async (req, res) => {
const schema = Validator.orderFilter()
@@ -21,6 +21,12 @@ import { defaultRelations, defaultFields } from "./"
* item_id:
* description: The id of the Line Item.
* type: string
* reason_id:
* description: The id of the Return Reason to use.
* type: string
* note:
* description: An optional note with information about the Return.
* type: string
* quantity:
* description: The quantity of the Line Item.
* type: integer
@@ -60,6 +66,8 @@ export default async (req, res) => {
.items({
item_id: Validator.string().required(),
quantity: Validator.number().required(),
reason_id: Validator.string().optional(),
note: Validator.string().optional(),
})
.required(),
return_shipping: Validator.object()
@@ -115,13 +123,6 @@ export default async (req, res) => {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async manager => {
const order = await orderService
.withTransaction(manager)
.retrieve(id, {
select: ["refunded_total", "total"],
relations: ["items"],
})
const returnObj = {
order_id: id,
idempotency_key: idempotencyKey.idempotency_key,
@@ -142,7 +143,7 @@ export default async (req, res) => {
const createdReturn = await returnService
.withTransaction(manager)
.create(returnObj, order)
.create(returnObj)
if (value.return_shipping) {
await returnService
@@ -200,7 +201,7 @@ export default async (req, res) => {
order = await returnService
.withTransaction(manager)
.receiveReturn(order.id, ret.id, value.items, value.refund)
.receive(ret.id, value.items, value.refund)
}
order = await orderService.withTransaction(manager).retrieve(id, {
@@ -0,0 +1,66 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /return-reasons
* operationId: "PostReturnReasons"
* summary: "Create a Return Reason"
* description: "Creates a Return Reason"
* requestBody:
* content:
* application/json:
* schema:
* properties:
* label:
* description: "The label to display to the Customer."
* type: string
* value:
* description: "The value that the Return Reason will be identified by. Must be unique."
* type: string
* description:
* description: "An optional description to for the Reason."
* type: string
* metadata:
* description: An optional set of key-value pairs with additional information.
* type: object
* tags:
* - Return Reason
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return_reason:
* $ref: "#/components/schemas/return_reason"
*/
export default async (req, res) => {
const schema = Validator.object().keys({
value: Validator.string().required(),
label: Validator.string().required(),
description: Validator.string()
.optional()
.allow(""),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const returnReasonService = req.scope.resolve("returnReasonService")
const result = await returnReasonService.create(value)
const reason = await returnReasonService.retrieve(result.id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ return_reason: reason })
} catch (err) {
throw err
}
}
@@ -0,0 +1,37 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [get] /return-reasons/{id}
* operationId: "GetReturnReasonsReason"
* summary: "Retrieve a Return Reason"
* description: "Retrieves a Return Reason."
* parameters:
* - (path) id=* {string} The id of the Return Reason.
* tags:
* - Return Reason
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return_reason:
* $ref: "#/components/schemas/return_reason"
*/
export default async (req, res) => {
const { id } = req.params
try {
const returnReasonService = req.scope.resolve("returnReasonService")
const data = await returnReasonService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ return_reason: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,42 @@
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/return-reasons", route)
/**
* List reasons
*/
route.get("/", middlewares.wrap(require("./list-reasons").default))
/**
* Retrieve reason
*/
route.get("/:id", middlewares.wrap(require("./get-reason").default))
/**
* Create a reason
*/
route.post("/", middlewares.wrap(require("./create-reason").default))
/**
* Update a reason
*/
route.post("/:id", middlewares.wrap(require("./update-reason").default))
return app
}
export const defaultFields = [
"id",
"value",
"label",
"description",
"created_at",
"updated_at",
"deleted_at",
]
export const defaultRelations = []
@@ -0,0 +1,37 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [get] /return-reasons
* operationId: "GetReturnReasons"
* summary: "List Return Reasons"
* description: "Retrieves a list of Return Reasons."
* tags:
* - Return Reason
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return_reasons:
* type: array
* items:
* $ref: "#/components/schemas/return_reason"
*/
export default async (req, res) => {
try {
const returnReasonService = req.scope.resolve("returnReasonService")
const query = {}
const data = await returnReasonService.list(query, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ return_reasons: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,70 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /return-reasons/{id}
* operationId: "PostReturnReasonsReason"
* summary: "Update a Return Reason"
* description: "Updates a Return Reason"
* parameters:
* - (path) id=* {string} The id of the Return Reason.
* requestBody:
* content:
* application/json:
* schema:
* properties:
* label:
* description: "The label to display to the Customer."
* type: string
* value:
* description: "The value that the Return Reason will be identified by. Must be unique."
* type: string
* description:
* description: "An optional description to for the Reason."
* type: string
* metadata:
* description: An optional set of key-value pairs with additional information.
* type: object
* tags:
* - Return Reason
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return_reason:
* $ref: "#/components/schemas/return_reason"
*/
export default async (req, res) => {
const { id } = req.params
const schema = Validator.object().keys({
label: Validator.string().optional(),
description: Validator.string()
.optional()
.allow(""),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const returnReasonService = req.scope.resolve("returnReasonService")
await returnReasonService.update(id, value)
const reason = await returnReasonService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ return_reason: reason })
} catch (err) {
throw err
}
}
@@ -0,0 +1,58 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { OrderServiceMock } from "../../../../../services/__mocks__/order"
import { ReturnService } from "../../../../../services/__mocks__/return"
describe("POST /admin/returns/:id/receive", () => {
describe("successfully receives a return", () => {
let subject
beforeAll(async () => {
subject = await request(
"POST",
`/admin/returns/${IdMap.getId("test-return")}/receive`,
{
payload: {
items: [
{
item_id: IdMap.getId("test"),
quantity: 2,
},
],
},
adminSession: {
jwt: {
userId: IdMap.getId("admin_user"),
},
},
}
)
})
it("returns 200", () => {
expect(subject.status).toEqual(200)
})
it("calls ReturnService receive", () => {
expect(ReturnService.receive).toHaveBeenCalledTimes(1)
expect(ReturnService.receive).toHaveBeenCalledWith(
IdMap.getId("test-return"),
[{ item_id: IdMap.getId("test"), quantity: 2 }],
undefined,
true
)
})
it("calls OrderService registerReturnReceived", () => {
expect(OrderServiceMock.registerReturnReceived).toHaveBeenCalledTimes(1)
expect(OrderServiceMock.registerReturnReceived).toHaveBeenCalledWith(
IdMap.getId("test-order"),
{
id: IdMap.getId("test-return"),
order_id: IdMap.getId("test-order"),
},
undefined
)
})
})
})
@@ -11,5 +11,10 @@ export default app => {
*/
route.get("/", middlewares.wrap(require("./list-returns").default))
route.post(
"/:id/receive",
middlewares.wrap(require("./receive-return").default)
)
return app
}
@@ -1,14 +1,12 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /orders/{id}/returns/{return_id}/receive
* operationId: "PostOrdersOrderReturnsReturnReceive"
* @oas [post] /returns/{id}receive
* operationId: "PostReturnsReturnReceive"
* summary: "Receive a Return"
* description: "Registers a Return as received."
* description: "Registers a Return as received. Updates statuses on Orders and Swaps accordingly."
* parameters:
* - (path) id=* {string} The id of the Order.
* - (path) return_id=* {string} The id of the Return.
* - (path) id=* {string} The id of the Return.
* requestBody:
* content:
* application/json:
@@ -29,7 +27,7 @@ import { defaultRelations, defaultFields } from "./"
* description: The amount to refund.
* type: integer
* tags:
* - Order
* - Return
* responses:
* 200:
* description: OK
@@ -37,11 +35,11 @@ import { defaultRelations, defaultFields } from "./"
* application/json:
* schema:
* properties:
* order:
* $ref: "#/components/schemas/order"
* return:
* $ref: "#/components/schemas/return"
*/
export default async (req, res) => {
const { id, return_id } = req.params
const { id } = req.params
const schema = Validator.object().keys({
items: Validator.array()
@@ -61,28 +59,43 @@ export default async (req, res) => {
}
try {
const returnService = req.scope.resolve("returnService")
const orderService = req.scope.resolve("orderService")
const swapService = req.scope.resolve("swapService")
const entityManager = req.scope.resolve("manager")
let refundAmount = value.refund
let receivedReturn
await entityManager.transaction(async manager => {
let refundAmount = value.refund
if (typeof value.refund !== "undefined" && value.refund < 0) {
refundAmount = 0
}
if (typeof value.refund !== "undefined" && value.refund < 0) {
refundAmount = 0
}
let order = await orderService.receiveReturn(
id,
return_id,
value.items,
refundAmount,
true
)
receivedReturn = await returnService
.withTransaction(manager)
.receive(id, value.items, refundAmount, true)
order = await orderService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
if (receivedReturn.order_id) {
await orderService
.withTransaction(manager)
.registerReturnReceived(
receivedReturn.order_id,
receivedReturn,
refundAmount
)
}
if (receivedReturn.swap_id) {
await swapService
.withTransaction(manager)
.registerReceived(receivedReturn.swap_id)
}
})
res.status(200).json({ order })
receivedReturn = await returnService.retrieve(id, { relations: ["swap"] })
res.status(200).json({ return: receivedReturn })
} catch (err) {
throw err
}
@@ -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",
},
})
})
@@ -65,7 +65,7 @@ describe("POST /store/carts/:id", () => {
expect(CartServiceMock.retrieve).toHaveBeenCalledWith(
IdMap.getId("emptyCart"),
{
relations: ["payment_sessions"],
relations: ["payment_sessions", "shipping_methods"],
}
)
expect(CartServiceMock.retrieve).toHaveBeenCalledWith(
@@ -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)
@@ -100,7 +104,7 @@ export default async (req, res) => {
// If the cart has payment sessions update these
const updated = await cartService.retrieve(id, {
relations: ["payment_sessions"],
relations: ["payment_sessions", "shipping_methods"],
})
if (updated.payment_sessions?.length && !value.region_id) {
@@ -5,8 +5,10 @@ import { defaultRelations, defaultFields } from "./"
* operationId: "GetGiftCardsCode"
* summary: "Retrieve Gift Card by Code"
* description: "Retrieves a Gift Card by its associated unqiue code."
* parameters:
* - (path) code=* {string} The unique Gift Card code.
* tags:
* - Region
* - Gift Card
* responses:
* 200:
* description: OK
@@ -22,6 +24,8 @@ import { defaultRelations, defaultFields } from "./"
* description: The original value of the Gift Card.
* balance:
* description: The current balanace of the Gift Card
* region:
* $ref: "#/components/schemas/region"
*/
export default async (req, res) => {
const { code } = req.params
@@ -11,10 +11,10 @@ export default app => {
return app
}
export const defaultRelations = []
export const defaultRelations = ["region"]
export const defaultFields = ["id", "code", "value", "balance"]
export const allowedRelations = []
export const allowedRelations = ["region"]
export const allowedFields = ["id", "code", "value", "balance"]
@@ -11,6 +11,7 @@ import customerRoutes from "./customers"
import shippingOptionRoutes from "./shipping-options"
import regionRoutes from "./regions"
import returnRoutes from "./returns"
import returnReasonRoutes from "./return-reasons"
import swapRoutes from "./swaps"
import variantRoutes from "./variants"
import giftCardRoutes from "./gift-cards"
@@ -41,6 +42,7 @@ export default (app, container, config) => {
variantRoutes(route)
returnRoutes(route)
giftCardRoutes(route)
returnReasonRoutes(route)
return app
}
@@ -6,9 +6,19 @@ const route = Router()
export default app => {
app.use("/orders", route)
/**
* Lookup
*/
route.get("/", middlewares.wrap(require("./lookup-order").default))
/**
* Retrieve Order
*/
route.get("/:id", middlewares.wrap(require("./get-order").default))
/**
* Retrieve by Cart Id
*/
route.get(
"/cart/:cart_id",
middlewares.wrap(require("./get-order-by-cart").default)
@@ -32,6 +42,9 @@ export const defaultRelations = [
export const defaultFields = [
"id",
"status",
"fulfillment_status",
"payment_status",
"display_id",
"cart_id",
"customer_id",
@@ -43,6 +56,7 @@ export const defaultFields = [
"shipping_total",
"discount_total",
"tax_total",
"items.refundable",
"refunded_total",
"gift_card_total",
"subtotal",
@@ -65,12 +79,16 @@ export const allowedRelations = [
export const allowedFields = [
"id",
"status",
"fulfillment_status",
"payment_status",
"display_id",
"cart_id",
"customer_id",
"email",
"region_id",
"currency_code",
"items.refundable",
"tax_rate",
"created_at",
"shipping_total",
@@ -1,6 +1,26 @@
import { Validator, MedusaError } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./index"
/**
* @oas [get] /orders
* operationId: "GetOrders"
* summary: "Look Up an Order"
* description: "Looks for an Order with a given `display_id`, `email` pair. The `display_id`, `email` pair must match in order for the Order to be returned."
* parameters:
* - (query) display_id=* {number} The display id given to the Order.
* - (query) email=* {string} The email of the Order with the given display_id.
* tags:
* - Order
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* order:
* $ref: "#/components/schemas/order"
*/
export default async (req, res) => {
const schema = Validator.object().keys({
display_id: Validator.number().required(),
@@ -0,0 +1,37 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [get] /return-reasons/{id}
* operationId: "GetReturnReasonsReason"
* summary: "Retrieve a Return Reason"
* description: "Retrieves a Return Reason."
* parameters:
* - (path) id=* {string} The id of the Return Reason.
* tags:
* - Return Reason
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return_reason:
* $ref: "#/components/schemas/return_reason"
*/
export default async (req, res) => {
const { id } = req.params
try {
const returnReasonService = req.scope.resolve("returnReasonService")
const data = await returnReasonService.retrieve(id, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ return_reason: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,32 @@
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
export default app => {
app.use("/return-reasons", route)
/**
* List reasons
*/
route.get("/", middlewares.wrap(require("./list-reasons").default))
/**
* Retrieve reason
*/
route.get("/:id", middlewares.wrap(require("./get-reason").default))
return app
}
export const defaultFields = [
"id",
"value",
"label",
"description",
"created_at",
"updated_at",
"deleted_at",
]
export const defaultRelations = []
@@ -0,0 +1,37 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [get] /return-reasons
* operationId: "GetReturnReasons"
* summary: "List Return Reasons"
* description: "Retrieves a list of Return Reasons."
* tags:
* - Return Reason
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return_reasons:
* type: array
* items:
* $ref: "#/components/schemas/return_reason"
*/
export default async (req, res) => {
try {
const returnReasonService = req.scope.resolve("returnReasonService")
const query = {}
const data = await returnReasonService.list(query, {
select: defaultFields,
relations: defaultRelations,
})
res.status(200).json({ return_reasons: data })
} catch (err) {
throw err
}
}
@@ -1,6 +1,48 @@
import { MedusaError, Validator } from "medusa-core-utils"
import { defaultRelations, defaultFields } from "./"
/**
* @oas [post] /returns
* operationId: "PostReturns"
* summary: "Create Return"
* description: "Creates a Return for an Order."
* requestBody:
* content:
* application/json:
* schema:
* properties:
* order_id:
* type: string
* description: The id of the Order to create the Return from.
* items:
* description: "The items to include in the Return."
* type: array
* items:
* properties:
* item_id:
* description: The id of the Line Item from the Order.
* type: string
* quantity:
* description: The quantity to return.
* type: integer
* return_shipping:
* description: If the Return is to be handled by the store operator the Customer can choose a Return Shipping Method. Alternatvely the Customer can handle the Return themselves.
* type: object
* properties:
* option_id:
* type: string
* description: The id of the Shipping Option to create the Shipping Method from.
* tags:
* - Return
* responses:
* 200:
* description: OK
* content:
* application/json:
* schema:
* properties:
* return:
* $ref: "#/components/schemas/return"
*/
export default async (req, res) => {
const schema = Validator.object().keys({
order_id: Validator.string().required(),
@@ -8,6 +50,8 @@ export default async (req, res) => {
.items({
item_id: Validator.string().required(),
quantity: Validator.number().required(),
reason_id: Validator.string().optional(),
note: Validator.string().optional(),
})
.required(),
return_shipping: Validator.object()
@@ -109,13 +153,14 @@ export default async (req, res) => {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async manager => {
let order = await orderService
.withTransaction(manager)
.retrieve(value.order_id, { relations: ["returns"] })
let ret = await returnService.withTransaction(manager).list({
idempotency_key: idempotencyKey.idempotency_key,
})
let ret = await returnService.withTransaction(manager).list(
{
idempotency_key: idempotencyKey.idempotency_key,
},
{
relations: ["items", "items.reason"],
}
)
if (!ret.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -4,6 +4,7 @@
* summary: Retrieve Shipping Options
* description: "Retrieves a list of Shipping Options."
* parameters:
* - (query) is_return {boolean} Whether return Shipping Options should be included. By default all Shipping Options are returned.
* - (query) product_ids {string} A comma separated list of Product ids to filter Shipping Options by.
* - (query) region_id {string} the Region to retrieve Shipping Options from.
* tags:
@@ -31,6 +32,10 @@ export default async (req, res) => {
const query = {}
if ("is_return" in req.query) {
query.is_return = req.query.is_return === "true"
}
if (regionId) {
query.region_id = regionId
}
+1
View File
@@ -42,3 +42,4 @@ export { Store } from "./models/store"
export { Swap } from "./models/swap"
export { User } from "./models/user"
export { DraftOrder } from "./models/draft-order"
export { ReturnReason } from "./models/return-reason"
+4 -1
View File
@@ -11,7 +11,7 @@ import modelsLoader from "./models"
import servicesLoader from "./services"
import subscribersLoader from "./subscribers"
import passportLoader from "./passport"
import pluginsLoader from "./plugins"
import pluginsLoader, { registerPluginModels } from "./plugins"
import defaultsLoader from "./defaults"
import Logger from "./logger"
import { getManager } from "typeorm"
@@ -64,6 +64,9 @@ export default async ({ directory: rootDirectory, expressApp }) => {
await modelsLoader({ container })
Logger.info("Models initialized")
await registerPluginModels({ rootDirectory, container })
Logger.info("Models initialized")
await repositoriesLoader({ container })
Logger.info("Repositories initialized")
+1 -2
View File
@@ -1,8 +1,7 @@
import glob from "glob"
import path from "path"
import { BaseModel } from "medusa-interfaces"
import { EntitySchema } from "typeorm"
import { Lifetime, asClass, asValue } from "awilix"
import { asClass, asValue } from "awilix"
/**
* Registers all models in the model directory
+62 -22
View File
@@ -1,4 +1,5 @@
import glob from "glob"
import { EntitySchema } from "typeorm"
import {
BaseModel,
BaseService,
@@ -12,13 +13,32 @@ import { getConfigFile, createRequireFromPath } from "medusa-core-utils"
import _ from "lodash"
import path from "path"
import fs from "fs"
import { asFunction, aliasTo } from "awilix"
import { asValue, asClass, asFunction, aliasTo } from "awilix"
import { sync as existsSync } from "fs-exists-cached"
/**
* Registers all services in the services directory
*/
export default async ({ rootDirectory, container, app }) => {
const resolved = getResolvedPlugins(rootDirectory)
await Promise.all(
resolved.map(async pluginDetails => {
registerRepositories(pluginDetails, container)
await registerServices(pluginDetails, container)
registerMedusaApi(pluginDetails, container)
registerApi(pluginDetails, app, rootDirectory, container)
registerCoreRouters(pluginDetails, container)
registerSubscribers(pluginDetails, container)
})
)
await Promise.all(
resolved.map(async pluginDetails => runLoaders(pluginDetails, container))
)
}
function getResolvedPlugins(rootDirectory) {
const { configModule } = getConfigFile(rootDirectory, `medusa-config`)
if (!configModule) {
@@ -46,20 +66,16 @@ export default async ({ rootDirectory, container, app }) => {
version: createFileContentHash(process.cwd(), `**`),
})
return resolved
}
export async function registerPluginModels({ rootDirectory, container }) {
const resolved = getResolvedPlugins(rootDirectory)
await Promise.all(
resolved.map(async pluginDetails => {
registerModels(pluginDetails, container)
await registerServices(pluginDetails, container)
registerMedusaApi(pluginDetails, container)
registerApi(pluginDetails, app, rootDirectory, container)
registerCoreRouters(pluginDetails, container)
registerSubscribers(pluginDetails, container)
})
)
await Promise.all(
resolved.map(async pluginDetails => runLoaders(pluginDetails, container))
)
}
async function runLoaders(pluginDetails, container) {
@@ -288,6 +304,33 @@ function registerSubscribers(pluginDetails, container) {
})
}
/**
* Registers a plugin's models at the right location in our container. Models
* must inherit from BaseModel. Models are registered directly in the container.
* Names are camelCase formatted and namespaced by the folder i.e:
* models/example-person -> examplePersonModel
* @param {object} pluginDetails - the plugin details including plugin options,
* version, id, resolved path, etc. See resolvePlugin
* @param {object} container - the container where the services will be
* registered
* @return {void}
*/
function registerRepositories(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/repositories/*.js`, {})
files.forEach(fn => {
const loaded = require(fn)
Object.entries(loaded).map(([key, val]) => {
if (typeof val === "function") {
const name = formatRegistrationName(fn)
container.register({
[name]: asClass(val),
})
}
})
})
}
/**
* Registers a plugin's models at the right location in our container. Models
* must inherit from BaseModel. Models are registered directly in the container.
@@ -302,20 +345,17 @@ function registerSubscribers(pluginDetails, container) {
function registerModels(pluginDetails, container) {
const files = glob.sync(`${pluginDetails.resolve}/models/*.js`, {})
files.forEach(fn => {
const loaded = require(fn).default
const loaded = require(fn)
if (!(loaded.prototype instanceof BaseModel)) {
const logger = container.resolve("logger")
const message = `Models must inherit from BaseModel, please check ${fn}`
logger.error(message)
throw new Error(message)
}
Object.entries(loaded).map(([key, val]) => {
if (typeof val === "function" || val instanceof EntitySchema) {
const name = formatRegistrationName(fn)
container.register({
[name]: asValue(val),
})
const name = formatRegistrationName(fn)
container.register({
[name]: asFunction(
cradle => new loaded(cradle, pluginDetails.options)
).singleton(),
container.registerAdd("db_entities", asValue(val))
}
})
})
}
@@ -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"`)
}
}
@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class returnReason1615891636559 implements MigrationInterface {
name = "returnReason1615891636559"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "return_reason" ("id" character varying NOT NULL, "value" character varying NOT NULL, "label" character varying NOT NULL, "description" character varying, "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, CONSTRAINT "PK_95fd1172973165790903e65660a" PRIMARY KEY ("id"))`
)
await queryRunner.query(
`CREATE UNIQUE INDEX "IDX_00605f9d662c06b81c1b60ce24" ON "return_reason" ("value") `
)
await queryRunner.query(
`ALTER TABLE "return_item" ADD "reason_id" character varying`
)
await queryRunner.query(
`ALTER TABLE "return_item" ADD "note" character varying`
)
await queryRunner.query(
`ALTER TABLE "return_item" ADD CONSTRAINT "FK_d742532378a65022e7ceb328828" FOREIGN KEY ("reason_id") REFERENCES "return_reason"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "return_item" DROP CONSTRAINT "FK_d742532378a65022e7ceb328828"`
)
await queryRunner.query(`ALTER TABLE "return_item" DROP COLUMN "note"`)
await queryRunner.query(`ALTER TABLE "return_item" DROP COLUMN "reason_id"`)
await queryRunner.query(`DROP INDEX "IDX_00605f9d662c06b81c1b60ce24"`)
await queryRunner.query(`DROP TABLE "return_reason"`)
}
}
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class discountUsageCount1615970124120 implements MigrationInterface {
name = "discountUsageCount1615970124120"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "discount_rule" ADD "usage_count" integer NOT NULL DEFAULT '0'`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "discount_rule" DROP COLUMN "usage_count"`
)
}
}
@@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class discountUsage1617002207608 implements MigrationInterface {
name = "discountUsage1617002207608"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "discount_rule" DROP COLUMN "usage_limit"`
)
await queryRunner.query(
`ALTER TABLE "discount_rule" DROP COLUMN "usage_count"`
)
await queryRunner.query(`ALTER TABLE "discount" ADD "usage_limit" integer`)
await queryRunner.query(
`ALTER TABLE "discount" ADD "usage_count" integer NOT NULL DEFAULT '0'`
)
await queryRunner.query(
`ALTER TABLE "discount_rule" ALTER COLUMN "description" DROP NOT NULL`
)
await queryRunner.query(
`COMMENT ON COLUMN "discount_rule"."description" IS NULL`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "discount" DROP COLUMN "usage_count"`)
await queryRunner.query(`ALTER TABLE "discount" DROP COLUMN "usage_limit"`)
await queryRunner.query(
`ALTER TABLE "discount_rule" ADD "usage_count" integer NOT NULL DEFAULT '0'`
)
await queryRunner.query(
`ALTER TABLE "discount_rule" ADD "usage_limit" integer`
)
await queryRunner.query(
`COMMENT ON COLUMN "discount_rule"."description" IS NULL`
)
await queryRunner.query(
`ALTER TABLE "discount_rule" ALTER COLUMN "description" SET NOT NULL`
)
}
}
+3
View File
@@ -244,6 +244,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
+1 -8
View File
@@ -7,7 +7,6 @@ import {
Index,
Column,
PrimaryColumn,
OneToOne,
ManyToMany,
JoinTable,
} from "typeorm"
@@ -31,7 +30,7 @@ export class DiscountRule {
@PrimaryColumn()
id: string
@Column()
@Column({ nullable: true })
description: string
@Column({
@@ -64,9 +63,6 @@ export class DiscountRule {
})
valid_for: Product[]
@Column({ nullable: true })
usage_limit: number
@CreateDateColumn({ type: "timestamptz" })
created_at: Date
@@ -119,9 +115,6 @@ export class DiscountRule {
* type: array
* items:
* $ref: "#/components/schemas/product"
* usage_limit:
* description: "The maximum number of times that a discount can be used."
* type: integer
* created_at:
* description: "The date with timezone at which the resource was created."
* type: string
+12
View File
@@ -68,6 +68,12 @@ export class Discount {
})
regions: Region[]
@Column({ nullable: true })
usage_limit: number
@Column({ default: 0 })
usage_count: number
@CreateDateColumn({ type: "timestamptz" })
created_at: Date
@@ -127,6 +133,12 @@ export class Discount {
* type: array
* items:
* $ref: "#/components/schemas/region"
* usage_limit:
* description: "The maximum number of times that a discount can be used."
* type: integer
* usage_count:
* description: "The number of times a discount has been used."
* type: integer
* created_at:
* description: "The date with timezone at which the resource was created."
* type: string
+3
View File
@@ -255,6 +255,7 @@ export class Order {
refunded_total: number
total: number
subtotal: number
paid_total: number
refundable_amount: number
gift_card_total: number
@@ -411,4 +412,6 @@ export class Order {
* type: integer
* gift_card_total:
* type: integer
* paid_total:
* type: integer
*/
+1 -1
View File
@@ -46,7 +46,7 @@ export class Product {
@Column({ default: false })
is_giftcard: boolean
@ManyToMany(() => Image)
@ManyToMany(() => Image, { cascade: ["insert"] })
@JoinTable({
name: "product_images",
joinColumn: {
+18
View File
@@ -16,6 +16,7 @@ import {
JoinTable,
} from "typeorm"
import { ReturnReason } from "./return-reason"
import { Return } from "./return"
import { LineItem } from "./line-item"
@@ -47,6 +48,16 @@ export class ReturnItem {
@Column({ type: "int", nullable: true })
received_quantity: number
@Column({ nullable: true })
reason_id: string
@ManyToOne(() => ReturnReason, { eager: true })
@JoinColumn({ name: "reason_id" })
reason: ReturnReason
@Column({ nullable: true })
note: string
@Column({ type: "jsonb", nullable: true })
metadata: any
}
@@ -79,6 +90,13 @@ export class ReturnItem {
* recieved_quantity:
* description: "The quantity that was received in the warehouse."
* type: integer
* reason:
* description: "The reason for returning the item."
* anyOf:
* - $ref: "#/components/schemas/return_reason"
* note:
* description: "An optional note with additional details about the Return."
* type: string
* metadata:
* description: "An optional key-value map with additional information."
* type: object
@@ -0,0 +1,81 @@
import {
Entity,
Index,
BeforeInsert,
Column,
DeleteDateColumn,
CreateDateColumn,
UpdateDateColumn,
PrimaryColumn,
} from "typeorm"
import { ulid } from "ulid"
@Entity()
export class ReturnReason {
@PrimaryColumn()
id: string
@Index({ unique: true })
@Column()
value: string
@Column()
label: string
@Column({ nullable: true })
description: string
@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
@BeforeInsert()
private beforeInsert() {
if (this.id) return
const id = ulid()
this.id = `rr_${id}`
}
}
/**
* @schema return_reason
* title: "Return Reason"
* description: "A Reason for why a given product is returned. A Return Reason can be used on Return Items in order to indicate why a Line Item was returned."
* x-resourceId: return_reason
* properties:
* id:
* description: "The id of the Return Reason will start with `rr_`."
* type: string
* description:
* description: "A description of the Reason."
* type: string
* label:
* description: "A text that can be displayed to the Customer as a reason."
* type: string
* value:
* description: "The value to identify the reason by."
* type: string
* created_at:
* description: "The date with timezone at which the resource was created."
* type: string
* format: date-time
* updated_at:
* description: "The date with timezone at which the resource was last updated."
* type: string
* format: date-time
* deleted_at:
* description: "The date with timezone at which the resource was deleted."
* type: string
* format: date-time
* metadata:
* description: "An optional key-value map with additional information."
* type: object
*/
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { Image } from "../models/image"
@EntityRepository(Image)
export class ImageRepository extends Repository<Image> {}
@@ -0,0 +1,5 @@
import { EntityRepository, Repository } from "typeorm"
import { ReturnReason } from "../models/return-reason"
@EntityRepository(ReturnReason)
export class ReturnReasonRepository extends Repository<ReturnReason> {}
@@ -126,9 +126,13 @@ export const OrderServiceMock = {
withTransaction: function() {
return this
},
create: jest.fn().mockImplementation(data => {
return Promise.resolve(orders.testOrder)
}),
registerReturnReceived: jest.fn().mockImplementation(data => {
return Promise.resolve()
}),
createFromCart: jest.fn().mockImplementation(data => {
return Promise.resolve(orders.testOrder)
}),
@@ -197,12 +201,6 @@ export const OrderServiceMock = {
}
return Promise.resolve(undefined)
}),
requestReturn: jest.fn().mockImplementation(order => {
if (order === IdMap.getId("test-order")) {
return Promise.resolve(orders.testOrder)
}
return Promise.resolve(undefined)
}),
receiveReturn: jest.fn().mockImplementation(order => {
if (order === IdMap.getId("test-order")) {
return Promise.resolve(orders.testOrder)
@@ -1,3 +1,5 @@
import { IdMap } from "medusa-test-utils"
export const ReturnService = {
withTransaction: function() {
return this
@@ -5,6 +7,13 @@ export const ReturnService = {
create: jest.fn(() => Promise.resolve({ id: "return" })),
fulfill: jest.fn(),
update: jest.fn(),
receive: jest.fn(() =>
Promise.resolve({
id: IdMap.getId("test-return"),
order_id: IdMap.getId("test-order"),
})
),
retrieve: jest.fn(() => Promise.resolve("test-return")),
}
const mock = jest.fn().mockImplementation(() => {
@@ -1281,6 +1281,7 @@ describe("CartService", () => {
profile_id: IdMap.getId(m.profile),
},
})),
discounts: [],
}
}
@@ -1462,6 +1463,26 @@ describe("CartService", () => {
regions: [{ id: IdMap.getId("bad") }],
})
}
if (code === "limit-reached") {
return Promise.resolve({
id: IdMap.getId("limit-reached"),
code: "limit-reached",
regions: [{ id: IdMap.getId("good") }],
rule: {},
usage_count: 2,
usage_limit: 2,
})
}
if (code === "null-count") {
return Promise.resolve({
id: IdMap.getId("null-count"),
code: "null-count",
regions: [{ id: IdMap.getId("good") }],
rule: {},
usage_count: null,
usage_limit: 2,
})
}
if (code === "FREESHIPPING") {
return Promise.resolve({
id: IdMap.getId("freeship"),
@@ -1594,6 +1615,42 @@ describe("CartService", () => {
})
})
it("successfully applies discount with usage count null", async () => {
await cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "null-count" }],
})
expect(discountService.retrieveByCode).toHaveBeenCalledTimes(1)
expect(cartRepository.save).toHaveBeenCalledTimes(1)
expect(cartRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("cart"),
discounts: [
{
id: IdMap.getId("null-count"),
code: "null-count",
regions: [{ id: IdMap.getId("good") }],
usage_count: 0,
usage_limit: 2,
rule: {},
},
],
discount_total: 0,
shipping_total: 0,
subtotal: 0,
tax_total: 0,
total: 0,
region_id: IdMap.getId("good"),
})
})
it("throws if discount limit is reached", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
discounts: [{ code: "limit-reached" }],
})
).rejects.toThrow("Discount has been used maximum allowed times")
})
it("throws if discount is not available in region", async () => {
await expect(
cartService.update(IdMap.getId("cart"), {
+13 -24
View File
@@ -100,31 +100,20 @@ describe("ClaimService", () => {
expect(withTransactionMock).toHaveBeenCalledWith("return")
expect(returnService.create).toHaveBeenCalledTimes(1)
expect(returnService.create).toHaveBeenCalledWith(
{
claim_order_id: "claim_134",
shipping_method: {
option_id: "opt_13",
price: 0,
},
items: [
{
item_id: "itm_1",
quantity: 1,
},
],
expect(returnService.create).toHaveBeenCalledWith({
order_id: "1234",
claim_order_id: "claim_134",
shipping_method: {
option_id: "opt_13",
price: 0,
},
{
id: "1234",
region_id: "order_region",
items: [
{
id: "itm_1",
unit_price: 8000,
},
],
}
)
items: [
{
item_id: "itm_1",
quantity: 1,
},
],
})
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
+29 -297
View File
@@ -25,6 +25,9 @@ describe("OrderService", () => {
getSubtotal: o => {
return o.subtotal || 0
},
getPaidTotal: o => {
return o.paid_total || 0
},
}
const eventBusService = {
@@ -858,16 +861,20 @@ describe("OrderService", () => {
})
})
describe("receiveReturn", () => {
describe("registerReturnReceived", () => {
const order = {
items: [
{
id: "item_1",
quantity: 10,
returned_quantity: 0,
returned_quantity: 10,
},
],
payments: [{ id: "payment_test" }],
payments: [{ id: "payment_test", amount: 100 }],
refunded_total: 0,
paid_total: 100,
refundable_amount: 100,
total: 100,
}
const orderRepo = MockRepository({
findOneWithRelations: (rel, q) => {
@@ -878,24 +885,6 @@ describe("OrderService", () => {
},
})
const returnService = {
retrieve: () => {
return Promise.resolve({
order_id: IdMap.getId("order"),
})
},
receiveReturn: jest
.fn()
.mockImplementation((id, items, amount, mism) =>
id === IdMap.getId("good")
? Promise.resolve({ items, status: "received", refund_amount: 100 })
: Promise.resolve({ status: "requires_action" })
),
withTransaction: function() {
return this
},
}
const paymentProviderService = {
refundPayment: jest
.fn()
@@ -907,20 +896,11 @@ describe("OrderService", () => {
},
}
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
return this
},
}
const orderService = new OrderService({
manager: MockManager,
orderRepository: orderRepo,
paymentProviderService,
totalsService,
returnService,
lineItemService,
eventBusService,
})
@@ -929,29 +909,11 @@ describe("OrderService", () => {
})
it("calls order model functions", async () => {
const items = [
{
item_id: "item_1",
quantity: 10,
},
]
await orderService.receiveReturn(
IdMap.getId("order"),
IdMap.getId("good"),
items
)
expect(returnService.receiveReturn).toHaveBeenCalledTimes(1)
expect(returnService.receiveReturn).toHaveBeenCalledWith(
IdMap.getId("good"),
items,
undefined,
false
)
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith("item_1", {
returned_quantity: 10,
await orderService.registerReturnReceived(IdMap.getId("order"), {
id: IdMap.getId("good"),
order_id: IdMap.getId("order"),
status: "received",
refund_amount: 100,
})
expect(orderRepo.save).toHaveBeenCalledTimes(1)
@@ -969,136 +931,23 @@ describe("OrderService", () => {
})
it("return with custom refund", async () => {
const items = [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
]
await orderService.receiveReturn(
await orderService.registerReturnReceived(
IdMap.getId("order"),
IdMap.getId("good"),
items,
102
)
expect(returnService.receiveReturn).toHaveBeenCalledTimes(1)
expect(returnService.receiveReturn).toHaveBeenCalledWith(
IdMap.getId("good"),
items,
102,
false
)
})
it("calls order model functions and sets partially_returned", async () => {
const items = [
{
item_id: IdMap.getId("existingLine"),
quantity: 2,
id: IdMap.getId("good"),
order_id: IdMap.getId("order"),
status: "received",
refund_amount: 95,
},
]
await orderService.receiveReturn(
IdMap.getId("order"),
IdMap.getId("good"),
items
95
)
expect(orderRepo.save).toHaveBeenCalledTimes(1)
expect(orderRepo.save).toHaveBeenCalledWith({
...order,
fulfillment_status: "partially_returned",
})
})
it("sets requires_action on additional items", async () => {
await orderService.receiveReturn(
IdMap.getId("order"),
IdMap.getId("action"),
[
{
item_id: IdMap.getId("existingLine2"),
quantity: 2,
},
]
expect(paymentProviderService.refundPayment).toHaveBeenCalledTimes(1)
expect(paymentProviderService.refundPayment).toHaveBeenCalledWith(
order.payments,
95,
"return"
)
expect(orderRepo.save).toHaveBeenCalledTimes(1)
expect(orderRepo.save).toHaveBeenCalledWith({
...order,
fulfillment_status: "requires_action",
})
})
})
describe("requestReturn", () => {
const order = {
items: [
{
id: "item_1",
quantity: 10,
returned_quantity: 0,
},
],
payments: [{ id: "payment_test" }],
}
const orderRepo = MockRepository({
findOneWithRelations: (rel, q) => {
switch (q.where.id) {
default:
return Promise.resolve(order)
}
},
})
const returnService = {
create: jest.fn(() => Promise.resolve({ id: "ret" })),
fulfill: jest.fn(() => Promise.resolve({ id: "ret" })),
withTransaction: function() {
return this
},
}
const orderService = new OrderService({
manager: MockManager,
orderRepository: orderRepo,
totalsService,
returnService,
eventBusService,
})
beforeEach(async () => {
jest.clearAllMocks()
})
it("successfully creates return request", async () => {
const items = [
{
item_id: IdMap.getId("existingLine"),
quantity: 10,
},
]
const shipping_method = {
id: IdMap.getId("return-shipping"),
price: 2,
}
await orderService.requestReturn(
"processed-order",
items,
shipping_method
)
expect(returnService.create).toHaveBeenCalledTimes(1)
expect(returnService.create).toHaveBeenCalledWith(
{
items,
shipping_method,
refund_amount: undefined,
order_id: "processed-order",
},
order
)
expect(returnService.fulfill).toHaveBeenCalledWith("ret")
})
})
@@ -1201,125 +1050,6 @@ describe("OrderService", () => {
})
})
describe("registerSwapReceived", () => {
beforeEach(async () => {
jest.clearAllMocks()
})
const orderRepo = MockRepository({
findOneWithRelations: () => Promise.resolve({ id: IdMap.getId("order") }),
})
it("fails if order/swap relationship not satisfied", async () => {
const swapService = {
retrieve: jest
.fn()
.mockReturnValue(
Promise.resolve({ id: "1235", order_id: IdMap.getId("order_1") })
),
withTransaction: function() {
return this
},
}
const orderService = new OrderService({
manager: MockManager,
orderRepository: orderRepo,
totalsService,
swapService,
eventBusService,
})
const res = orderService.registerSwapReceived(
IdMap.getId("order"),
"1235"
)
await expect(res).rejects.toThrow("Swap must belong to the given order")
})
it("fails if swap doesn't have status received", async () => {
const swapService = {
retrieve: jest.fn().mockReturnValue(
Promise.resolve({
id: "1235",
order_id: IdMap.getId("order"),
return_order: { status: "requested" },
})
),
withTransaction: function() {
return this
},
}
const orderService = new OrderService({
manager: MockManager,
orderRepository: orderRepo,
swapService,
totalsService,
eventBusService: { emit: jest.fn().mockReturnValue(Promise.resolve()) },
})
const res = orderService.registerSwapReceived(
IdMap.getId("order"),
"1235"
)
await expect(res).rejects.toThrow("Swap is not received")
})
it("registers a swap as received", async () => {
const orderRepo = MockRepository({
findOneWithRelations: () =>
Promise.resolve({
id: IdMap.getId("order_123"),
items: [
{
id: IdMap.getId("1234"),
returned_quantity: 0,
quantity: 1,
},
],
}),
})
const swapService = {
retrieve: jest.fn().mockReturnValue(
Promise.resolve({
id: "1235",
order_id: IdMap.getId("order_123"),
return_order: {
status: "received",
items: [{ item_id: IdMap.getId("1234"), quantity: 1 }],
},
})
),
withTransaction: function() {
return this
},
}
const lineItemService = {
update: jest.fn(),
withTransaction: function() {
return this
},
}
const orderService = new OrderService({
manager: MockManager,
orderRepository: orderRepo,
totalsService,
swapService,
lineItemService,
eventBusService,
})
await orderService.registerSwapReceived(IdMap.getId("order_123"), "1235")
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith(IdMap.getId("1234"), {
returned_quantity: 1,
})
})
})
describe("createRefund", () => {
beforeEach(async () => {
jest.clearAllMocks()
@@ -1341,13 +1071,15 @@ describe("OrderService", () => {
}
return Promise.resolve({
id: IdMap.getId("order"),
id: IdMap.getId("order_123"),
payments: [
{
id: "payment",
},
],
total: 100,
paid_total: 100,
refundable_amount: 100,
refunded_total: 0,
})
},
@@ -309,7 +309,7 @@ describe("ProductVariantService", () => {
"product-variant.updated",
{
id: IdMap.getId("ironman"),
title: "new title",
fields: ["title"],
}
)
@@ -333,7 +333,7 @@ describe("ProductVariantService", () => {
"product-variant.updated",
{
id: IdMap.getId("ironman"),
title: "new title",
fields: ["title"],
}
)
@@ -370,10 +370,7 @@ describe("ProductVariantService", () => {
"product-variant.updated",
{
id: IdMap.getId("ironman"),
title: "new title",
metadata: {
testing: "this",
},
fields: ["title", "metadata"],
}
)
@@ -1,5 +1,4 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import idMap from "medusa-test-utils/dist/id-map"
import ReturnService from "../return"
describe("ReturnService", () => {
@@ -141,7 +140,7 @@ describe("ReturnService", () => {
// })
// })
describe("receiveReturn", () => {
describe("receive", () => {
const returnRepository = MockRepository({
findOne: query => {
if (query.where.id === IdMap.getId("test-return-2")) {
@@ -189,9 +188,44 @@ describe("ReturnService", () => {
}),
}
const orderService = {
retrieve: jest.fn().mockImplementation(() => {
return Promise.resolve({
items: [
{
id: IdMap.getId("test-line"),
quantity: 10,
returned_quantity: 0,
},
{
id: IdMap.getId("test-line-2"),
quantity: 10,
returned_quantity: 0,
},
],
payments: [{ id: "payment_test" }],
})
}),
withTransaction: function() {
return this
},
}
const lineItemService = {
retrieve: jest.fn().mockImplementation(data => {
return Promise.resolve({ ...data, returned_quantity: 0 })
}),
update: jest.fn(),
withTransaction: function() {
return this
},
}
const returnService = new ReturnService({
manager: MockManager,
totalsService,
lineItemService,
orderService,
returnRepository,
})
@@ -200,7 +234,7 @@ describe("ReturnService", () => {
})
it("successfully receives a return", async () => {
await returnService.receiveReturn(
await returnService.receive(
IdMap.getId("test-return"),
[{ item_id: IdMap.getId("test-line"), quantity: 10 }],
1000
@@ -226,10 +260,18 @@ describe("ReturnService", () => {
refund_amount: 1000,
received_at: expect.anything(),
})
expect(lineItemService.update).toHaveBeenCalledTimes(1)
expect(lineItemService.update).toHaveBeenCalledWith(
IdMap.getId("test-line"),
{
returned_quantity: 10,
}
)
})
it("successfully receives a return with requires_action status", async () => {
await returnService.receiveReturn(
await returnService.receive(
IdMap.getId("test-return-2"),
[
{ item_id: IdMap.getId("test-line"), quantity: 10 },
@@ -192,6 +192,8 @@ describe("SwapService", () => {
relations: [
"order",
"order.items",
"order.swaps",
"order.swaps.additional_items",
"order.discounts",
"additional_items",
"return_order",
@@ -864,5 +866,56 @@ describe("SwapService", () => {
)
})
})
describe("registerReceived", () => {
beforeEach(async () => {
jest.clearAllMocks()
})
const eventBusService = {
emit: jest.fn().mockReturnValue(Promise.resolve()),
withTransaction: function() {
return this
},
}
const swapRepo = MockRepository({
findOne: q => {
switch (q.where.id) {
case "requested":
return Promise.resolve({
id: "requested",
order_id: IdMap.getId("order"),
return_order: { status: "requested" },
})
case "received":
return Promise.resolve({
id: "received",
order_id: IdMap.getId("order"),
return_order: { status: "received" },
})
default:
return Promise.resolve()
}
},
})
const swapService = new SwapService({
manager: MockManager,
swapRepository: swapRepo,
eventBusService,
})
it("fails if swap doesn't have status received", async () => {
const res = swapService.registerReceived("requested")
await expect(res).rejects.toThrow("Swap is not received")
})
it("registers a swap as received", async () => {
await swapService.registerReceived("received")
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
})
})
})
})
+98 -3
View File
@@ -1,7 +1,6 @@
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
import { defaultFields, defaultRelations } from "../api/routes/store/carts"
/**
* Provides layer to manipulate carts.
@@ -574,6 +573,49 @@ class CartService extends BaseService {
})
}
/**
* Ensures shipping total on cart is correct in regards to a potential free
* shipping discount
* If a free shipping is present, we set shipping methods price to 0
* if a free shipping was present, we set shipping methods to original amount
* @param {Cart} cart - the the cart to adjust free shipping for
* @param {boolean} shouldAdd - flag to indicate, if we should add or remove
*/
async adjustFreeShipping_(cart, shouldAdd) {
if (cart.shipping_methods?.length) {
// if any free shipping discounts, we ensure to update shipping method amount
if (shouldAdd) {
await Promise.all(
cart.shipping_methods.map(async sm => {
const smRepo = this.manager_.getCustomRepository(
this.shippingMethodRepository_
)
const method = await smRepo.findOne({ where: { id: sm.id } })
if (method) {
method.price = 0
await smRepo.save(method)
}
})
)
} else {
await Promise.all(
cart.shipping_methods.map(async sm => {
const smRepo = this.manager_.getCustomRepository(
this.shippingMethodRepository_
)
// if free shipping discount is removed, we adjust the shipping
// back to its original amount
sm.price = sm.shipping_option.amount
await smRepo.save(sm)
})
)
}
}
}
async update(cartId, update) {
return this.atomicPhase_(async manager => {
const cartRepo = manager.getCustomRepository(this.cartRepository_)
@@ -627,10 +669,29 @@ class CartService extends BaseService {
}
if ("discounts" in update) {
const previousDiscounts = cart.discounts
cart.discounts = []
for (const { code } of update.discounts) {
await this.applyDiscount(cart, code)
}
const hasFreeShipping = cart.discounts.some(
({ rule }) => rule.type === "free_shipping"
)
// if we previously had a free shipping discount and then removed it,
// we need to update shipping methods to original price
if (
previousDiscounts.some(({ rule }) => rule.type === "free_shipping") &&
!hasFreeShipping
) {
await this.adjustFreeShipping_(cart, false)
}
if (hasFreeShipping) {
await this.adjustFreeShipping_(cart, true)
}
}
if ("gift_cards" in update) {
@@ -641,6 +702,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) {
@@ -804,6 +873,18 @@ class CartService extends BaseService {
])
const rule = discount.rule
// if limit is set and reached, we make an early exit
if (discount.usage_limit) {
discount.usage_count = discount.usage_count || 0
if (discount.usage_limit === discount.usage_count)
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount has been used maximum allowed times"
)
}
let regions = discount.regions
if (discount.parent_discount_id) {
const parent = await this.discountService_.retrieve(
@@ -870,8 +951,13 @@ class CartService extends BaseService {
async removeDiscount(cartId, discountCode) {
return this.atomicPhase_(async manager => {
const cart = await this.retrieve(cartId, {
relations: ["discounts", "payment_sessions"],
relations: ["discounts", "payment_sessions", "shipping_methods"],
})
if (cart.discounts.some(({ rule }) => rule.type === "free_shipping")) {
await this.adjustFreeShipping_(cart, false)
}
cart.discounts = cart.discounts.filter(d => d.code !== discountCode)
const cartRepo = manager.getCustomRepository(this.cartRepository_)
@@ -1213,6 +1299,7 @@ class CartService extends BaseService {
select: ["subtotal"],
relations: [
"shipping_methods",
"discounts",
"shipping_methods.shipping_option",
"items",
"items.variant",
@@ -1248,7 +1335,15 @@ class CartService extends BaseService {
})
}
const result = await this.retrieve(cartId)
const result = await this.retrieve(cartId, {
relations: ["discounts", "shipping_methods"],
})
// if cart has freeshipping, adjust price
if (result.discounts.some(({ rule }) => rule.type === "free_shipping")) {
await this.adjustFreeShipping_(result, true)
}
await this.eventBus_
.withTransaction(manager)
.emit(CartService.Events.UPDATED, result)
+25 -18
View File
@@ -83,27 +83,34 @@ class ClaimItemService extends BaseService {
)
}
const claimTagRepo = manager.getCustomRepository(this.claimTagRepository_)
const tagsToAdd = await Promise.all(
tags.map(async t => {
const normalized = t.trim().toLowerCase()
const existing = await claimTagRepo.findOne({
where: { value: normalized },
let tagsToAdd = []
if (tags && tags.length) {
const claimTagRepo = manager.getCustomRepository(
this.claimTagRepository_
)
tagsToAdd = await Promise.all(
tags.map(async t => {
const normalized = t.trim().toLowerCase()
const existing = await claimTagRepo.findOne({
where: { value: normalized },
})
if (existing) {
return existing
}
return claimTagRepo.create({ value: normalized })
})
if (existing) {
return existing
}
)
}
return claimTagRepo.create({ value: normalized })
let imagesToAdd = []
if (images && images.length) {
const claimImgRepo = manager.getCustomRepository(
this.claimImageRepository_
)
imagesToAdd = images.map(url => {
return claimImgRepo.create({ url })
})
)
const claimImgRepo = manager.getCustomRepository(
this.claimImageRepository_
)
const imagesToAdd = images.map(url => {
return claimImgRepo.create({ url })
})
}
const created = ciRepo.create({
...rest,
+10 -12
View File
@@ -252,18 +252,16 @@ class ClaimService extends BaseService {
}
if (return_shipping) {
await this.returnService_.withTransaction(manager).create(
{
claim_order_id: result.id,
items: claim_items.map(ci => ({
item_id: ci.item_id,
quantity: ci.quantity,
metadata: ci.metadata,
})),
shipping_method: return_shipping,
},
order
)
await this.returnService_.withTransaction(manager).create({
order_id: order.id,
claim_order_id: result.id,
items: claim_items.map(ci => ({
item_id: ci.item_id,
quantity: ci.quantity,
metadata: ci.metadata,
})),
shipping_method: return_shipping,
})
}
await this.eventBus_
+11 -1
View File
@@ -83,7 +83,16 @@ class DiscountService extends BaseService {
.required(),
allocation: Validator.string().required(),
valid_for: Validator.array().optional(),
user_limit: Validator.number().optional(),
created_at: Validator.date().optional(),
updated_at: Validator.date()
.allow(null)
.optional(),
deleted_at: Validator.date()
.allow(null)
.optional(),
metadata: Validator.object()
.allow(null)
.optional(),
})
const { value, error } = schema.validate(discountRule)
@@ -321,6 +330,7 @@ class DiscountService extends BaseService {
is_disabled: false,
code: data.code.toUpperCase(),
parent_discount_id: discount.id,
usage_limit: discount.usage_limit,
}
const created = await discountRepo.create(toCreate)
+79 -243
View File
@@ -16,7 +16,6 @@ class OrderService extends BaseService {
REFUND_CREATED: "order.refund_created",
REFUND_FAILED: "order.refund_failed",
SWAP_CREATED: "order.swap_created",
SWAP_RECEIVED: "order.swap_received",
PLACED: "order.placed",
UPDATED: "order.updated",
CANCELED: "order.canceled",
@@ -36,8 +35,6 @@ class OrderService extends BaseService {
lineItemService,
totalsService,
regionService,
returnService,
swapService,
cartService,
addressRepository,
giftCardService,
@@ -55,10 +52,10 @@ class OrderService extends BaseService {
/** @private @constant {CustomerService} */
this.customerService_ = customerService
/** @private @constantant {PaymentProviderService} */
/** @private @constant {PaymentProviderService} */
this.paymentProviderService_ = paymentProviderService
/** @private @constantant {ShippingProvileService} */
/** @private @constant {ShippingProvileService} */
this.shippingProfileService_ = shippingProfileService
/** @private @constant {FulfillmentProviderService} */
@@ -73,9 +70,6 @@ class OrderService extends BaseService {
/** @private @constant {RegionService} */
this.regionService_ = regionService
/** @private @constant {ReturnService} */
this.returnService_ = returnService
/** @private @constant {FulfillmentService} */
this.fulfillmentService_ = fulfillmentService
@@ -122,7 +116,6 @@ class OrderService extends BaseService {
discountService: this.discountService_,
totalsService: this.totalsService_,
cartService: this.cartService_,
swapService: this.swapService_,
giftCardService: this.giftCardService_,
draftOrderService: this.draftOrderService_,
})
@@ -282,15 +275,18 @@ class OrderService extends BaseService {
"discount_total",
"gift_card_total",
"total",
"paid_total",
"refunded_total",
"refundable_amount",
"items.refundable",
"swaps.additional_items.refundable",
]
const totalsToSelect = select.filter(v => totalFields.includes(v))
if (totalsToSelect.length > 0) {
const relationSet = new Set(relations)
relationSet.add("items")
relationSet.add("swaps")
relationSet.add("discounts")
relationSet.add("gift_cards")
relationSet.add("gift_card_transactions")
@@ -1084,177 +1080,6 @@ class OrderService extends BaseService {
return toReturn.filter(i => !!i)
}
/**
* Checks that a given quantity of a line item can be returned. Fails if the
* item is undefined or if the returnable quantity of the item is lower, than
* the quantity that is requested to be returned.
* @param {LineItem?} item - the line item to check has sufficient returnable
* quantity.
* @param {number} quantity - the quantity that is requested to be returned.
* @return {LineItem} a line item where the quantity is set to the requested
* return quantity.
*/
validateReturnLineItem_(item, quantity) {
if (!item) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Return contains invalid line item"
)
}
const returnable = item.quantity - (item.returned_quantity || 0)
if (quantity > returnable) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Cannot return more items than have been purchased"
)
}
return {
...item,
quantity,
}
}
/**
* Creates a return request for an order, with given items, and a shipping
* method. If no refundAmount is provided the refund amount is calculated from
* the return lines and the shipping cost.
* @param {String} orderId - the id of the order to create a return for.
* @param {Array<{item_id: String, quantity: Int}>} items - the line items to
* return
* @param {ShippingMethod?} shippingMethod - the shipping method used for the
* return
* @param {Number?} refundAmount - the amount to refund when the return is
* received.
* @returns {Promise<Order>} the resulting order.
*/
async requestReturn(orderId, items, shippingMethod, refundAmount) {
return this.atomicPhase_(async manager => {
const order = await this.retrieve(orderId, {
select: ["refunded_total", "total"],
relations: ["items"],
})
const returnObj = {
order_id: orderId,
items,
shipping_method: shippingMethod,
refund_amount: refundAmount,
}
const returnRequest = await this.returnService_
.withTransaction(manager)
.create(returnObj, order)
const fulfilledReturn = await this.returnService_
.withTransaction(manager)
.fulfill(returnRequest.id)
const result = await this.retrieve(orderId)
this.eventBus_
.withTransaction(manager)
.emit(OrderService.Events.RETURN_REQUESTED, {
id: result.id,
return_id: fulfilledReturn.id,
})
return result
})
}
/**
* Registers a previously requested return as received. This will create a
* refund to the customer. If the returned items don't match the requested
* items the return status will be updated to requires_action. This behaviour
* is useful in sitautions where a custom refund amount is requested, but the
* retuned items are not matching the requested items. Setting the
* allowMismatch argument to true, will process the return, ignoring any
* mismatches.
* @param {string} orderId - the order to return.
* @param {string[]} lineItems - the line items to return
* @return {Promise} the result of the update operation
*/
async receiveReturn(
orderId,
returnId,
items,
refundAmount,
allowMismatch = false
) {
return this.atomicPhase_(async manager => {
const order = await this.retrieve(orderId, {
relations: ["items", "returns", "payments"],
})
const returnRequest = await this.returnService_.retrieve(returnId)
if (!returnRequest || returnRequest.order_id !== orderId) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Return request with id ${returnId} was not found`
)
}
const updatedReturn = await this.returnService_
.withTransaction(manager)
.receiveReturn(returnId, items, refundAmount, allowMismatch)
const orderRepo = manager.getCustomRepository(this.orderRepository_)
if (updatedReturn.status === "requires_action") {
order.fulfillment_status = "requires_action"
const result = await orderRepo.save(order)
this.eventBus_
.withTransaction(manager)
.emit(OrderService.Events.RETURN_ACTION_REQUIRED, {
id: result.id,
return_id: updatedReturn.id,
})
return result
}
let isFullReturn = true
for (const i of order.items) {
const isReturn = updatedReturn.items.find(r => i.id === r.item_id)
if (isReturn) {
const returnedQuantity =
(i.returned_quantity || 0) + isReturn.quantity
if (i.quantity !== returnedQuantity) {
isFullReturn = false
}
await this.lineItemService_.withTransaction(manager).update(i.id, {
returned_quantity: returnedQuantity,
})
} else {
if (!i.returned_quantity !== i.quantity) {
isFullReturn = false
}
}
}
if (updatedReturn.refund_amount > 0) {
await this.paymentProviderService_
.withTransaction(manager)
.refundPayment(order.payments, updatedReturn.refund_amount, "return")
}
if (isFullReturn) {
order.fulfillment_status = "returned"
} else {
order.fulfillment_status = "partially_returned"
}
const result = await orderRepo.save(order)
await this.eventBus_
.withTransaction(manager)
.emit(OrderService.Events.ITEMS_RETURNED, {
id: order.id,
return_id: updatedReturn.id,
})
return result
})
}
/**
* Archives an order. It only alloved, if the order has been fulfilled
* and payment has been captured.
@@ -1331,10 +1156,13 @@ class OrderService extends BaseService {
if (totalsFields.includes("refunded_total")) {
order.refunded_total = this.totalsService_.getRefundedTotal(order)
}
if (totalsFields.includes("paid_total")) {
order.paid_total = this.totalsService_.getPaidTotal(order)
}
if (totalsFields.includes("refundable_amount")) {
const total = this.totalsService_.getTotal(order)
const paid_total = this.totalsService_.getPaidTotal(order)
const refunded_total = this.totalsService_.getRefundedTotal(order)
order.refundable_amount = total - refunded_total
order.refundable_amount = paid_total - refunded_total
}
if (totalsFields.includes("items.refundable")) {
@@ -1347,86 +1175,94 @@ class OrderService extends BaseService {
}))
}
if (
totalsFields.includes("swaps.additional_items.refundable") &&
order.swaps &&
order.swaps.length
) {
for (const s of order.swaps) {
s.additional_items = s.additional_items.map(i => ({
...i,
refundable: this.totalsService_.getLineItemRefund(order, {
...i,
quantity: i.quantity - (i.returned_quantity || 0),
}),
}))
}
}
return order
}
/**
* Dedicated method to set metadata for an order.
* To ensure that plugins does not overwrite each
* others metadata fields, setMetadata is provided.
* @param {string} orderId - the order to decorate.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
* Handles receiving a return. This will create a
* refund to the customer. If the returned items don't match the requested
* items the return status will be updated to requires_action. This behaviour
* is useful in sitautions where a custom refund amount is requested, but the
* retuned items are not matching the requested items. Setting the
* allowMismatch argument to true, will process the return, ignoring any
* mismatches.
* @param {string} orderId - the order to return.
* @param {object} receivedReturn - the received return
* @return {Promise} the result of the update operation
*/
setMetadata_(order, metadata) {
const existing = order.metadata || {}
const newData = {}
for (const [key, value] of Object.entries(metadata)) {
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
newData[key] = value
}
const updated = {
...existing,
...newData,
}
return updated
}
/**
* Registers the swap return items as received so that they cannot be used
* as a part of other swaps/returns.
* @param {string} id - the id of the order with the swap.
* @param {string} swapId - the id of the swap that has been received.
* @returns {Promise<Order>} the resulting order
*/
async registerSwapReceived(id, swapId) {
async registerReturnReceived(orderId, receivedReturn, customRefundAmount) {
return this.atomicPhase_(async manager => {
const order = await this.retrieve(id, { relations: ["items"] })
const swap = await this.swapService_
.withTransaction(manager)
.retrieve(swapId, { relations: ["return_order", "return_order.items"] })
const order = await this.retrieve(orderId, {
select: ["total", "refunded_total", "refundable_amount"],
relations: ["items", "returns", "payments"],
})
if (!swap || swap.order_id !== id) {
if (!receivedReturn || receivedReturn.order_id !== orderId) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Swap must belong to the given order"
MedusaError.Types.NOT_FOUND,
`Received return does not exist`
)
}
if (swap.return_order.status !== "received") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Swap is not received"
)
}
let refundAmount = customRefundAmount || receivedReturn.refund_amount
for (const i of order.items) {
const isReturn = swap.return_order.items.find(ri => i.id === ri.item_id)
const orderRepo = manager.getCustomRepository(this.orderRepository_)
if (isReturn) {
const returnedQuantity =
(i.returned_quantity || 0) + isReturn.quantity
await this.lineItemService_.withTransaction(manager).update(i.id, {
returned_quantity: returnedQuantity,
if (refundAmount > order.refundable_amount) {
order.fulfillment_status = "requires_action"
const result = await orderRepo.save(order)
this.eventBus_
.withTransaction(manager)
.emit(OrderService.Events.RETURN_ACTION_REQUIRED, {
id: result.id,
return_id: receivedReturn.id,
})
return result
}
let isFullReturn = true
for (const i of order.items) {
if (i.returned_quantity !== i.quantity) {
isFullReturn = false
}
}
const result = await this.retrieve(id)
if (receivedReturn.refund_amount > 0) {
const refund = await this.paymentProviderService_
.withTransaction(manager)
.refundPayment(order.payments, receivedReturn.refund_amount, "return")
order.refunds = [...(order.refunds || []), refund]
}
if (isFullReturn) {
order.fulfillment_status = "returned"
} else {
order.fulfillment_status = "partially_returned"
}
const result = await orderRepo.save(order)
await this.eventBus_
.withTransaction(manager)
.emit(OrderService.Events.SWAP_RECEIVED, {
id: result.id,
swap_id: swapId,
.emit(OrderService.Events.ITEMS_RETURNED, {
id: order.id,
return_id: receivedReturn.id,
})
return result
})
@@ -403,13 +403,15 @@ class PaymentProviderService extends BaseService {
}
const refundRepo = manager.getCustomRepository(this.refundRepository_)
const created = refundRepo.create({
const toCreate = {
order_id,
amount,
reason,
note,
})
}
const created = refundRepo.create(toCreate)
return refundRepo.save(created)
})
}
@@ -298,7 +298,10 @@ class ProductVariantService extends BaseService {
const result = await variantRepo.save(variant)
await this.eventBus_
.withTransaction(manager)
.emit(ProductVariantService.Events.UPDATED, result)
.emit(ProductVariantService.Events.UPDATED, {
id: result.id,
fields: Object.keys(update),
})
return result
})
}
+43 -3
View File
@@ -23,6 +23,7 @@ class ProductService extends BaseService {
productCollectionService,
productTypeRepository,
productTagRepository,
imageRepository,
}) {
super()
@@ -52,6 +53,9 @@ class ProductService extends BaseService {
/** @private @const {ProductCollectionService} */
this.productTagRepository_ = productTagRepository
/** @private @const {ImageRepository} */
this.imageRepository_ = imageRepository
}
withTransaction(transactionManager) {
@@ -69,6 +73,7 @@ class ProductService extends BaseService {
productCollectionService: this.productCollectionService_,
productTagRepository: this.productTagRepository_,
productTypeRepository: this.productTypeRepository_,
imageRepository: this.imageRepository_,
})
cloned.transactionManager_ = transactionManager
@@ -233,7 +238,7 @@ class ProductService extends BaseService {
})
if (existing) {
return existing
return existing.id
}
const created = productTypeRepository.create(type)
@@ -277,10 +282,18 @@ class ProductService extends BaseService {
this.productOptionRepository_
)
const { options, tags, type, ...rest } = productObject
const { options, tags, type, images, ...rest } = productObject
if (!rest.thumbnail && images && images.length) {
rest.thumbnail = images[0]
}
let product = productRepo.create(rest)
if (images && images.length) {
product.images = await this.upsertImages_(images)
}
if (tags) {
product.tags = await this.upsertProductTags_(tags)
}
@@ -310,6 +323,28 @@ class ProductService extends BaseService {
})
}
async upsertImages_(images) {
const imageRepository = this.manager_.getCustomRepository(
this.imageRepository_
)
let productImages = []
for (const img of images) {
const existing = await imageRepository.findOne({
where: { url: img },
})
if (existing) {
productImages.push(existing)
} else {
const created = imageRepository.create({ url: img })
productImages.push(created)
}
}
return productImages
}
/**
* Updates a product. Product variant updates should use dedicated methods,
* e.g. `addVariant`, etc. The function will throw errors if metadata or
@@ -327,7 +362,7 @@ class ProductService extends BaseService {
)
const product = await this.retrieve(productId, {
relations: ["variants", "tags"],
relations: ["variants", "tags", "images"],
})
const {
@@ -344,6 +379,10 @@ class ProductService extends BaseService {
product.thumbnail = images[0]
}
if (images && images.length) {
product.images = await this.upsertImages_(images)
}
if (metadata) {
product.metadata = this.setMetadata_(product, metadata)
}
@@ -405,6 +444,7 @@ class ProductService extends BaseService {
.withTransaction(manager)
.emit(ProductService.Events.UPDATED, {
id: result.id,
fields: Object.keys(update),
})
return result
})
@@ -0,0 +1,97 @@
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces"
class ReturnReasonService extends BaseService {
constructor({ manager, returnReasonRepository }) {
super()
/** @private @constant {EntityManager} */
this.manager_ = manager
/** @private @constant {ReturnReasonRepository} */
this.retReasonRepo_ = returnReasonRepository
}
withTransaction(manager) {
if (!manager) {
return this
}
const cloned = new ReturnReasonService({
manager,
returnReasonRepository: this.retReasonRepo_,
})
cloned.transactionManager_ = manager
return cloned
}
create(data) {
return this.atomicPhase_(async manager => {
const rrRepo = manager.getCustomRepository(this.retReasonRepo_)
const created = rrRepo.create(data)
const result = await rrRepo.save(created)
return result
})
}
update(id, data) {
return this.atomicPhase_(async manager => {
const rrRepo = manager.getCustomRepository(this.retReasonRepo_)
const reason = await this.retrieve(id)
if ("description" in data) {
reason.description = data.description
}
if ("label" in data) {
reason.label = data.label
}
await rrRepo.save(reason)
return reason
})
}
/**
* @param {Object} selector - the query object for find
* @return {Promise} the result of the find operation
*/
async list(
selector,
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
) {
const rrRepo = this.manager_.getCustomRepository(this.retReasonRepo_)
const query = this.buildQuery_(selector, config)
return rrRepo.find(query)
}
/**
* Gets an order by id.
* @param {string} orderId - id of order to retrieve
* @return {Promise<Order>} the order document
*/
async retrieve(id, config = {}) {
const rrRepo = this.manager_.getCustomRepository(this.retReasonRepo_)
const validatedId = this.validateId_(id)
const query = this.buildQuery_({ id: validatedId }, config)
const item = await rrRepo.findOne(query)
if (!item) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Return Reason with id: ${id} was not found.`
)
}
return item
}
}
export default ReturnReasonService
+96 -47
View File
@@ -14,7 +14,9 @@ class ReturnService extends BaseService {
returnRepository,
returnItemRepository,
shippingOptionService,
returnReasonService,
fulfillmentProviderService,
orderService,
}) {
super()
@@ -38,6 +40,11 @@ class ReturnService extends BaseService {
/** @private @const {FulfillmentProviderService} */
this.fulfillmentProviderService_ = fulfillmentProviderService
this.returnReasonService_ = returnReasonService
/** @private @const {OrderService} */
this.orderService_ = orderService
}
withTransaction(transactionManager) {
@@ -53,6 +60,8 @@ class ReturnService extends BaseService {
returnItemRepository: this.returnItemRepository_,
shippingOptionService: this.shippingOptionService_,
fulfillmentProviderService: this.fulfillmentProviderService_,
returnReasonService: this.returnReasonService_,
orderService: this.orderService_,
})
cloned.transactionManager_ = transactionManager
@@ -61,7 +70,7 @@ class ReturnService extends BaseService {
}
/**
* Retrieves the order line items, given an array of items.
* Retrieves the order line items, given an array of items
* @param {Order} order - the order to get line items from
* @param {{ item_id: string, quantity: number }} items - the items to get
* @param {function} transformer - a function to apply to each of the items
@@ -71,10 +80,19 @@ class ReturnService extends BaseService {
* @return {Promise<Array<LineItem>>} the line items generated by the transformer.
*/
async getFulfillmentItems_(order, items, transformer) {
let merged = [...order.items]
// merge items from order with items from order swaps
if (order.swaps && order.swaps.length) {
for (const s of order.swaps) {
merged = [...merged, ...s.additional_items]
}
}
const toReturn = await Promise.all(
items.map(async ({ item_id, quantity }) => {
const item = order.items.find(i => i.id === item_id)
return transformer(item, quantity)
items.map(async data => {
const item = merged.find(i => i.id === data.item_id)
return transformer(item, data.quantity, data)
})
)
@@ -127,10 +145,11 @@ class ReturnService extends BaseService {
* @param {LineItem?} item - the line item to check has sufficient returnable
* quantity.
* @param {number} quantity - the quantity that is requested to be returned.
* @param {object} additional - the quantity that is requested to be returned.
* @return {LineItem} a line item where the quantity is set to the requested
* return quantity.
*/
validateReturnLineItem_(item, quantity) {
validateReturnLineItem_(item, quantity, additional) {
if (!item) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -146,10 +165,20 @@ class ReturnService extends BaseService {
)
}
return {
const toReturn = {
...item,
quantity,
}
if ("reason_id" in additional) {
toReturn.reason_id = additional.reason_id
}
if ("note" in additional) {
toReturn.note = additional.note
}
return toReturn
}
/**
@@ -228,14 +257,26 @@ class ReturnService extends BaseService {
* @param {object} orderLike - order object
* @returns {Promise<Return>} the resulting order.
*/
async create(data, orderLike) {
async create(data) {
return this.atomicPhase_(async manager => {
const returnRepository = manager.getCustomRepository(
this.returnRepository_
)
let orderId = data.order_id
if (data.swap_id) {
delete data.order_id
}
const order = await this.orderService_
.withTransaction(manager)
.retrieve(orderId, {
select: ["refunded_total", "total", "refundable_amount"],
relations: ["swaps", "swaps.additional_items", "items"],
})
const returnLines = await this.getFulfillmentItems_(
orderLike,
order,
data.items,
this.validateReturnLineItem_
)
@@ -251,7 +292,9 @@ class ReturnService extends BaseService {
let toRefund = data.refund_amount
if (typeof toRefund !== "undefined") {
const refundable = orderLike.total - orderLike.refunded_total
// refundable from order
let refundable = order.refundable_amount
if (toRefund > refundable) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -259,16 +302,12 @@ class ReturnService extends BaseService {
)
}
} else {
toRefund = await this.totalsService_.getRefundTotal(
orderLike,
returnLines
)
toRefund = await this.totalsService_.getRefundTotal(order, returnLines)
if (data.shipping_method) {
toRefund = Math.max(
0,
toRefund -
data.shipping_method.price * (1 + orderLike.tax_rate / 100)
toRefund - data.shipping_method.price * (1 + order.tax_rate / 100)
)
}
}
@@ -288,6 +327,8 @@ class ReturnService extends BaseService {
item_id: i.id,
quantity: i.quantity,
requested_quantity: i.quantity,
reason_id: i.reason_id,
note: i.note,
metadata: i.metadata,
})
)
@@ -324,11 +365,13 @@ class ReturnService extends BaseService {
],
})
let returnData = { ...returnOrder }
const items = await this.lineItemService_.list({
id: returnOrder.items.map(({ item_id }) => item_id),
})
returnOrder.items = returnOrder.items.map(item => {
returnData.items = returnOrder.items.map(item => {
const found = items.find(i => i.id === item.item_id)
return {
...item,
@@ -348,7 +391,7 @@ class ReturnService extends BaseService {
}
const fulfillmentData = await this.fulfillmentProviderService_.createReturn(
returnOrder
returnData
)
returnOrder.shipping_data = fulfillmentData
@@ -371,36 +414,37 @@ class ReturnService extends BaseService {
* @param {string[]} lineItems - the line items to return
* @return {Promise} the result of the update operation
*/
async receiveReturn(
returnId,
receivedItems,
refundAmount,
allowMismatch = false
) {
async receive(returnId, receivedItems, refundAmount, allowMismatch = false) {
return this.atomicPhase_(async manager => {
const returnRepository = manager.getCustomRepository(
this.returnRepository_
)
const returnObj = await this.retrieve(returnId, {
relations: [
"items",
"order",
"order.items",
"order.discounts",
"order.refunds",
"order.shipping_methods",
"order.region",
"swap",
"swap.order",
"swap.order.items",
"swap.order.refunds",
"swap.order.shipping_methods",
"swap.order.region",
],
relations: ["items", "swap", "swap.additional_items"],
})
const order = returnObj.order || (returnObj.swap && returnObj.swap.order)
let orderId = returnObj.order_id
// check if return is requested on a swap
if (returnObj.swap) {
orderId = returnObj.swap.order_id
}
const order = await this.orderService_
.withTransaction(manager)
.retrieve(orderId, {
relations: [
"items",
"returns",
"payments",
"discounts",
"refunds",
"shipping_methods",
"region",
"swaps",
"swaps.additional_items",
],
})
if (returnObj.status === "received") {
throw new MedusaError(
@@ -445,24 +489,29 @@ class ReturnService extends BaseService {
returnStatus = "requires_action"
}
const toRefund = refundAmount || returnObj.refund_amount
const total = await this.totalsService_.getTotal(order)
const refunded = await this.totalsService_.getRefundedTotal(order)
if (toRefund > total - refunded) {
returnStatus = "requires_action"
}
const totalRefundableAmount = refundAmount || returnObj.refund_amount
const now = new Date()
const updateObj = {
...returnObj,
status: returnStatus,
items: newLines,
refund_amount: toRefund,
refund_amount: totalRefundableAmount,
received_at: now.toISOString(),
}
const result = await returnRepository.save(updateObj)
for (const i of returnObj.items) {
const lineItem = await this.lineItemService_
.withTransaction(manager)
.retrieve(i.item_id)
const returnedQuantity = (lineItem.returned_quantity || 0) + i.quantity
await this.lineItemService_.withTransaction(manager).update(i.item_id, {
returned_quantity: returnedQuantity,
})
}
return result
})
}
+75 -41
View File
@@ -9,6 +9,7 @@ import { MedusaError } from "medusa-core-utils"
class SwapService extends BaseService {
static Events = {
CREATED: "swap.created",
RECEIVED: "swap.received",
SHIPMENT_CREATED: "swap.shipment_created",
PAYMENT_COMPLETED: "swap.payment_completed",
PAYMENT_CAPTURED: "swap.payment_captured",
@@ -28,6 +29,7 @@ class SwapService extends BaseService {
paymentProviderService,
shippingOptionService,
fulfillmentService,
orderService,
}) {
super()
@@ -55,6 +57,9 @@ class SwapService extends BaseService {
/** @private @const {FulfillmentService} */
this.fulfillmentService_ = fulfillmentService
/** @private @const {OrderService} */
this.orderService_ = orderService
/** @private @const {ShippingOptionService} */
this.shippingOptionService_ = shippingOptionService
@@ -77,6 +82,7 @@ class SwapService extends BaseService {
lineItemService: this.lineItemService_,
paymentProviderService: this.paymentProviderService_,
shippingOptionService: this.shippingOptionService_,
orderService: this.orderService_,
fulfillmentService: this.fulfillmentService_,
})
@@ -239,14 +245,12 @@ class SwapService extends BaseService {
const result = await swapRepo.save(created)
await this.returnService_.withTransaction(manager).create(
{
swap_id: result.id,
items: returnItems,
shipping_method: returnShipping,
},
order
)
await this.returnService_.withTransaction(manager).create({
swap_id: result.id,
order_id: order.id,
items: returnItems,
shipping_method: returnShipping,
})
await this.eventBus_
.withTransaction(manager)
@@ -273,6 +277,10 @@ class SwapService extends BaseService {
const swapRepo = manager.getCustomRepository(this.swapRepository_)
if (swap.difference_due < 0) {
if (swap.payment_status === "difference_refunded") {
return swap
}
try {
await this.paymentProviderService_
.withTransaction(manager)
@@ -299,6 +307,10 @@ class SwapService extends BaseService {
.emit(SwapService.Events.REFUND_PROCESSED, result)
return result
} else if (swap.difference_due === 0) {
if (swap.payment_status === "difference_refunded") {
return swap
}
swap.payment_status = "difference_refunded"
const result = await swapRepo.save(swap)
@@ -309,6 +321,10 @@ class SwapService extends BaseService {
}
try {
if (swap.payment_status === "captured") {
return swap
}
await this.paymentProviderService_
.withTransaction(manager)
.capturePayment(swap.payment)
@@ -365,6 +381,8 @@ class SwapService extends BaseService {
relations: [
"order",
"order.items",
"order.swaps",
"order.swaps.additional_items",
"order.discounts",
"additional_items",
"return_order",
@@ -382,8 +400,13 @@ class SwapService extends BaseService {
const order = swap.order
// filter out free shipping discounts
const discounts =
order?.discounts?.filter(({ rule }) => rule.type !== "free_shipping") ||
undefined
const cart = await this.cartService_.withTransaction(manager).create({
discounts: order.discounts,
discounts,
email: order.email,
billing_address_id: order.billing_address_id,
shipping_address_id: order.shipping_address_id,
@@ -419,7 +442,15 @@ class SwapService extends BaseService {
}
for (const r of swap.return_order.items) {
const lineItem = order.items.find(i => i.id === r.item_id)
let allItems = [...order.items]
if (order.swaps && order.swaps.length) {
for (const s of order.swaps) {
allItems = [...allItems, ...s.additional_items]
}
}
const lineItem = allItems.find(i => i.id === r.item_id)
const toCreate = {
cart_id: cart.id,
@@ -499,6 +530,7 @@ class SwapService extends BaseService {
.withTransaction(manager)
.updatePayment(payment.id, {
swap_id: swapId,
order_id: swap.order_id,
})
}
@@ -720,37 +752,6 @@ class SwapService extends BaseService {
})
}
/**
* Dedicated method to set metadata for an order.
* To ensure that plugins does not overwrite each
* others metadata fields, setMetadata is provided.
* @param {string} orderId - the order to decorate.
* @param {string} key - key for metadata field
* @param {string} value - value for metadata field.
* @return {Promise} resolves to the updated result.
*/
setMetadata_(swap, metadata) {
const existing = swap.metadata || {}
const newData = {}
for (const [key, value] of Object.entries(metadata)) {
if (typeof key !== "string") {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Key type is invalid. Metadata keys must be strings"
)
}
newData[key] = value
}
const updated = {
...existing,
...newData,
}
return updated
}
/**
* Dedicated method to delete metadata for a swap.
* @param {string} swapId - the order to delete metadata from.
@@ -774,6 +775,39 @@ class SwapService extends BaseService {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
}
/**
* Registers the swap return items as received so that they cannot be used
* as a part of other swaps/returns.
* @param {string} id - the id of the order with the swap.
* @param {string} swapId - the id of the swap that has been received.
* @returns {Promise<Order>} the resulting order
*/
async registerReceived(id) {
return this.atomicPhase_(async manager => {
const swap = await this.retrieve(id, {
relations: ["return_order", "return_order.items"],
})
if (swap.return_order.status !== "received") {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Swap is not received"
)
}
const result = await this.retrieve(id)
await this.eventBus_
.withTransaction(manager)
.emit(SwapService.Events.RECEIVED, {
id: id,
order_id: result.order_id,
})
return result
})
}
}
export default SwapService
+43 -4
View File
@@ -26,6 +26,26 @@ class TotalsService extends BaseService {
return subtotal + taxTotal + shippingTotal - discountTotal - giftCardTotal
}
getPaidTotal(order) {
const total = order.payments?.reduce((acc, next) => {
acc += next.amount
return acc
}, 0)
return total
}
getSwapTotal(order) {
let swapTotal = 0
if (order.swaps && order.swaps.length) {
for (const s of order.swaps) {
swapTotal = swapTotal + s.difference_due
}
}
return swapTotal
}
/**
* Calculates subtotal of a given cart or order.
* @param {Cart || Order} object - cart or order to calculate subtotal for
@@ -128,7 +148,16 @@ class TotalsService extends BaseService {
* @return {int} the calculated subtotal
*/
getRefundTotal(order, lineItems) {
const itemIds = order.items.map(i => i.id)
let itemIds = order.items.map(i => i.id)
// in case we swap a swap, we need to include swap items
if (order.swaps && order.swaps.length) {
for (const s of order.swaps) {
const swapItemIds = s.additional_items.map(el => el.id)
itemIds = [...itemIds, ...swapItemIds]
}
}
const refunds = lineItems.map(i => {
if (!itemIds.includes(i.id)) {
throw new MedusaError(
@@ -213,6 +242,16 @@ class TotalsService extends BaseService {
getLineDiscounts(cart, discount) {
const subtotal = this.getSubtotal(cart, { excludeNonDiscounts: true })
let merged = [...cart.items]
// merge items from order with items from order swaps
if (cart.swaps && cart.swaps.length) {
for (const s of cart.swaps) {
merged = [...merged, ...s.additional_items]
}
}
const { type, allocation, value } = discount.rule
if (allocation === "total") {
let percentage = 0
@@ -225,7 +264,7 @@ class TotalsService extends BaseService {
percentage = nominator / subtotal
}
return cart.items.map(item => {
return merged.map(item => {
const lineTotal = item.unit_price * item.quantity
return {
@@ -239,7 +278,7 @@ class TotalsService extends BaseService {
cart,
type
)
return cart.items.map(item => {
return merged.map(item => {
const discounted = allocationDiscounts.find(
a => a.lineItem.id === item.id
)
@@ -250,7 +289,7 @@ class TotalsService extends BaseService {
})
}
return cart.items.map(i => ({ item: i, amount: 0 }))
return merged.map(i => ({ item: i, amount: 0 }))
}
getGiftCardTotal(cart) {
+2 -1
View File
@@ -53,8 +53,9 @@ class OrderSubscriber {
await Promise.all(
order.discounts.map(async d => {
const usageCount = d?.usage_count || 0
return this.discountService_.update(d.id, {
usage_count: d.usage_count + 1,
usage_count: usageCount + 1,
})
})
)