diff --git a/integration-tests/api/__tests__/admin/__snapshots__/return-reason.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/return-reason.js.snap new file mode 100644 index 0000000000..79d0282927 --- /dev/null +++ b/integration-tests/api/__tests__/admin/__snapshots__/return-reason.js.snap @@ -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, + "deleted_at": null, + "description": "Use this if the size was too big", + "id": Any, + "label": "Too Big", + "parent_return_reason": null, + "parent_return_reason_id": null, + "return_reason_children": Array [], + "updated_at": Any, + "value": "too_big", +} +`; diff --git a/integration-tests/api/__tests__/admin/return-reason.js b/integration-tests/api/__tests__/admin/return-reason.js index 6255c3bfac..94e77e0745 100644 --- a/integration-tests/api/__tests__/admin/return-reason.js +++ b/integration-tests/api/__tests__/admin/return-reason.js @@ -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 { useApi } = require("../../../helpers/use-api"); -const { initDb, useDb } = require("../../../helpers/use-db"); +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +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", () => { - let medusaProcess; - let dbConnection; + let medusaProcess + let dbConnection beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")); - dbConnection = await initDb({ cwd }); - medusaProcess = await setupServer({ cwd }); - }); + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) afterAll(async () => { - const db = useDb(); - await db.shutdown(); + const db = useDb() + await db.shutdown() - medusaProcess.kill(); - }); + medusaProcess.kill() + }) describe("POST /admin/return-reasons", () => { beforeEach(async () => { try { - await adminSeeder(dbConnection); + await adminSeeder(dbConnection) } catch (err) { - console.log(err); - throw err; + console.log(err) + throw err } - }); + }) afterEach(async () => { - const db = useDb(); - await db.teardown(); - }); + const db = useDb() + await db.teardown() + }) it("creates a return_reason", async () => { - const api = useApi(); + 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, { @@ -56,10 +58,172 @@ describe("/admin/return-reasons", () => { }, }) .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.objectContaining({ @@ -67,17 +231,37 @@ describe("/admin/return-reasons", () => { description: "Use this if the size was 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 () => { - const api = useApi(); + const api = useApi() const payload = { label: "Too Big Typo", description: "Use this if the size was too big", value: "too_big", - }; + } const response = await api .post("/admin/return-reasons", payload, { @@ -86,10 +270,10 @@ describe("/admin/return-reasons", () => { }, }) .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.objectContaining({ @@ -97,7 +281,7 @@ describe("/admin/return-reasons", () => { description: "Use this if the size was too big", value: "too_big", }) - ); + ) const newResponse = await api .post( @@ -113,8 +297,8 @@ describe("/admin/return-reasons", () => { } ) .catch((err) => { - console.log(err); - }); + console.log(err) + }) expect(newResponse.data.return_reason).toEqual( expect.objectContaining({ @@ -122,17 +306,81 @@ describe("/admin/return-reasons", () => { description: "new desc", 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 () => { - const api = useApi(); + const api = useApi() const payload = { label: "Too Big Typo", description: "Use this if the size was too big", value: "too_big", - }; + } await api .post("/admin/return-reasons", payload, { @@ -141,8 +389,8 @@ describe("/admin/return-reasons", () => { }, }) .catch((err) => { - console.log(err); - }); + console.log(err) + }) const response = await api .get("/admin/return-reasons", { @@ -151,15 +399,191 @@ describe("/admin/return-reasons", () => { }, }) .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.objectContaining({ 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") + }) + }) + }) +}) diff --git a/integration-tests/api/__tests__/store/return-reason.js b/integration-tests/api/__tests__/store/return-reason.js index 4698fb86d6..8aa0c4c9d4 100644 --- a/integration-tests/api/__tests__/store/return-reason.js +++ b/integration-tests/api/__tests__/store/return-reason.js @@ -26,16 +26,35 @@ describe("/store/return-reasons", () => { describe("GET /store/return-reasons", () => { let rrId; + let rrId_1; + let rrId_2; beforeEach(async () => { try { const created = dbConnection.manager.create(ReturnReason, { - value: "too_big", - label: "Too Big", + value: "wrong_size", + label: "Wrong size", }); const result = await dbConnection.manager.save(created); 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) { console.log(err); throw err; @@ -59,7 +78,15 @@ describe("/store/return-reasons", () => { expect(response.data.return_reasons).toEqual([ expect.objectContaining({ id: rrId, - value: "too_big", + value: "wrong_size", + return_reason_children:[expect.objectContaining({ + id: rrId_1, + value: "too_big", + }),] + }), + expect.objectContaining({ + id: rrId_2, + value: "too_big_1", }), ]); }); diff --git a/integration-tests/api/__tests__/store/returns.js b/integration-tests/api/__tests__/store/returns.js index f916572a52..3d5151dedb 100644 --- a/integration-tests/api/__tests__/store/returns.js +++ b/integration-tests/api/__tests__/store/returns.js @@ -8,6 +8,7 @@ const { Product, ProductVariant, ShippingOption, + FulfillmentProvider, LineItem, Discount, DiscountRule, @@ -37,6 +38,8 @@ describe("/store/carts", () => { describe("POST /store/returns", () => { let rrId; + let rrId_child; + let rrResult; beforeEach(async () => { const manager = dbConnection.manager; @@ -44,13 +47,17 @@ describe("/store/carts", () => { `ALTER SEQUENCE order_display_id_seq RESTART WITH 111` ); + const defaultProfile = await manager.findOne(ShippingProfile, { + type: "default", + }); + await manager.insert(Region, { id: "region", name: "Test Region", currency_code: "usd", tax_rate: 0, }); - + await manager.insert(Customer, { id: "cus_1234", email: "test@email.com", @@ -98,10 +105,6 @@ describe("/store/carts", () => { await manager.save(ord); - const defaultProfile = await manager.findOne(ShippingProfile, { - type: "default", - }); - await manager.insert(Product, { id: "test-product", title: "test product", @@ -147,12 +150,23 @@ describe("/store/carts", () => { }); const created = dbConnection.manager.create(ReturnReason, { - value: "too_big", - label: "Too Big", + value: "wrong_size", + label: "Wrong Size", }); const result = await dbConnection.manager.save(created); + rrResult = result 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 () => { @@ -181,6 +195,59 @@ describe("/store/carts", () => { 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 () => { const api = useApi(); @@ -234,32 +301,5 @@ describe("/store/carts", () => { 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", - }), - ]); - }); }); }); diff --git a/packages/medusa/src/api/routes/admin/return-reasons/create-reason.js b/packages/medusa/src/api/routes/admin/return-reasons/create-reason.js index 42b9eb8760..d089865088 100644 --- a/packages/medusa/src/api/routes/admin/return-reasons/create-reason.js +++ b/packages/medusa/src/api/routes/admin/return-reasons/create-reason.js @@ -39,6 +39,7 @@ export default async (req, res) => { const schema = Validator.object().keys({ value: Validator.string().required(), label: Validator.string().required(), + parent_return_reason_id: Validator.string().optional(), description: Validator.string() .optional() .allow(""), diff --git a/packages/medusa/src/api/routes/admin/return-reasons/delete-reason.js b/packages/medusa/src/api/routes/admin/return-reasons/delete-reason.js new file mode 100644 index 0000000000..9688d69205 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/return-reasons/delete-reason.js @@ -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 + } +} diff --git a/packages/medusa/src/api/routes/admin/return-reasons/index.js b/packages/medusa/src/api/routes/admin/return-reasons/index.js index 7ad111137f..b535b488f5 100644 --- a/packages/medusa/src/api/routes/admin/return-reasons/index.js +++ b/packages/medusa/src/api/routes/admin/return-reasons/index.js @@ -26,6 +26,11 @@ export default app => { */ route.post("/:id", middlewares.wrap(require("./update-reason").default)) + /** + * Delete a reason + */ + route.delete("/:id", middlewares.wrap(require("./delete-reason").default)) + return app } @@ -33,10 +38,14 @@ export const defaultFields = [ "id", "value", "label", + "parent_return_reason_id", "description", "created_at", "updated_at", "deleted_at", ] -export const defaultRelations = [] +export const defaultRelations = [ + "parent_return_reason", + "return_reason_children", +] diff --git a/packages/medusa/src/api/routes/admin/return-reasons/list-reasons.js b/packages/medusa/src/api/routes/admin/return-reasons/list-reasons.js index 367a2fb651..2c1f8664ed 100644 --- a/packages/medusa/src/api/routes/admin/return-reasons/list-reasons.js +++ b/packages/medusa/src/api/routes/admin/return-reasons/list-reasons.js @@ -24,7 +24,7 @@ export default async (req, res) => { try { const returnReasonService = req.scope.resolve("returnReasonService") - const query = {} + const query = { parent_return_reason_id: null } const data = await returnReasonService.list(query, { select: defaultFields, relations: defaultRelations, diff --git a/packages/medusa/src/api/routes/admin/return-reasons/update-reason.js b/packages/medusa/src/api/routes/admin/return-reasons/update-reason.js index 82d933c406..398c661a9d 100644 --- a/packages/medusa/src/api/routes/admin/return-reasons/update-reason.js +++ b/packages/medusa/src/api/routes/admin/return-reasons/update-reason.js @@ -42,6 +42,7 @@ export default async (req, res) => { const schema = Validator.object().keys({ label: Validator.string().optional(), + parent_return_reason_id: Validator.string().optional(), description: Validator.string() .optional() .allow(""), diff --git a/packages/medusa/src/api/routes/store/return-reasons/index.js b/packages/medusa/src/api/routes/store/return-reasons/index.js index 4590bb5c6a..071d27e8cc 100644 --- a/packages/medusa/src/api/routes/store/return-reasons/index.js +++ b/packages/medusa/src/api/routes/store/return-reasons/index.js @@ -23,10 +23,14 @@ export const defaultFields = [ "id", "value", "label", + "parent_return_reason_id", "description", "created_at", "updated_at", "deleted_at", ] -export const defaultRelations = [] +export const defaultRelations = [ + "parent_return_reason", + "return_reason_children", +] diff --git a/packages/medusa/src/api/routes/store/return-reasons/list-reasons.js b/packages/medusa/src/api/routes/store/return-reasons/list-reasons.js index 367a2fb651..563e5a893b 100644 --- a/packages/medusa/src/api/routes/store/return-reasons/list-reasons.js +++ b/packages/medusa/src/api/routes/store/return-reasons/list-reasons.js @@ -24,7 +24,7 @@ export default async (req, res) => { try { const returnReasonService = req.scope.resolve("returnReasonService") - const query = {} + const query = { parent_return_reason_id: null} const data = await returnReasonService.list(query, { select: defaultFields, relations: defaultRelations, diff --git a/packages/medusa/src/migrations/1631800727788-nested_return_reasons.ts b/packages/medusa/src/migrations/1631800727788-nested_return_reasons.ts new file mode 100644 index 0000000000..c50c957e09 --- /dev/null +++ b/packages/medusa/src/migrations/1631800727788-nested_return_reasons.ts @@ -0,0 +1,20 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; + +export class nestedReturnReasons1631800727788 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query( + `ALTER TABLE "return_reason" DROP COLUMN "parent_return_reason_id"` + ) + + } + +} diff --git a/packages/medusa/src/models/return-reason.ts b/packages/medusa/src/models/return-reason.ts index 6846e0da27..9cbfb47a52 100644 --- a/packages/medusa/src/models/return-reason.ts +++ b/packages/medusa/src/models/return-reason.ts @@ -7,6 +7,9 @@ import { CreateDateColumn, UpdateDateColumn, PrimaryColumn, + ManyToOne, + OneToMany, + JoinColumn } from "typeorm" import { ulid } from "ulid" import { resolveDbType, DbAwareColumn } from "../utils/db-aware-column" @@ -26,6 +29,21 @@ export class ReturnReason { @Column({ nullable: true }) 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") }) created_at: Date diff --git a/packages/medusa/src/services/return-reason.js b/packages/medusa/src/services/return-reason.js index 34784c78e9..0a25556a79 100644 --- a/packages/medusa/src/services/return-reason.js +++ b/packages/medusa/src/services/return-reason.js @@ -1,6 +1,6 @@ -import _ from "lodash" import { Validator, MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" +import { In } from "typeorm" class ReturnReasonService extends BaseService { constructor({ manager, returnReasonRepository }) { @@ -32,6 +32,17 @@ class ReturnReasonService extends BaseService { return this.atomicPhase_(async manager => { 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 result = await rrRepo.save(created) @@ -44,14 +55,20 @@ class ReturnReasonService extends BaseService { const rrRepo = manager.getCustomRepository(this.retReasonRepo_) const reason = await this.retrieve(id) - if ("description" in data) { + const { description, label, parent_return_reason_id } = data + + if (description) { reason.description = data.description } - if ("label" in data) { + if (label) { reason.label = data.label } + if (parent_return_reason_id) { + reason.parent_return_reason_id = parent_return_reason_id + } + await rrRepo.save(reason) return reason @@ -92,6 +109,25 @@ class ReturnReasonService extends BaseService { 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 diff --git a/packages/medusa/src/services/return.js b/packages/medusa/src/services/return.js index 23bbca9e08..0092e0e5c7 100644 --- a/packages/medusa/src/services/return.js +++ b/packages/medusa/src/services/return.js @@ -374,6 +374,18 @@ class ReturnService extends BaseService { 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_) returnObject.items = returnLines.map(i => rItemRepo.create({