Feat/nested return reasons (#418)

* api endpoints for nested return_reasons

* add nested return reasons to database

* add parent reason to update

* integration tests

* add children relation

* integration tests for nested returns and failing doubly nesting returns

* add delete-route and nested relations

* delete return reason route

* doubly nested return reason creation check and deletion

* nested return reasons migration

* list only parent reasons

* removed null filter

* remove empty migration

* add return reason filter to get list of categories with children

* removed console log

* corrected delete route

* return reason testing

* return reasons query

* listREasonsFromIDs

* create return testing

* listReasonsFromIds

* return reason tests

* failing if returnreason has child on return

* integration tests

* cascading deletes on return reasons

* more elegant checking for children of return reasons when creating a return

* remove console.log

* pr adjust

Co-authored-by: Philip Korsholm <phko@MacBook-Pro.localdomain>
This commit is contained in:
pKorsholmandPhilip Korsholm authored and GitHub committed 2021-09-29 09:22:09 +02:00
1 parent 4db860f9ab
commit 42cdfde6d9
15 files changed
+742 -93

No files matched your search

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/admin/return-reasons POST /admin/return-reasons creates a return_reason 1`] = `
Object {
"created_at": Any<String>,
"deleted_at": null,
"description": "Use this if the size was too big",
"id": Any<String>,
"label": "Too Big",
"parent_return_reason": null,
"parent_return_reason_id": null,
"return_reason_children": Array [],
"updated_at": Any<String>,
"value": "too_big",
}
`;
@@ -1,53 +1,55 @@
const path = require("path"); const { match } = require("assert")
const path = require("path")
const { RepositoryNotTreeError } = require("typeorm")
const setupServer = require("../../../helpers/setup-server"); const setupServer = require("../../../helpers/setup-server")
const { useApi } = require("../../../helpers/use-api"); const { useApi } = require("../../../helpers/use-api")
const { initDb, useDb } = require("../../../helpers/use-db"); const { initDb, useDb } = require("../../../helpers/use-db")
const adminSeeder = require("../../helpers/admin-seeder"); const adminSeeder = require("../../helpers/admin-seeder")
jest.setTimeout(30000); jest.setTimeout(30000)
describe("/admin/return-reasons", () => { describe("/admin/return-reasons", () => {
let medusaProcess; let medusaProcess
let dbConnection; let dbConnection
beforeAll(async () => { beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..")); const cwd = path.resolve(path.join(__dirname, "..", ".."))
dbConnection = await initDb({ cwd }); dbConnection = await initDb({ cwd })
medusaProcess = await setupServer({ cwd }); medusaProcess = await setupServer({ cwd })
}); })
afterAll(async () => { afterAll(async () => {
const db = useDb(); const db = useDb()
await db.shutdown(); await db.shutdown()
medusaProcess.kill(); medusaProcess.kill()
}); })
describe("POST /admin/return-reasons", () => { describe("POST /admin/return-reasons", () => {
beforeEach(async () => { beforeEach(async () => {
try { try {
await adminSeeder(dbConnection); await adminSeeder(dbConnection)
} catch (err) { } catch (err) {
console.log(err); console.log(err)
throw err; throw err
} }
}); })
afterEach(async () => { afterEach(async () => {
const db = useDb(); const db = useDb()
await db.teardown(); await db.teardown()
}); })
it("creates a return_reason", async () => { it("creates a return_reason", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
label: "Too Big", label: "Too Big",
description: "Use this if the size was too big", description: "Use this if the size was too big",
value: "too_big", value: "too_big",
}; }
const response = await api const response = await api
.post("/admin/return-reasons", payload, { .post("/admin/return-reasons", payload, {
@@ -56,10 +58,172 @@ describe("/admin/return-reasons", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.return_reason).toMatchSnapshot({
id: expect.any(String),
created_at: expect.any(String),
updated_at: expect.any(String),
parent_return_reason: null,
parent_return_reason_id: null,
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
})
})
it("creates a nested return reason", async () => {
const api = useApi()
const payload = {
label: "Wrong size",
description: "Use this if the size was too big",
value: "wrong_size",
}
const response = await api
.post("/admin/return-reasons", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual(
expect.objectContaining({
label: "Wrong size",
description: "Use this if the size was too big",
value: "wrong_size",
})
)
const nested_payload = {
parent_return_reason_id: response.data.return_reason.id,
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
}
const nested_response = await api
.post("/admin/return-reasons", nested_payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(nested_response.status).toEqual(200)
expect(nested_response.data.return_reason).toEqual(
expect.objectContaining({
parent_return_reason_id: response.data.return_reason.id,
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
})
)
})
it("fails to create a doubly nested return reason", async () => {
expect.assertions(5)
const api = useApi()
const payload = {
label: "Wrong size",
description: "Use this if the size was too big",
value: "wrong_size",
}
const response = await api
.post("/admin/return-reasons", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual(
expect.objectContaining({
label: "Wrong size",
description: "Use this if the size was too big",
value: "wrong_size",
})
)
const nested_payload = {
parent_return_reason_id: response.data.return_reason.id,
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
}
const nested_response = await api
.post("/admin/return-reasons", nested_payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
const dbl_nested_payload = {
parent_return_reason_id: nested_response.data.return_reason.id,
label: "Too large size",
description: "Use this if the size was too big",
value: "large_size",
}
const dbl_nested_response = await api
.post("/admin/return-reasons", dbl_nested_payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
expect(err.response.status).toEqual(400)
expect(err.response.data.type).toEqual("invalid_data")
expect(err.response.data.message).toEqual(
"Doubly nested return reasons is not supported"
)
})
})
it("deletes a return_reason", async () => {
const api = useApi()
const payload = {
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
}
const response = await api
.post("/admin/return-reasons", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual( expect(response.data.return_reason).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -67,17 +231,37 @@ describe("/admin/return-reasons", () => {
description: "Use this if the size was too big", description: "Use this if the size was too big",
value: "too_big", value: "too_big",
}) })
); )
});
const deleteResponse = await api
.delete(`/admin/return-reasons/${response.data.return_reason.id}`, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(deleteResponse.data).toEqual(
expect.objectContaining({
id: response.data.return_reason.id,
object: "return_reason",
deleted: true,
})
)
})
it("update a return reason", async () => { it("update a return reason", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
label: "Too Big Typo", label: "Too Big Typo",
description: "Use this if the size was too big", description: "Use this if the size was too big",
value: "too_big", value: "too_big",
}; }
const response = await api const response = await api
.post("/admin/return-reasons", payload, { .post("/admin/return-reasons", payload, {
@@ -86,10 +270,10 @@ describe("/admin/return-reasons", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual( expect(response.data.return_reason).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -97,7 +281,7 @@ describe("/admin/return-reasons", () => {
description: "Use this if the size was too big", description: "Use this if the size was too big",
value: "too_big", value: "too_big",
}) })
); )
const newResponse = await api const newResponse = await api
.post( .post(
@@ -113,8 +297,8 @@ describe("/admin/return-reasons", () => {
} }
) )
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(newResponse.data.return_reason).toEqual( expect(newResponse.data.return_reason).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -122,17 +306,81 @@ describe("/admin/return-reasons", () => {
description: "new desc", description: "new desc",
value: "too_big", value: "too_big",
}) })
); )
}); })
it("lists nested return reasons", async () => {
const api = useApi()
const payload = {
label: "Wrong size",
description: "Use this if the size was too big",
value: "wrong_size",
}
const response = await api
.post("/admin/return-reasons", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
const nested_payload = {
parent_return_reason_id: response.data.return_reason.id,
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
}
const resp = await api
.post("/admin/return-reasons", nested_payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
const nested_response = await api
.get("/admin/return-reasons", {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(nested_response.status).toEqual(200)
expect(nested_response.data.return_reasons).toEqual([
expect.objectContaining({
label: "Wrong size",
description: "Use this if the size was too big",
value: "wrong_size",
return_reason_children: expect.arrayContaining([
expect.objectContaining({
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
}),
]),
}),
])
})
it("list return reasons", async () => { it("list return reasons", async () => {
const api = useApi(); const api = useApi()
const payload = { const payload = {
label: "Too Big Typo", label: "Too Big Typo",
description: "Use this if the size was too big", description: "Use this if the size was too big",
value: "too_big", value: "too_big",
}; }
await api await api
.post("/admin/return-reasons", payload, { .post("/admin/return-reasons", payload, {
@@ -141,8 +389,8 @@ describe("/admin/return-reasons", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
const response = await api const response = await api
.get("/admin/return-reasons", { .get("/admin/return-reasons", {
@@ -151,15 +399,191 @@ describe("/admin/return-reasons", () => {
}, },
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
}); })
expect(response.status).toEqual(200); expect(response.status).toEqual(200)
expect(response.data.return_reasons).toEqual([ expect(response.data.return_reasons).toEqual([
expect.objectContaining({ expect.objectContaining({
value: "too_big", value: "too_big",
}), }),
]); ])
}); })
}); })
});
describe("DELETE /admin/return-reasons", () => {
beforeEach(async () => {
try {
await adminSeeder(dbConnection)
} catch (err) {
console.log(err)
throw err
}
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("deletes single return reason", async () => {
expect.assertions(6)
const api = useApi()
const payload = {
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
}
const response = await api
.post("/admin/return-reasons", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual(
expect.objectContaining({
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
})
)
const deleteResult = await api.delete(
`/admin/return-reasons/${response.data.return_reason.id}`,
{
headers: {
Authorization: "Bearer test_token",
},
}
)
expect(deleteResult.status).toEqual(200)
expect(deleteResult.data).toEqual({
id: response.data.return_reason.id,
object: "return_reason",
deleted: true,
})
const getResult = await api
.get(`/admin/return-reasons/${response.data.return_reason.id}`, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
expect(err.response.status).toEqual(404)
expect(err.response.data.type).toEqual("not_found")
})
})
it("deletes cascade through nested return reasons", async () => {
expect.assertions(10)
const api = useApi()
const payload = {
label: "Wrong Size",
description: "Use this if the size was wrong",
value: "wrong_size",
}
const response = await api
.post("/admin/return-reasons", payload, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response.status).toEqual(200)
expect(response.data.return_reason).toEqual(
expect.objectContaining({
label: "Wrong Size",
description: "Use this if the size was wrong",
value: "wrong_size",
})
)
const payload_child = {
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
parent_return_reason_id: response.data.return_reason.id,
}
const response_child = await api
.post("/admin/return-reasons", payload_child, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err)
})
expect(response_child.status).toEqual(200)
expect(response_child.data.return_reason).toEqual(
expect.objectContaining({
label: "Too Big",
description: "Use this if the size was too big",
value: "too_big",
parent_return_reason_id: response.data.return_reason.id,
})
)
const deleteResult = await api
.delete(`/admin/return-reasons/${response.data.return_reason.id}`, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
console.log(err.response.data)
})
expect(deleteResult.status).toEqual(200)
expect(deleteResult.data).toEqual({
id: response.data.return_reason.id,
object: "return_reason",
deleted: true,
})
await api
.get(`/admin/return-reasons/${response.data.return_reason.id}`, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
expect(err.response.status).toEqual(404)
expect(err.response.data.type).toEqual("not_found")
})
await api
.get(`/admin/return-reasons/${response_child.data.return_reason.id}`, {
headers: {
Authorization: "Bearer test_token",
},
})
.catch((err) => {
expect(err.response.status).toEqual(404)
expect(err.response.data.type).toEqual("not_found")
})
})
})
})
@@ -26,16 +26,35 @@ describe("/store/return-reasons", () => {
describe("GET /store/return-reasons", () => { describe("GET /store/return-reasons", () => {
let rrId; let rrId;
let rrId_1;
let rrId_2;
beforeEach(async () => { beforeEach(async () => {
try { try {
const created = dbConnection.manager.create(ReturnReason, { const created = dbConnection.manager.create(ReturnReason, {
value: "too_big", value: "wrong_size",
label: "Too Big", label: "Wrong size",
}); });
const result = await dbConnection.manager.save(created); const result = await dbConnection.manager.save(created);
rrId = result.id; rrId = result.id;
const created_child = dbConnection.manager.create(ReturnReason, {
value: "too_big",
label: "Too Big",
parent_return_reason_id: rrId
});
const result_child = await dbConnection.manager.save(created_child);
rrId_1 = result_child.id;
const created_2 = dbConnection.manager.create(ReturnReason, {
value: "too_big_1",
label: "Too Big 1",
});
const result_2 = await dbConnection.manager.save(created_2);
rrId_2 = result_2.id;
} catch (err) { } catch (err) {
console.log(err); console.log(err);
throw err; throw err;
@@ -59,7 +78,15 @@ describe("/store/return-reasons", () => {
expect(response.data.return_reasons).toEqual([ expect(response.data.return_reasons).toEqual([
expect.objectContaining({ expect.objectContaining({
id: rrId, id: rrId,
value: "wrong_size",
return_reason_children:[expect.objectContaining({
id: rrId_1,
value: "too_big", value: "too_big",
}),]
}),
expect.objectContaining({
id: rrId_2,
value: "too_big_1",
}), }),
]); ]);
}); });
@@ -8,6 +8,7 @@ const {
Product, Product,
ProductVariant, ProductVariant,
ShippingOption, ShippingOption,
FulfillmentProvider,
LineItem, LineItem,
Discount, Discount,
DiscountRule, DiscountRule,
@@ -37,6 +38,8 @@ describe("/store/carts", () => {
describe("POST /store/returns", () => { describe("POST /store/returns", () => {
let rrId; let rrId;
let rrId_child;
let rrResult;
beforeEach(async () => { beforeEach(async () => {
const manager = dbConnection.manager; const manager = dbConnection.manager;
@@ -44,6 +47,10 @@ describe("/store/carts", () => {
`ALTER SEQUENCE order_display_id_seq RESTART WITH 111` `ALTER SEQUENCE order_display_id_seq RESTART WITH 111`
); );
const defaultProfile = await manager.findOne(ShippingProfile, {
type: "default",
});
await manager.insert(Region, { await manager.insert(Region, {
id: "region", id: "region",
name: "Test Region", name: "Test Region",
@@ -98,10 +105,6 @@ describe("/store/carts", () => {
await manager.save(ord); await manager.save(ord);
const defaultProfile = await manager.findOne(ShippingProfile, {
type: "default",
});
await manager.insert(Product, { await manager.insert(Product, {
id: "test-product", id: "test-product",
title: "test product", title: "test product",
@@ -147,12 +150,23 @@ describe("/store/carts", () => {
}); });
const created = dbConnection.manager.create(ReturnReason, { const created = dbConnection.manager.create(ReturnReason, {
value: "too_big", value: "wrong_size",
label: "Too Big", label: "Wrong Size",
}); });
const result = await dbConnection.manager.save(created); const result = await dbConnection.manager.save(created);
rrResult = result
rrId = result.id; rrId = result.id;
const created_1 = dbConnection.manager.create(ReturnReason, {
value: "too_big",
label: "Too Big",
parent_return_reason_id: rrId,
});
const result_1 = await dbConnection.manager.save(created_1);
rrId_child = result_1.id;
}); });
afterEach(async () => { afterEach(async () => {
@@ -181,6 +195,59 @@ describe("/store/carts", () => {
expect(response.data.return.refund_amount).toEqual(8000); expect(response.data.return.refund_amount).toEqual(8000);
}); });
it("failes to create a return with a reason category", async () => {
const api = useApi();
const response = await api
.post("/store/returns", {
order_id: "order_test",
items: [
{
reason_id: rrId,
note: "TOO small",
item_id: "test-item",
quantity: 1,
},
],
})
.catch((err) => {
return err.response;
});
expect(response.status).toEqual(400);
expect(response.data.message).toEqual('Cannot apply return reason category')
});
it("creates a return with reasons", async () => {
const api = useApi();
const response = await api
.post("/store/returns", {
order_id: "order_test",
items: [
{
reason_id: rrId_child,
note: "TOO small",
item_id: "test-item",
quantity: 1,
},
],
})
.catch((err) => {
console.log(err.response)
return err.response;
});
expect(response.status).toEqual(200);
expect(response.data.return.items).toEqual([
expect.objectContaining({
reason_id: rrId_child,
note: "TOO small",
}),
]);
});
it("creates a return with discount and non-discountable item", async () => { it("creates a return with discount and non-discountable item", async () => {
const api = useApi(); const api = useApi();
@@ -234,32 +301,5 @@ describe("/store/carts", () => {
expect(response.data.return.refund_amount).toEqual(7000); expect(response.data.return.refund_amount).toEqual(7000);
}); });
it("creates a return with reasons", async () => {
const api = useApi();
const response = await api
.post("/store/returns", {
order_id: "order_test",
items: [
{
reason_id: rrId,
note: "TOO small",
item_id: "test-item",
quantity: 1,
},
],
})
.catch((err) => {
return err.response;
});
expect(response.status).toEqual(200);
expect(response.data.return.items).toEqual([
expect.objectContaining({
reason_id: rrId,
note: "TOO small",
}),
]);
});
}); });
}); });
@@ -39,6 +39,7 @@ export default async (req, res) => {
const schema = Validator.object().keys({ const schema = Validator.object().keys({
value: Validator.string().required(), value: Validator.string().required(),
label: Validator.string().required(), label: Validator.string().required(),
parent_return_reason_id: Validator.string().optional(),
description: Validator.string() description: Validator.string()
.optional() .optional()
.allow(""), .allow(""),
@@ -0,0 +1,41 @@
/**
* @oas [delete] /return-reason/{id}
* operationId: "DeleteReturnReason"
* summary: "Delete a return reason"
* description: "Deletes 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:
* id:
* type: string
* description: The id of the deleted return reason
* object:
* type: string
* description: The type of the object that was deleted.
* deleted:
* type: boolean
*/
export default async (req, res) => {
const { id } = req.params
try {
const returnReasonService = req.scope.resolve("returnReasonService")
await returnReasonService.delete(id)
res.json({
id: id,
object: "return_reason",
deleted: true,
})
} catch (err) {
throw err
}
}
@@ -26,6 +26,11 @@ export default app => {
*/ */
route.post("/:id", middlewares.wrap(require("./update-reason").default)) route.post("/:id", middlewares.wrap(require("./update-reason").default))
/**
* Delete a reason
*/
route.delete("/:id", middlewares.wrap(require("./delete-reason").default))
return app return app
} }
@@ -33,10 +38,14 @@ export const defaultFields = [
"id", "id",
"value", "value",
"label", "label",
"parent_return_reason_id",
"description", "description",
"created_at", "created_at",
"updated_at", "updated_at",
"deleted_at", "deleted_at",
] ]
export const defaultRelations = [] export const defaultRelations = [
"parent_return_reason",
"return_reason_children",
]
@@ -24,7 +24,7 @@ export default async (req, res) => {
try { try {
const returnReasonService = req.scope.resolve("returnReasonService") const returnReasonService = req.scope.resolve("returnReasonService")
const query = {} const query = { parent_return_reason_id: null }
const data = await returnReasonService.list(query, { const data = await returnReasonService.list(query, {
select: defaultFields, select: defaultFields,
relations: defaultRelations, relations: defaultRelations,
@@ -42,6 +42,7 @@ export default async (req, res) => {
const schema = Validator.object().keys({ const schema = Validator.object().keys({
label: Validator.string().optional(), label: Validator.string().optional(),
parent_return_reason_id: Validator.string().optional(),
description: Validator.string() description: Validator.string()
.optional() .optional()
.allow(""), .allow(""),
@@ -23,10 +23,14 @@ export const defaultFields = [
"id", "id",
"value", "value",
"label", "label",
"parent_return_reason_id",
"description", "description",
"created_at", "created_at",
"updated_at", "updated_at",
"deleted_at", "deleted_at",
] ]
export const defaultRelations = [] export const defaultRelations = [
"parent_return_reason",
"return_reason_children",
]
@@ -24,7 +24,7 @@ export default async (req, res) => {
try { try {
const returnReasonService = req.scope.resolve("returnReasonService") const returnReasonService = req.scope.resolve("returnReasonService")
const query = {} const query = { parent_return_reason_id: null}
const data = await returnReasonService.list(query, { const data = await returnReasonService.list(query, {
select: defaultFields, select: defaultFields,
relations: defaultRelations, relations: defaultRelations,
@@ -0,0 +1,20 @@
import {MigrationInterface, QueryRunner} from "typeorm";
export class nestedReturnReasons1631800727788 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "return_reason" ADD "parent_return_reason_id" character varying`
)
await queryRunner.query(`ALTER TABLE "return_reason" ADD CONSTRAINT "FK_2250c5d9e975987ab212f61a657" FOREIGN KEY ("parent_return_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_reason" DROP COLUMN "parent_return_reason_id"`
)
}
}
@@ -7,6 +7,9 @@ import {
CreateDateColumn, CreateDateColumn,
UpdateDateColumn, UpdateDateColumn,
PrimaryColumn, PrimaryColumn,
ManyToOne,
OneToMany,
JoinColumn
} from "typeorm" } from "typeorm"
import { ulid } from "ulid" import { ulid } from "ulid"
import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column" import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column"
@@ -26,6 +29,21 @@ export class ReturnReason {
@Column({ nullable: true }) @Column({ nullable: true })
description: string description: string
@Column({ nullable: true })
parent_return_reason_id: string
@ManyToOne(() => ReturnReason, {cascade: ['soft-remove']}
)
@JoinColumn({ name: "parent_return_reason_id" })
parent_return_reason: ReturnReason
@OneToMany(
() => ReturnReason,
return_reason => return_reason.parent_return_reason,
{ cascade: ["insert", 'soft-remove'] }
)
return_reason_children: ReturnReason[]
@CreateDateColumn({ type: resolveDbType("timestamptz") }) @CreateDateColumn({ type: resolveDbType("timestamptz") })
created_at: Date created_at: Date
+39 -3
View File
@@ -1,6 +1,6 @@
import _ from "lodash"
import { Validator, MedusaError } from "medusa-core-utils" import { Validator, MedusaError } from "medusa-core-utils"
import { BaseService } from "medusa-interfaces" import { BaseService } from "medusa-interfaces"
import { In } from "typeorm"
class ReturnReasonService extends BaseService { class ReturnReasonService extends BaseService {
constructor({ manager, returnReasonRepository }) { constructor({ manager, returnReasonRepository }) {
@@ -32,6 +32,17 @@ class ReturnReasonService extends BaseService {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async manager => {
const rrRepo = manager.getCustomRepository(this.retReasonRepo_) const rrRepo = manager.getCustomRepository(this.retReasonRepo_)
if (data.parent_return_reason_id && data.parent_return_reason_id !== "") {
const parentReason = await this.retrieve(data.parent_return_reason_id)
if (parentReason.parent_return_reason_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Doubly nested return reasons is not supported"
)
}
}
const created = rrRepo.create(data) const created = rrRepo.create(data)
const result = await rrRepo.save(created) const result = await rrRepo.save(created)
@@ -44,14 +55,20 @@ class ReturnReasonService extends BaseService {
const rrRepo = manager.getCustomRepository(this.retReasonRepo_) const rrRepo = manager.getCustomRepository(this.retReasonRepo_)
const reason = await this.retrieve(id) const reason = await this.retrieve(id)
if ("description" in data) { const { description, label, parent_return_reason_id } = data
if (description) {
reason.description = data.description reason.description = data.description
} }
if ("label" in data) { if (label) {
reason.label = data.label reason.label = data.label
} }
if (parent_return_reason_id) {
reason.parent_return_reason_id = parent_return_reason_id
}
await rrRepo.save(reason) await rrRepo.save(reason)
return reason return reason
@@ -92,6 +109,25 @@ class ReturnReasonService extends BaseService {
return item return item
} }
async delete(returnReasonId) {
return this.atomicPhase_(async manager => {
const rrRepo = manager.getCustomRepository(this.retReasonRepo_)
// We include the relation 'return_reason_children' to enable cascading deletes of return reasons if a parent is removed
const reason = await this.retrieve(returnReasonId, {
relations: ["return_reason_children"],
})
if (!reason) {
return Promise.resolve()
}
await rrRepo.softRemove(reason)
return Promise.resolve()
})
}
} }
export default ReturnReasonService export default ReturnReasonService
+12
View File
@@ -374,6 +374,18 @@ class ReturnService extends BaseService {
refund_amount: Math.floor(toRefund), refund_amount: Math.floor(toRefund),
} }
const returnReasons = await this.returnReasonService_.list(
{ id: [...returnLines.map(rl => rl.reason_id)] },
{ relations: ["return_reason_children"] }
)
if (returnReasons.some(rr => rr.return_reason_children?.length > 0)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"Cannot apply return reason category"
)
}
const rItemRepo = manager.getCustomRepository(this.returnItemRepository_) const rItemRepo = manager.getCustomRepository(this.returnItemRepository_)
returnObject.items = returnLines.map(i => returnObject.items = returnLines.map(i =>
rItemRepo.create({ rItemRepo.create({