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:
pKorsholm
2021-09-29 09:22:09 +02:00
committed by GitHub
co-authored by Philip Korsholm
parent 4db860f9ab
commit 42cdfde6d9
15 changed files with 744 additions and 95 deletions
@@ -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(""),
@@ -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))
/**
* 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",
]
@@ -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,
@@ -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(""),
@@ -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",
]
@@ -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,
@@ -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,
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
+39 -3
View File
@@ -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
+12
View File
@@ -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({