Merge remote-tracking branch 'origin/develop' into release/next

This commit is contained in:
Sebastian Rindom committed 2021-03-16 13:58:18 +01:00
commit b18beb5a84
51 files changed
+7273 -42

No files matched your search

@@ -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"
@@ -64,6 +65,7 @@ export default (app, container, config) => {
variantRoutes(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",
@@ -195,6 +195,8 @@ export const defaultRelations = [
"fulfillments.tracking_links",
"fulfillments.items",
"returns",
"returns.items",
"returns.items.reason",
"gift_cards",
"gift_card_transactions",
"claims",
@@ -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()
@@ -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
}
}
@@ -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
}
@@ -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
}
}
@@ -50,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()
@@ -151,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,
+1
View File
@@ -41,3 +41,4 @@ export { StagedJob } from "./models/staged-job"
export { Store } from "./models/store"
export { Swap } from "./models/swap"
export { User } from "./models/user"
export { ReturnReason } from "./models/return-reason"
@@ -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"`)
}
}
+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 { ReturnReason } from "../models/return-reason"
@EntityRepository(ReturnReason)
export class ReturnReasonRepository extends Repository<ReturnReason> {}
@@ -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
+22 -5
View File
@@ -14,6 +14,7 @@ class ReturnService extends BaseService {
returnRepository,
returnItemRepository,
shippingOptionService,
returnReasonService,
fulfillmentProviderService,
}) {
super()
@@ -38,6 +39,8 @@ class ReturnService extends BaseService {
/** @private @const {FulfillmentProviderService} */
this.fulfillmentProviderService_ = fulfillmentProviderService
this.returnReasonService_ = returnReasonService
}
withTransaction(transactionManager) {
@@ -53,6 +56,7 @@ class ReturnService extends BaseService {
returnItemRepository: this.returnItemRepository_,
shippingOptionService: this.shippingOptionService_,
fulfillmentProviderService: this.fulfillmentProviderService_,
returnReasonService: this.returnReasonService_,
})
cloned.transactionManager_ = transactionManager
@@ -72,9 +76,9 @@ class ReturnService extends BaseService {
*/
async getFulfillmentItems_(order, items, transformer) {
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 = order.items.find(i => i.id === data.item_id)
return transformer(item, data.quantity, data)
})
)
@@ -127,10 +131,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 +151,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
}
/**
@@ -288,6 +303,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,
})
)