feat(medusa,brightpearl,segment,webshipper): claims (#163)
* chore: create tests * chore: models * fix: passing initial tests * test: adds integration test * test: clean up integration implementation * fix: claims * fix: brightpearl + webshipper * tests: passing * fix: update claim items * fix: adds gitignore * fix: pr comments * fix: single migration * fix(medusa-plugin-segment): adds item claimed event to segment
This commit is contained in:
@@ -5,7 +5,7 @@ export default () => {
|
||||
const logger = req.scope.resolve("logger")
|
||||
logger.error(err.message)
|
||||
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
|
||||
let statusCode = 500
|
||||
switch (err.name) {
|
||||
|
||||
@@ -13,6 +13,15 @@ const defaultRelations = [
|
||||
"returns",
|
||||
"gift_cards",
|
||||
"gift_card_transactions",
|
||||
"claims",
|
||||
"claims.return_order",
|
||||
"claims.shipping_methods",
|
||||
"claims.shipping_address",
|
||||
"claims.additional_items",
|
||||
"claims.fulfillments",
|
||||
"claims.claim_items",
|
||||
"claims.claim_items.images",
|
||||
"claims.claim_items.tags",
|
||||
"swaps",
|
||||
"swaps.return_order",
|
||||
"swaps.payment",
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultRelations, defaultFields } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
type: Validator.string()
|
||||
.valid("replace", "refund")
|
||||
.required(),
|
||||
claim_items: Validator.array()
|
||||
.items({
|
||||
item_id: Validator.string().required(),
|
||||
quantity: Validator.number().required(),
|
||||
note: Validator.string().optional(),
|
||||
reason: Validator.string().valid(
|
||||
"missing_item",
|
||||
"wrong_item",
|
||||
"production_failure",
|
||||
"other"
|
||||
),
|
||||
tags: Validator.array().items(Validator.string()),
|
||||
images: Validator.array().items(Validator.string()),
|
||||
})
|
||||
.required(),
|
||||
return_shipping: Validator.object()
|
||||
.keys({
|
||||
option_id: Validator.string().optional(),
|
||||
price: Validator.number()
|
||||
.integer()
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
additional_items: Validator.array()
|
||||
.items({
|
||||
variant_id: Validator.string().required(),
|
||||
quantity: Validator.number().required(),
|
||||
})
|
||||
.optional(),
|
||||
shipping_methods: Validator.array()
|
||||
.items({
|
||||
id: Validator.string().optional(),
|
||||
option_id: Validator.string().optional(),
|
||||
price: Validator.number()
|
||||
.integer()
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
refund_amount: Validator.number()
|
||||
.integer()
|
||||
.optional(),
|
||||
metadata: Validator.object().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
const idempotencyKeyService = req.scope.resolve("idempotencyKeyService")
|
||||
|
||||
const headerKey = req.get("Idempotency-Key") || ""
|
||||
|
||||
let idempotencyKey
|
||||
try {
|
||||
idempotencyKey = await idempotencyKeyService.initializeRequest(
|
||||
headerKey,
|
||||
req.method,
|
||||
req.params,
|
||||
req.path
|
||||
)
|
||||
} catch (error) {
|
||||
res.status(409).send("Failed to create idempotency key")
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
|
||||
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const claimService = req.scope.resolve("claimService")
|
||||
const returnService = req.scope.resolve("returnService")
|
||||
|
||||
let inProgress = true
|
||||
let err = false
|
||||
|
||||
while (inProgress) {
|
||||
switch (idempotencyKey.recovery_point) {
|
||||
case "started": {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async manager => {
|
||||
const order = await orderService
|
||||
.withTransaction(manager)
|
||||
.retrieve(id, {
|
||||
relations: ["items", "discounts"],
|
||||
})
|
||||
|
||||
await claimService.withTransaction(manager).create({
|
||||
idempotency_key: idempotencyKey.idempotency_key,
|
||||
order,
|
||||
type: value.type,
|
||||
claim_items: value.claim_items,
|
||||
return_shipping: value.return_shipping,
|
||||
additional_items: value.additional_items,
|
||||
shipping_methods: value.shipping_methods,
|
||||
metadata: value.metadata,
|
||||
})
|
||||
|
||||
return {
|
||||
recovery_point: "claim_created",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "claim_created": {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async manager => {
|
||||
let claim = await claimService.withTransaction(manager).list({
|
||||
idempotency_key: idempotencyKey.idempotency_key,
|
||||
})
|
||||
|
||||
if (!claim.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claim not found`
|
||||
)
|
||||
}
|
||||
|
||||
claim = claim[0]
|
||||
|
||||
if (claim.type === "refund") {
|
||||
await claimService
|
||||
.withTransaction(manager)
|
||||
.processRefund(claim.id)
|
||||
}
|
||||
|
||||
return {
|
||||
recovery_point: "refund_handled",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "refund_handled": {
|
||||
const { key, error } = await idempotencyKeyService.workStage(
|
||||
idempotencyKey.idempotency_key,
|
||||
async manager => {
|
||||
let order = await orderService
|
||||
.withTransaction(manager)
|
||||
.retrieve(id, {
|
||||
relations: ["items", "discounts"],
|
||||
})
|
||||
|
||||
let claim = await claimService.withTransaction(manager).list(
|
||||
{
|
||||
idempotency_key: idempotencyKey.idempotency_key,
|
||||
},
|
||||
{
|
||||
relations: ["return_order"],
|
||||
}
|
||||
)
|
||||
|
||||
if (!claim.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claim not found`
|
||||
)
|
||||
}
|
||||
|
||||
claim = claim[0]
|
||||
|
||||
if (claim.return_order) {
|
||||
await returnService
|
||||
.withTransaction(manager)
|
||||
.fulfill(claim.return_order.id)
|
||||
}
|
||||
|
||||
order = await orderService.withTransaction(manager).retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { order },
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (error) {
|
||||
inProgress = false
|
||||
err = error
|
||||
} else {
|
||||
idempotencyKey = key
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "finished": {
|
||||
inProgress = false
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
idempotencyKey = await idempotencyKeyService.update(
|
||||
idempotencyKey.idempotency_key,
|
||||
{
|
||||
recovery_point: "finished",
|
||||
response_code: 500,
|
||||
response_body: { message: "Unknown recovery point" },
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultRelations, defaultFields } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, claim_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
metadata: Validator.object().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const claimService = req.scope.resolve("claimService")
|
||||
const entityManager = req.scope.resolve("manager")
|
||||
|
||||
await entityManager.transaction(async manager => {
|
||||
await claimService
|
||||
.withTransaction(manager)
|
||||
.createFulfillment(claim_id, value.metadata)
|
||||
})
|
||||
|
||||
const order = await orderService.retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.status(200).json({ order })
|
||||
} catch (err) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,27 @@ export default app => {
|
||||
middlewares.wrap(require("./process-swap-payment").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates a claim
|
||||
*/
|
||||
route.post("/:id/claims", middlewares.wrap(require("./create-claim").default))
|
||||
|
||||
/**
|
||||
* Updates a claim
|
||||
*/
|
||||
route.post(
|
||||
"/:id/claims/:claim_id",
|
||||
middlewares.wrap(require("./update-claim").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Creates claim fulfillment
|
||||
*/
|
||||
route.post(
|
||||
"/:id/claims/:claim_id/fulfillments",
|
||||
middlewares.wrap(require("./fulfill-claim").default)
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete metadata key / value pair.
|
||||
*/
|
||||
@@ -162,6 +183,15 @@ export const defaultRelations = [
|
||||
"returns",
|
||||
"gift_cards",
|
||||
"gift_card_transactions",
|
||||
"claims",
|
||||
"claims.return_order",
|
||||
"claims.shipping_methods",
|
||||
"claims.shipping_address",
|
||||
"claims.additional_items",
|
||||
"claims.fulfillments",
|
||||
"claims.claim_items",
|
||||
"claims.claim_items.images",
|
||||
"claims.claim_items.tags",
|
||||
"swaps",
|
||||
"swaps.return_order",
|
||||
"swaps.payment",
|
||||
@@ -234,6 +264,7 @@ export const allowedRelations = [
|
||||
"payments",
|
||||
"fulfillments",
|
||||
"returns",
|
||||
"claims",
|
||||
"swaps",
|
||||
"swaps.return_order",
|
||||
"swaps.additional_items",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { MedusaError, Validator } from "medusa-core-utils"
|
||||
import { defaultRelations, defaultFields } from "./"
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id, claim_id } = req.params
|
||||
|
||||
const schema = Validator.object().keys({
|
||||
claim_items: Validator.array()
|
||||
.items({
|
||||
id: Validator.string().required(),
|
||||
note: Validator.string().allow(null, ""),
|
||||
reason: Validator.string().allow(null, ""),
|
||||
images: Validator.array().items({
|
||||
id: Validator.string().optional(),
|
||||
url: Validator.string().optional(),
|
||||
}),
|
||||
tags: Validator.array().items({
|
||||
id: Validator.string().optional(),
|
||||
value: Validator.string().optional(),
|
||||
}),
|
||||
metadata: Validator.object().optional(),
|
||||
})
|
||||
.optional(),
|
||||
shipping_methods: Validator.array()
|
||||
.items({
|
||||
id: Validator.string().optional(),
|
||||
option_id: Validator.string().optional(),
|
||||
price: Validator.number()
|
||||
.integer()
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
metadata: Validator.object().optional(),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
if (error) {
|
||||
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
|
||||
}
|
||||
|
||||
try {
|
||||
const orderService = req.scope.resolve("orderService")
|
||||
const claimService = req.scope.resolve("claimService")
|
||||
|
||||
await claimService.update(claim_id, value)
|
||||
|
||||
const data = await orderService.retrieve(id, {
|
||||
select: defaultFields,
|
||||
relations: defaultRelations,
|
||||
})
|
||||
|
||||
res.json({ order: data })
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import "core-js/stable"
|
||||
import "regenerator-runtime/runtime"
|
||||
import "reflect-metadata"
|
||||
import express from "express"
|
||||
import loaders from "./loaders"
|
||||
import Logger from "./loaders/logger"
|
||||
|
||||
const PORT = process.env.PORT || 80
|
||||
|
||||
const startServer = async () => {
|
||||
const app = express()
|
||||
|
||||
await loaders({ expressApp: app })
|
||||
|
||||
app.listen(PORT, err => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
return
|
||||
}
|
||||
Logger.info(`Server is ready on port: ${PORT}!`)
|
||||
})
|
||||
}
|
||||
|
||||
startServer()
|
||||
@@ -0,0 +1,39 @@
|
||||
export { Address } from "./models/address"
|
||||
export { Cart } from "./models/cart"
|
||||
export { ClaimImage } from "./models/claim-image"
|
||||
export { ClaimItem } from "./models/claim-item"
|
||||
export { ClaimTag } from "./models/claim-tag"
|
||||
export { Country } from "./models/country"
|
||||
export { Currency } from "./models/currency"
|
||||
export { Customer } from "./models/customer"
|
||||
export { DiscountRule } from "./models/discount-rule"
|
||||
export { Discount } from "./models/discount"
|
||||
export { FulfillmentItem } from "./models/fulfillment-item"
|
||||
export { FulfillmentProvider } from "./models/fulfillment-provider"
|
||||
export { GiftCardTransaction } from "./models/gift-card-transaction"
|
||||
export { GiftCard } from "./models/gift-card"
|
||||
export { IdempotencyKey } from "./models/idempotency-key"
|
||||
export { Image } from "./models/image"
|
||||
export { LineItem } from "./models/line-item"
|
||||
export { MoneyAmount } from "./models/money-amount"
|
||||
export { Oauth } from "./models/oauth"
|
||||
export { Order } from "./models/order"
|
||||
export { PaymentProvider } from "./models/payment-provider"
|
||||
export { PaymentSession } from "./models/payment-session"
|
||||
export { Payment } from "./models/payment"
|
||||
export { ProductOptionValue } from "./models/product-option-value"
|
||||
export { ProductOption } from "./models/product-option"
|
||||
export { ProductVariant } from "./models/product-variant"
|
||||
export { Product } from "./models/product"
|
||||
export { Refund } from "./models/refund"
|
||||
export { Region } from "./models/region"
|
||||
export { ReturnItem } from "./models/return-item"
|
||||
export { Return } from "./models/return"
|
||||
export { ShippingMethod } from "./models/shipping-method"
|
||||
export { ShippingOptionRequirement } from "./models/shipping-option-requirement"
|
||||
export { ShippingOption } from "./models/shipping-option"
|
||||
export { ShippingProfile } from "./models/shipping-profile"
|
||||
export { StagedJob } from "./models/staged-job"
|
||||
export { Store } from "./models/store"
|
||||
export { Swap } from "./models/swap"
|
||||
export { User } from "./models/user"
|
||||
@@ -7,25 +7,33 @@ import { Lifetime, asClass, asValue } from "awilix"
|
||||
/**
|
||||
* Registers all models in the model directory
|
||||
*/
|
||||
export default ({ container }) => {
|
||||
export default ({ container }, config = { register: true }) => {
|
||||
let corePath = "../models/*.js"
|
||||
const coreFull = path.join(__dirname, corePath)
|
||||
|
||||
const toReturn = []
|
||||
|
||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||
core.forEach(fn => {
|
||||
const loaded = require(fn)
|
||||
|
||||
Object.entries(loaded).map(([key, val]) => {
|
||||
if (typeof val === "function" || val instanceof EntitySchema) {
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asClass(val),
|
||||
})
|
||||
if (config.register) {
|
||||
const name = formatRegistrationName(fn)
|
||||
container.register({
|
||||
[name]: asClass(val),
|
||||
})
|
||||
|
||||
container.registerAdd("db_entities", asValue(val))
|
||||
container.registerAdd("db_entities", asValue(val))
|
||||
}
|
||||
|
||||
toReturn.push(val)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return toReturn
|
||||
}
|
||||
|
||||
function formatRegistrationName(fn) {
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
|
||||
export class claims1612284947120 implements MigrationInterface {
|
||||
name = "claims1612284947120"
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" DROP CONSTRAINT "CHK_3c00b878c1426d119cd70aa065"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "claim_image" ("id" character varying NOT NULL, "claim_item_id" character varying NOT NULL, "url" character varying NOT NULL, "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_7c49e44bfe8840ca7d917890101" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "claim_tag" ("id" character varying NOT NULL, "value" character varying NOT NULL, "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_7761180541142a5926501018d34" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_ec10c54769877840c132260e4a" ON "claim_tag" ("value") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "claim_item_reason_enum" AS ENUM('missing_item', 'wrong_item', 'production_failure', 'other')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "claim_item" ("id" character varying NOT NULL, "claim_order_id" character varying NOT NULL, "item_id" character varying NOT NULL, "variant_id" character varying NOT NULL, "reason" "claim_item_reason_enum" NOT NULL, "note" character varying, "quantity" integer NOT NULL, "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_5679662039bc4c7c6bc7fa1be2d" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_900a9c3834257304396b2b0fe7" ON "claim_item" ("claim_order_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_6e0cad0daef76bb642675910b9" ON "claim_item" ("item_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_64980511ca32c8e92b417644af" ON "claim_item" ("variant_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "claim_order_payment_status_enum" AS ENUM('na', 'not_refunded', 'refunded')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "claim_order_fulfillment_status_enum" AS ENUM('not_fulfilled', 'partially_fulfilled', 'fulfilled', 'partially_shipped', 'shipped', 'partially_returned', 'returned', 'canceled', 'requires_action')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "claim_order_type_enum" AS ENUM('refund', 'replace')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "claim_order" ("id" character varying NOT NULL, "payment_status" "claim_order_payment_status_enum" NOT NULL DEFAULT 'na', "fulfillment_status" "claim_order_fulfillment_status_enum" NOT NULL DEFAULT 'not_fulfilled', "type" "claim_order_type_enum" NOT NULL, "order_id" character varying NOT NULL, "shipping_address_id" character varying, "refund_amount" integer, "canceled_at" TIMESTAMP WITH TIME ZONE, "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, "idempotency_key" character varying, CONSTRAINT "PK_8981f5595a4424021466aa4c7a4" PRIMARY KEY ("id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "claim_item_tags" ("item_id" character varying NOT NULL, "tag_id" character varying NOT NULL, CONSTRAINT "PK_54ab8ce0f7e99167068188fbd81" PRIMARY KEY ("item_id", "tag_id"))`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_c2c0f3edf39515bd15432afe6e" ON "claim_item_tags" ("item_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_dc9bbf9fcb9ba458d25d512811" ON "claim_item_tags" ("tag_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" ADD "claim_order_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "return" ADD "claim_order_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "return" ADD CONSTRAINT "UQ_71773d56eb2bacb922bc3283398" UNIQUE ("claim_order_id")`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "fulfillment" ADD "claim_order_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" ADD "claim_order_id" character varying`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "public"."refund_reason_enum" RENAME TO "refund_reason_enum_old"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "refund_reason_enum" AS ENUM('discount', 'return', 'swap', 'claim', 'other')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund" ALTER COLUMN "reason" TYPE "refund_reason_enum" USING "reason"::"text"::"refund_reason_enum"`
|
||||
)
|
||||
await queryRunner.query(`DROP TYPE "refund_reason_enum_old"`)
|
||||
await queryRunner.query(`COMMENT ON COLUMN "refund"."reason" IS NULL`)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_d783a66d1c91c0858752c933e6" ON "shipping_method" ("claim_order_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_118e3c48f09a7728f41023c94e" ON "line_item" ("claim_order_id") `
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" ADD CONSTRAINT "CHK_a7020b08665bbd64d84a6641cf" CHECK ("claim_order_id" IS NOT NULL OR "order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL)`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_image" ADD CONSTRAINT "FK_21cbfedd83d736d86f4c6f4ce56" FOREIGN KEY ("claim_item_id") REFERENCES "claim_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item" ADD CONSTRAINT "FK_900a9c3834257304396b2b0fe7c" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item" ADD CONSTRAINT "FK_6e0cad0daef76bb642675910b9d" FOREIGN KEY ("item_id") REFERENCES "line_item"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item" ADD CONSTRAINT "FK_64980511ca32c8e92b417644afa" FOREIGN KEY ("variant_id") REFERENCES "product_variant"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" ADD CONSTRAINT "FK_d783a66d1c91c0858752c933e68" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "return" ADD CONSTRAINT "FK_71773d56eb2bacb922bc3283398" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_order" ADD CONSTRAINT "FK_f49e3974465d3c3a33d449d3f31" FOREIGN KEY ("order_id") REFERENCES "order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_order" ADD CONSTRAINT "FK_017d58bf8260c6e1a2588d258e2" FOREIGN KEY ("shipping_address_id") REFERENCES "address"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "fulfillment" ADD CONSTRAINT "FK_d73e55964e0ff2db8f03807d52e" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" ADD CONSTRAINT "FK_118e3c48f09a7728f41023c94ef" FOREIGN KEY ("claim_order_id") REFERENCES "claim_order"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item_tags" ADD CONSTRAINT "FK_c2c0f3edf39515bd15432afe6e5" FOREIGN KEY ("item_id") REFERENCES "claim_item"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item_tags" ADD CONSTRAINT "FK_dc9bbf9fcb9ba458d25d512811b" FOREIGN KEY ("tag_id") REFERENCES "claim_tag"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
|
||||
)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item_tags" DROP CONSTRAINT "FK_dc9bbf9fcb9ba458d25d512811b"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item_tags" DROP CONSTRAINT "FK_c2c0f3edf39515bd15432afe6e5"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP CONSTRAINT "FK_118e3c48f09a7728f41023c94ef"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "fulfillment" DROP CONSTRAINT "FK_d73e55964e0ff2db8f03807d52e"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_order" DROP CONSTRAINT "FK_017d58bf8260c6e1a2588d258e2"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_order" DROP CONSTRAINT "FK_f49e3974465d3c3a33d449d3f31"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "return" DROP CONSTRAINT "FK_71773d56eb2bacb922bc3283398"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" DROP CONSTRAINT "FK_d783a66d1c91c0858752c933e68"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item" DROP CONSTRAINT "FK_64980511ca32c8e92b417644afa"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item" DROP CONSTRAINT "FK_6e0cad0daef76bb642675910b9d"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_item" DROP CONSTRAINT "FK_900a9c3834257304396b2b0fe7c"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "claim_image" DROP CONSTRAINT "FK_21cbfedd83d736d86f4c6f4ce56"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" DROP CONSTRAINT "CHK_a7020b08665bbd64d84a6641cf"`
|
||||
)
|
||||
await queryRunner.query(`DROP INDEX "IDX_118e3c48f09a7728f41023c94e"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_d783a66d1c91c0858752c933e6"`)
|
||||
await queryRunner.query(`COMMENT ON COLUMN "refund"."reason" IS NULL`)
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "refund_reason_enum_old" AS ENUM('discount', 'return', 'swap', 'other')`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "refund" ALTER COLUMN "reason" TYPE "refund_reason_enum_old" USING "reason"::"text"::"refund_reason_enum_old"`
|
||||
)
|
||||
await queryRunner.query(`DROP TYPE "refund_reason_enum"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TYPE "refund_reason_enum_old" RENAME TO "refund_reason_enum"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "line_item" DROP COLUMN "claim_order_id"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "fulfillment" DROP COLUMN "claim_order_id"`
|
||||
)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "return" DROP CONSTRAINT "UQ_71773d56eb2bacb922bc3283398"`
|
||||
)
|
||||
await queryRunner.query(`ALTER TABLE "return" DROP COLUMN "claim_order_id"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" DROP COLUMN "claim_order_id"`
|
||||
)
|
||||
await queryRunner.query(`DROP INDEX "IDX_dc9bbf9fcb9ba458d25d512811"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_c2c0f3edf39515bd15432afe6e"`)
|
||||
await queryRunner.query(`DROP TABLE "claim_item_tags"`)
|
||||
await queryRunner.query(`DROP TABLE "claim_order"`)
|
||||
await queryRunner.query(`DROP TYPE "claim_order_type_enum"`)
|
||||
await queryRunner.query(`DROP TYPE "claim_order_fulfillment_status_enum"`)
|
||||
await queryRunner.query(`DROP TYPE "claim_order_payment_status_enum"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_64980511ca32c8e92b417644af"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_6e0cad0daef76bb642675910b9"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_900a9c3834257304396b2b0fe7"`)
|
||||
await queryRunner.query(`DROP TABLE "claim_item"`)
|
||||
await queryRunner.query(`DROP TYPE "claim_item_reason_enum"`)
|
||||
await queryRunner.query(`DROP INDEX "IDX_ec10c54769877840c132260e4a"`)
|
||||
await queryRunner.query(`DROP TABLE "claim_tag"`)
|
||||
await queryRunner.query(`DROP TABLE "claim_image"`)
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "shipping_method" ADD CONSTRAINT "CHK_3c00b878c1426d119cd70aa065" CHECK (((order_id IS NOT NULL) OR (cart_id IS NOT NULL) OR (swap_id IS NOT NULL) OR (return_id IS NOT NULL)))`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { ClaimItem } from "./claim-item"
|
||||
|
||||
@Entity()
|
||||
export class ClaimImage {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column()
|
||||
claim_item_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => ClaimItem,
|
||||
ci => ci.images
|
||||
)
|
||||
@JoinColumn({ name: "claim_item_id" })
|
||||
claim_item: ClaimItem
|
||||
|
||||
@Column()
|
||||
url: 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 = `cimg_${id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
Index,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { LineItem } from "./line-item"
|
||||
import { ClaimImage } from "./claim-image"
|
||||
import { ClaimTag } from "./claim-tag"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
|
||||
export enum ClaimReason {
|
||||
MISSING_ITEM = "missing_item",
|
||||
WRONG_ITEM = "wrong_item",
|
||||
PRODUCTION_FAILURE = "production_failure",
|
||||
OTHER = "other",
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class ClaimItem {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@OneToMany(
|
||||
() => ClaimImage,
|
||||
ci => ci.claim_item,
|
||||
{ cascade: ["insert", "remove"] }
|
||||
)
|
||||
images: ClaimImage[]
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
claim_order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => ClaimOrder,
|
||||
co => co.claim_items
|
||||
)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
claim_order: ClaimOrder
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
item_id: string
|
||||
|
||||
@ManyToOne(() => LineItem)
|
||||
@JoinColumn({ name: "item_id" })
|
||||
item: LineItem
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
variant_id: string
|
||||
|
||||
@ManyToOne(() => ProductVariant)
|
||||
@JoinColumn({ name: "variant_id" })
|
||||
variant: ProductVariant
|
||||
|
||||
@Column({ type: "enum", enum: ClaimReason })
|
||||
reason: ClaimReason
|
||||
|
||||
@Column({ nullable: true })
|
||||
note: string
|
||||
|
||||
@Column({ type: "int" })
|
||||
quantity: number
|
||||
|
||||
@ManyToMany(() => ClaimTag, { cascade: ["insert"] })
|
||||
@JoinTable({
|
||||
name: "claim_item_tags",
|
||||
joinColumn: {
|
||||
name: "item_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "tag_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
tags: ClaimTag[]
|
||||
|
||||
@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 = `citm_${id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
Index,
|
||||
OneToOne,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { Fulfillment } from "./fulfillment"
|
||||
import { LineItem } from "./line-item"
|
||||
import { ClaimItem } from "./claim-item"
|
||||
import { Order } from "./order"
|
||||
import { Return } from "./return"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
import { Address } from "./address"
|
||||
|
||||
export enum ClaimType {
|
||||
REFUND = "refund",
|
||||
REPLACE = "replace",
|
||||
}
|
||||
|
||||
export enum ClaimPaymentStatus {
|
||||
NA = "na",
|
||||
NOT_REFUNDED = "not_refunded",
|
||||
REFUNDED = "refunded",
|
||||
}
|
||||
|
||||
export enum ClaimFulfillmentStatus {
|
||||
NOT_FULFILLED = "not_fulfilled",
|
||||
PARTIALLY_FULFILLED = "partially_fulfilled",
|
||||
FULFILLED = "fulfilled",
|
||||
PARTIALLY_SHIPPED = "partially_shipped",
|
||||
SHIPPED = "shipped",
|
||||
PARTIALLY_RETURNED = "partially_returned",
|
||||
RETURNED = "returned",
|
||||
CANCELED = "canceled",
|
||||
REQUIRES_ACTION = "requires_action",
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class ClaimOrder {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({
|
||||
type: "enum",
|
||||
enum: ClaimPaymentStatus,
|
||||
default: ClaimPaymentStatus.NA,
|
||||
})
|
||||
payment_status: ClaimPaymentStatus
|
||||
|
||||
@Column({
|
||||
type: "enum",
|
||||
enum: ClaimFulfillmentStatus,
|
||||
default: ClaimFulfillmentStatus.NOT_FULFILLED,
|
||||
})
|
||||
fulfillment_status: ClaimFulfillmentStatus
|
||||
|
||||
@OneToMany(
|
||||
() => ClaimItem,
|
||||
ci => ci.claim_order
|
||||
)
|
||||
claim_items: ClaimItem[]
|
||||
|
||||
@OneToMany(
|
||||
() => LineItem,
|
||||
li => li.claim_order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
additional_items: LineItem[]
|
||||
|
||||
@Column({ type: "enum", enum: ClaimType })
|
||||
type: ClaimType
|
||||
|
||||
@Column()
|
||||
order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => Order,
|
||||
o => o.claims
|
||||
)
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@OneToOne(
|
||||
() => Return,
|
||||
ret => ret.claim_order
|
||||
)
|
||||
return_order: Return
|
||||
|
||||
@Column({ nullable: true })
|
||||
shipping_address_id: string
|
||||
|
||||
@ManyToOne(() => Address, { cascade: ["insert"] })
|
||||
@JoinColumn({ name: "shipping_address_id" })
|
||||
shipping_address: Address
|
||||
|
||||
@OneToMany(
|
||||
() => ShippingMethod,
|
||||
method => method.claim_order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
shipping_methods: ShippingMethod[]
|
||||
|
||||
@OneToMany(
|
||||
() => Fulfillment,
|
||||
fulfillment => fulfillment.claim_order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
fulfillments: Fulfillment[]
|
||||
|
||||
@Column({ type: "int", nullable: true })
|
||||
refund_amount: number
|
||||
|
||||
@Column({ type: "timestamptz", nullable: true })
|
||||
canceled_at: Date
|
||||
|
||||
@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
|
||||
|
||||
@Column({ nullable: true })
|
||||
idempotency_key: string
|
||||
|
||||
@BeforeInsert()
|
||||
private beforeInsert() {
|
||||
if (this.id) return
|
||||
const id = ulid()
|
||||
this.id = `claim_${id}`
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Entity,
|
||||
BeforeInsert,
|
||||
DeleteDateColumn,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Column,
|
||||
PrimaryColumn,
|
||||
Index,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
JoinColumn,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
@Entity()
|
||||
export class ClaimTag {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
value: 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 = `ctag_${id}`
|
||||
}
|
||||
}
|
||||
@@ -20,12 +20,23 @@ import { Order } from "./order"
|
||||
import { FulfillmentProvider } from "./fulfillment-provider"
|
||||
import { FulfillmentItem } from "./fulfillment-item"
|
||||
import { Swap } from "./swap"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
|
||||
@Entity()
|
||||
export class Fulfillment {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ nullable: true })
|
||||
claim_order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => ClaimOrder,
|
||||
co => co.fulfillments
|
||||
)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
claim_order: ClaimOrder
|
||||
|
||||
@Column({ nullable: true })
|
||||
swap_id: string
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import { ulid } from "ulid"
|
||||
import { Swap } from "./swap"
|
||||
import { Cart } from "./cart"
|
||||
import { Order } from "./order"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
|
||||
@Check(`"fulfilled_quantity" <= "quantity"`)
|
||||
@@ -59,6 +60,17 @@ export class LineItem {
|
||||
@JoinColumn({ name: "swap_id" })
|
||||
swap: Swap
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
claim_order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => ClaimOrder,
|
||||
co => co.additional_items
|
||||
)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
claim_order: ClaimOrder
|
||||
|
||||
@Column()
|
||||
title: string
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Fulfillment } from "./fulfillment"
|
||||
import { Return } from "./return"
|
||||
import { Refund } from "./refund"
|
||||
import { Swap } from "./swap"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
|
||||
export enum OrderStatus {
|
||||
@@ -183,6 +184,13 @@ export class Order {
|
||||
)
|
||||
returns: Return[]
|
||||
|
||||
@OneToMany(
|
||||
() => ClaimOrder,
|
||||
co => co.order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
claims: ClaimOrder[]
|
||||
|
||||
@OneToMany(
|
||||
() => Refund,
|
||||
ref => ref.order,
|
||||
|
||||
@@ -19,6 +19,7 @@ export enum RefundReason {
|
||||
DISCOUNT = "discount",
|
||||
RETURN = "return",
|
||||
SWAP = "swap",
|
||||
CLAIM = "claim",
|
||||
OTHER = "other",
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { ulid } from "ulid"
|
||||
|
||||
import { Order } from "./order"
|
||||
import { Swap } from "./swap"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { ReturnItem } from "./return-item"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
|
||||
@@ -52,6 +53,16 @@ export class Return {
|
||||
@JoinColumn({ name: "swap_id" })
|
||||
swap: Swap
|
||||
|
||||
@Column({ nullable: true })
|
||||
claim_order_id: string
|
||||
|
||||
@OneToOne(
|
||||
() => ClaimOrder,
|
||||
co => co.return_order
|
||||
)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
claim_order: ClaimOrder
|
||||
|
||||
@Column({ nullable: true })
|
||||
order_id: string
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* models/money-amount.js
|
||||
*
|
||||
******************************************************************************/
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
region_id: { type: String },
|
||||
currency_code: { type: String, required: true },
|
||||
amount: { type: Number, required: true, min: 0 },
|
||||
sale_amount: { type: Number, min: 0 },
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { Order } from "./order"
|
||||
import { Cart } from "./cart"
|
||||
import { Swap } from "./swap"
|
||||
@@ -18,7 +19,7 @@ import { Return } from "./return"
|
||||
import { ShippingOption } from "./shipping-option"
|
||||
|
||||
@Check(
|
||||
`"order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL`
|
||||
`"claim_order_id" IS NOT NULL OR "order_id" IS NOT NULL OR "cart_id" IS NOT NULL OR "swap_id" IS NOT NULL OR "return_id" IS NOT NULL`
|
||||
)
|
||||
@Check(`"price" >= 0`)
|
||||
@Entity()
|
||||
@@ -38,6 +39,14 @@ export class ShippingMethod {
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
claim_order_id: string
|
||||
|
||||
@ManyToOne(() => ClaimOrder)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
claim_order: ClaimOrder
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
cart_id: string
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ClaimImage } from "../models/claim-image"
|
||||
|
||||
@EntityRepository(ClaimImage)
|
||||
export class ClaimImageRepository extends Repository<ClaimImage> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ClaimItem } from "../models/claim-item"
|
||||
|
||||
@EntityRepository(ClaimItem)
|
||||
export class ClaimItemRepository extends Repository<ClaimItem> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ClaimTag } from "../models/claim-tag"
|
||||
|
||||
@EntityRepository(ClaimTag)
|
||||
export class ClaimTagRepository extends Repository<ClaimTag> {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { ClaimOrder } from "../models/claim-order"
|
||||
|
||||
@EntityRepository(ClaimOrder)
|
||||
export class ClaimRepository extends Repository<ClaimOrder> {}
|
||||
@@ -0,0 +1,128 @@
|
||||
import _ from "lodash"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import ClaimItemService from "../claim-item"
|
||||
|
||||
const withTransactionMock = jest.fn()
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("eventBus")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
describe("ClaimItemService", () => {
|
||||
describe("create", () => {
|
||||
const testItem = {
|
||||
claim_order_id: "claim_13",
|
||||
item_id: "itm_1",
|
||||
tags: ["fluff"],
|
||||
reason: "production_failure",
|
||||
note: "Details",
|
||||
quantity: 1,
|
||||
images: ["url.com/1234"],
|
||||
}
|
||||
|
||||
const claimTagRepo = MockRepository({
|
||||
findOne: () => Promise.resolve(),
|
||||
create: d => d,
|
||||
})
|
||||
|
||||
const claimImgRepo = MockRepository({
|
||||
findOne: () => Promise.resolve(),
|
||||
create: d => d,
|
||||
})
|
||||
|
||||
const claimItemRepo = MockRepository({
|
||||
create: d => ({ id: "ci_1234", ...d }),
|
||||
})
|
||||
|
||||
const lineItemService = {
|
||||
withTransaction: function() {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimItemService = new ClaimItemService({
|
||||
manager: MockManager,
|
||||
lineItemService,
|
||||
claimTagRepository: claimTagRepo,
|
||||
claimItemRepository: claimItemRepo,
|
||||
claimImageRepository: claimImgRepo,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates a claim item", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 1 })
|
||||
)
|
||||
await claimItemService.create(testItem)
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
|
||||
expect(lineItemService.retrieve).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(claimTagRepo.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(claimTagRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { value: "fluff" },
|
||||
})
|
||||
|
||||
expect(claimTagRepo.create).toHaveBeenCalledTimes(1)
|
||||
expect(claimTagRepo.create).toHaveBeenCalledWith({
|
||||
value: "fluff",
|
||||
})
|
||||
|
||||
expect(claimItemRepo.create).toHaveBeenCalledTimes(1)
|
||||
expect(claimItemRepo.create).toHaveBeenCalledWith({
|
||||
claim_order_id: "claim_13",
|
||||
item_id: "itm_1",
|
||||
reason: "production_failure",
|
||||
note: "Details",
|
||||
quantity: 1,
|
||||
tags: [{ value: "fluff" }],
|
||||
images: [{ url: "url.com/1234" }],
|
||||
})
|
||||
})
|
||||
|
||||
it("normalizes claim tag value", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 1 })
|
||||
)
|
||||
await claimItemService.create({
|
||||
...testItem,
|
||||
tags: [" FLUFF "],
|
||||
})
|
||||
expect(claimTagRepo.findOne).toHaveBeenCalledTimes(1)
|
||||
expect(claimTagRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { value: "fluff" },
|
||||
})
|
||||
})
|
||||
|
||||
it("fails if fulfilled_quantity < quantity", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 0 })
|
||||
)
|
||||
await expect(claimItemService.create(testItem)).rejects.toThrow(
|
||||
"Cannot claim more of an item than has been fulfilled"
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if reason is unknown", async () => {
|
||||
lineItemService.retrieve = jest.fn(() =>
|
||||
Promise.resolve({ fulfilled_quantity: 1 })
|
||||
)
|
||||
await expect(
|
||||
claimItemService.create({
|
||||
...testItem,
|
||||
reason: "unknown",
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Claim Item reason must be one of "missing_item", "wrong_item", "production_failure" or "other".`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,539 @@
|
||||
import _ from "lodash"
|
||||
import { IdMap, MockRepository, MockManager } from "medusa-test-utils"
|
||||
import ClaimService from "../claim"
|
||||
|
||||
const withTransactionMock = jest.fn()
|
||||
const eventBusService = {
|
||||
emit: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("eventBus")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const totalsService = {
|
||||
getRefundTotal: jest.fn(() => 1000),
|
||||
}
|
||||
|
||||
describe("ClaimService", () => {
|
||||
describe("create", () => {
|
||||
const testClaim = {
|
||||
type: "refund",
|
||||
order: {
|
||||
id: "1234",
|
||||
region_id: "order_region",
|
||||
items: [
|
||||
{
|
||||
id: "itm_1",
|
||||
unit_price: 8000,
|
||||
},
|
||||
],
|
||||
},
|
||||
claim_items: [
|
||||
{
|
||||
item_id: "itm_1",
|
||||
tags: ["fluff"],
|
||||
reason: "production_failure",
|
||||
note: "Details",
|
||||
quantity: 1,
|
||||
images: ["url.com/1234"],
|
||||
},
|
||||
],
|
||||
return_shipping: {
|
||||
option_id: "opt_13",
|
||||
price: 0,
|
||||
},
|
||||
additional_items: [
|
||||
{
|
||||
variant_id: "var_123",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
shipping_address_id: "adr_1234",
|
||||
}
|
||||
|
||||
const claimRepo = MockRepository({
|
||||
create: d => ({ id: "claim_134", ...d }),
|
||||
})
|
||||
|
||||
const returnService = {
|
||||
create: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("return")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const lineItemService = {
|
||||
generate: jest.fn((d, _, q) => ({ variant_id: d, quantity: q })),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimItemService = {
|
||||
create: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("claimItem")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
totalsService,
|
||||
returnService,
|
||||
lineItemService,
|
||||
claimItemService,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates a claim", async () => {
|
||||
await claimService.create(testClaim)
|
||||
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "1234",
|
||||
region_id: "order_region",
|
||||
items: [
|
||||
{
|
||||
id: "itm_1",
|
||||
unit_price: 8000,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
|
||||
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.generate).toHaveBeenCalledWith(
|
||||
"var_123",
|
||||
"order_region",
|
||||
1
|
||||
)
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("claimItem")
|
||||
expect(claimItemService.create).toHaveBeenCalledTimes(1)
|
||||
expect(claimItemService.create).toHaveBeenCalledWith({
|
||||
claim_order_id: "claim_134",
|
||||
item_id: "itm_1",
|
||||
tags: ["fluff"],
|
||||
reason: "production_failure",
|
||||
note: "Details",
|
||||
quantity: 1,
|
||||
images: ["url.com/1234"],
|
||||
})
|
||||
|
||||
expect(claimRepo.create).toHaveBeenCalledTimes(1)
|
||||
expect(claimRepo.create).toHaveBeenCalledWith({
|
||||
payment_status: "not_refunded",
|
||||
refund_amount: 1000,
|
||||
type: "refund",
|
||||
order_id: "1234",
|
||||
additional_items: [
|
||||
{
|
||||
variant_id: "var_123",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
shipping_address_id: "adr_1234",
|
||||
})
|
||||
|
||||
expect(claimRepo.save).toHaveBeenCalledTimes(1)
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
|
||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||
expect(eventBusService.emit).toHaveBeenCalledWith("claim.created", {
|
||||
id: "claim_134",
|
||||
})
|
||||
})
|
||||
|
||||
it("return only created when return shipping provided", async () => {
|
||||
await claimService.create({
|
||||
...testClaim,
|
||||
return_shipping: null,
|
||||
})
|
||||
expect(returnService.create).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
it("fails if replace and no additional items", async () => {
|
||||
await expect(
|
||||
claimService.create({
|
||||
...testClaim,
|
||||
type: "replace",
|
||||
additional_items: null,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Claims with type "replace" must have at least one additional item.`
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if replace and refund amount", async () => {
|
||||
await expect(
|
||||
claimService.create({
|
||||
...testClaim,
|
||||
type: "replace",
|
||||
refund_amount: 102,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Claim has type "replace" but must be type "refund" to have a refund_amount.`
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if type is unknown", async () => {
|
||||
await expect(
|
||||
claimService.create({
|
||||
...testClaim,
|
||||
type: "unknown",
|
||||
})
|
||||
).rejects.toThrow(`Claim type must be one of "refund" or "replace".`)
|
||||
})
|
||||
|
||||
it("fails if no claim items", async () => {
|
||||
await expect(
|
||||
claimService.create({
|
||||
...testClaim,
|
||||
claim_items: [],
|
||||
})
|
||||
).rejects.toThrow(`Claims must have at least one claim item.`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("retrieve", () => {
|
||||
const claimRepo = MockRepository()
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates a claim", async () => {
|
||||
claimRepo.setFindOne(() => Promise.resolve({ id: "claim_id" }))
|
||||
await claimService.retrieve("claim_id", {
|
||||
relations: ["order"],
|
||||
})
|
||||
|
||||
expect(claimRepo.findOne).toHaveBeenCalledWith({
|
||||
where: { id: "claim_id" },
|
||||
relations: ["order"],
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("createFulfillment", () => {
|
||||
const fulfillmentService = {
|
||||
createFulfillment: jest.fn((_, items) =>
|
||||
Promise.resolve([{ id: "ful", items }])
|
||||
),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("fulfillment")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const order = {
|
||||
email: "hi@123",
|
||||
discounts: [
|
||||
{
|
||||
id: "disc_1234",
|
||||
},
|
||||
],
|
||||
payments: [{ id: "pay_test" }],
|
||||
currency_code: "dkk",
|
||||
tax_rate: 25,
|
||||
region_id: "test_region",
|
||||
display_id: 112345,
|
||||
billing_address: { first_name: "hi" },
|
||||
}
|
||||
|
||||
const claim = {
|
||||
type: "replace",
|
||||
fulfillment_status: "not_fulfilled",
|
||||
order,
|
||||
additional_items: [{ id: "item_test", quantity: 1 }],
|
||||
shipping_methods: [{ id: "method_test" }],
|
||||
}
|
||||
|
||||
const claimRepo = MockRepository({
|
||||
findOne: () => Promise.resolve({ ...claim }),
|
||||
})
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
fulfillmentService,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("successfully creates fulfillment", async () => {
|
||||
await claimService.createFulfillment("claim_id", { meta: "data" })
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledTimes(3)
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("fulfillment")
|
||||
expect(fulfillmentService.createFulfillment).toHaveBeenCalledTimes(1)
|
||||
expect(fulfillmentService.createFulfillment).toHaveBeenCalledWith(
|
||||
{
|
||||
...claim,
|
||||
payments: order.payments,
|
||||
email: order.email,
|
||||
discounts: order.discounts,
|
||||
currency_code: order.currency_code,
|
||||
tax_rate: order.tax_rate,
|
||||
region_id: order.region_id,
|
||||
display_id: order.display_id,
|
||||
billing_address: order.billing_address,
|
||||
items: claim.additional_items,
|
||||
shipping_methods: claim.shipping_methods,
|
||||
is_claim: true,
|
||||
},
|
||||
[
|
||||
{
|
||||
item_id: "item_test",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
{ claim_order_id: "claim_id", metadata: { meta: "data" } }
|
||||
)
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
|
||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.update).toHaveBeenCalledWith("item_test", {
|
||||
fulfilled_quantity: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it("fails if no shipping_methods", async () => {
|
||||
claimRepo.setFindOne(() =>
|
||||
Promise.resolve({ ...claim, shipping_methods: [] })
|
||||
)
|
||||
|
||||
await expect(
|
||||
claimService.createFulfillment("claim_id", { meta: "data" })
|
||||
).rejects.toThrow(`Cannot fulfill a claim without a shipping method.`)
|
||||
})
|
||||
|
||||
it("fails if already fulfilled", async () => {
|
||||
claimRepo.setFindOne(() =>
|
||||
Promise.resolve({ ...claim, fulfillment_status: "fulfilled" })
|
||||
)
|
||||
|
||||
await expect(
|
||||
claimService.createFulfillment("claim_id", { meta: "data" })
|
||||
).rejects.toThrow(`The claim has already been fulfilled.`)
|
||||
})
|
||||
|
||||
it("fails if type is refund", async () => {
|
||||
claimRepo.setFindOne(() => Promise.resolve({ ...claim, type: "refund" }))
|
||||
|
||||
await expect(
|
||||
claimService.createFulfillment("claim_id", { meta: "data" })
|
||||
).rejects.toThrow(`Claims with the type "refund" can not be fulfilled`)
|
||||
})
|
||||
})
|
||||
|
||||
describe("createShipment", () => {
|
||||
const fulfillmentService = {
|
||||
createShipment: jest.fn(() => {
|
||||
return Promise.resolve({
|
||||
items: [
|
||||
{
|
||||
item_id: "item_1",
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
}),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("fulfillment")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const lineItemService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("lineItem")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimRepo = MockRepository()
|
||||
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
fulfillmentService,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls fulfillment service", async () => {
|
||||
claimRepo.setFindOne(() =>
|
||||
Promise.resolve({
|
||||
additional_items: [
|
||||
{
|
||||
id: "item_1",
|
||||
shippied_quantity: 1,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await claimService.createShipment("claim", "ful_123", ["track1234"], {
|
||||
meta: "data",
|
||||
})
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledTimes(3)
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("fulfillment")
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("lineItem")
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
|
||||
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledTimes(1)
|
||||
expect(fulfillmentService.createShipment).toHaveBeenCalledWith(
|
||||
"ful_123",
|
||||
["track1234"],
|
||||
{ meta: "data" }
|
||||
)
|
||||
|
||||
expect(lineItemService.update).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.update).toHaveBeenCalledWith("item_1", {
|
||||
shipped_quantity: 1,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancel", () => {
|
||||
const fulfillmentService = {
|
||||
cancelFulfillment: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("fulfillment")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const returnService = {
|
||||
cancel: jest.fn(),
|
||||
withTransaction: function() {
|
||||
withTransactionMock("return")
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const claimRepo = MockRepository()
|
||||
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
fulfillmentService,
|
||||
eventBusService,
|
||||
returnService,
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("calls fulfillment service", async () => {
|
||||
claimRepo.setFindOne(() =>
|
||||
Promise.resolve({
|
||||
return_order: {
|
||||
id: "ret",
|
||||
status: "requested",
|
||||
},
|
||||
fulfillments: [
|
||||
{
|
||||
id: "ful_21",
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
await claimService.cancel("claim")
|
||||
|
||||
expect(withTransactionMock).toHaveBeenCalledTimes(3)
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("fulfillment")
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("return")
|
||||
expect(withTransactionMock).toHaveBeenCalledWith("eventBus")
|
||||
|
||||
expect(fulfillmentService.cancelFulfillment).toHaveBeenCalledTimes(1)
|
||||
expect(fulfillmentService.cancelFulfillment).toHaveBeenCalledWith({
|
||||
id: "ful_21",
|
||||
})
|
||||
|
||||
expect(returnService.cancel).toHaveBeenCalledTimes(1)
|
||||
expect(returnService.cancel).toHaveBeenCalledWith("ret")
|
||||
})
|
||||
|
||||
it("fails if fulfillment is shipped", async () => {
|
||||
claimRepo.setFindOne(() =>
|
||||
Promise.resolve({
|
||||
fulfillment_status: "shipped",
|
||||
})
|
||||
)
|
||||
|
||||
await expect(claimService.cancel("id")).rejects.toThrow(
|
||||
"Cannot cancel a claim that has been shipped."
|
||||
)
|
||||
})
|
||||
|
||||
it("fails if return is received", async () => {
|
||||
claimRepo.setFindOne(() =>
|
||||
Promise.resolve({
|
||||
return_order: {
|
||||
id: "ret",
|
||||
status: "received",
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
await expect(claimService.cancel("id")).rejects.toThrow(
|
||||
"Cannot cancel a claim that has a received return."
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,271 @@
|
||||
import _ from "lodash"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets } from "typeorm"
|
||||
|
||||
class ClaimItemService extends BaseService {
|
||||
static Events = {
|
||||
CREATED: "claim_item.created",
|
||||
UPDATED: "claim_item.updated",
|
||||
CANCELED: "claim_item.canceled",
|
||||
}
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
claimItemRepository,
|
||||
claimTagRepository,
|
||||
claimImageRepository,
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @constant {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @constant {ClaimRepository} */
|
||||
this.claimItemRepository_ = claimItemRepository
|
||||
this.claimTagRepository_ = claimTagRepository
|
||||
this.claimImageRepository_ = claimImageRepository
|
||||
|
||||
/** @private @constant {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @constant {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
withTransaction(manager) {
|
||||
if (!manager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ClaimItemService({
|
||||
manager,
|
||||
claimItemRepository: this.claimItemRepository_,
|
||||
claimTagRepository: this.claimTagRepository_,
|
||||
claimImageRepository: this.claimImageRepository_,
|
||||
lineItemService: this.lineItemService_,
|
||||
eventBusService: this.eventBus_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = manager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
create(data) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const ciRepo = manager.getCustomRepository(this.claimItemRepository_)
|
||||
|
||||
const { item_id, reason, quantity, tags, images, ...rest } = data
|
||||
|
||||
if (
|
||||
reason !== "missing_item" &&
|
||||
reason !== "wrong_item" &&
|
||||
reason !== "production_failure" &&
|
||||
reason !== "other"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claim Item reason must be one of "missing_item", "wrong_item", "production_failure" or "other".`
|
||||
)
|
||||
}
|
||||
|
||||
const lineItem = await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(item_id)
|
||||
|
||||
if (lineItem.fulfilled_quantity < quantity) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot claim more of an item than has been fulfilled."
|
||||
)
|
||||
}
|
||||
|
||||
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 },
|
||||
})
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
return claimTagRepo.create({ value: normalized })
|
||||
})
|
||||
)
|
||||
|
||||
const claimImgRepo = manager.getCustomRepository(
|
||||
this.claimImageRepository_
|
||||
)
|
||||
const imagesToAdd = images.map(url => {
|
||||
return claimImgRepo.create({ url })
|
||||
})
|
||||
|
||||
const created = ciRepo.create({
|
||||
...rest,
|
||||
variant_id: lineItem.variant_id,
|
||||
tags: tagsToAdd,
|
||||
images: imagesToAdd,
|
||||
item_id,
|
||||
reason,
|
||||
quantity,
|
||||
})
|
||||
|
||||
const result = await ciRepo.save(created)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimItemService.Events.CREATED, {
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const ciRepo = manager.getCustomRepository(this.claimItemRepository_)
|
||||
const item = await this.retrieve(id, { relations: ["images", "tags"] })
|
||||
|
||||
const { tags, images, reason, note, metadata } = data
|
||||
|
||||
if (note) {
|
||||
item.note = note
|
||||
}
|
||||
|
||||
if (reason) {
|
||||
item.reason = reason
|
||||
}
|
||||
|
||||
if (metadata) {
|
||||
item.metadata = this.setMetadata_(item, update.metadata)
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
item.tags = []
|
||||
const claimTagRepo = manager.getCustomRepository(
|
||||
this.claimTagRepository_
|
||||
)
|
||||
for (const t of tags) {
|
||||
if (t.id) {
|
||||
item.tags.push(t)
|
||||
} else {
|
||||
const normalized = t.value.trim().toLowerCase()
|
||||
|
||||
const existing = await claimTagRepo.findOne({
|
||||
where: { value: normalized },
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
item.tags.push(existing)
|
||||
} else {
|
||||
item.tags.push(claimTagRepo.create({ value: normalized }))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (images) {
|
||||
const claimImgRepo = manager.getCustomRepository(
|
||||
this.claimImageRepository_
|
||||
)
|
||||
const ids = images.map(i => i.id)
|
||||
for (const i of item.images) {
|
||||
if (!ids.includes(i.id)) {
|
||||
await claimImgRepo.remove(i)
|
||||
}
|
||||
}
|
||||
|
||||
item.images = []
|
||||
|
||||
for (const i of images) {
|
||||
if (i.id) {
|
||||
item.images.push(i)
|
||||
} else {
|
||||
item.images.push(claimImgRepo.create({ url: i.url }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ciRepo.save(item)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimItemService.Events.UPDATED, {
|
||||
id: item.id,
|
||||
})
|
||||
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
async cancel(id) {}
|
||||
|
||||
/**
|
||||
* @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 ciRepo = this.manager_.getCustomRepository(this.claimItemRepository_)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return ciRepo.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 claimItemRepo = this.manager_.getCustomRepository(
|
||||
this.claimItemRepository_
|
||||
)
|
||||
const validatedId = this.validateId_(id)
|
||||
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
const item = await claimItemRepo.findOne(query)
|
||||
|
||||
if (!item) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Claim item with id: ${id} was not found.`
|
||||
)
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to delete metadata for an order.
|
||||
* @param {string} orderId - the order to delete metadata from.
|
||||
* @param {string} key - key for metadata field
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
async deleteMetadata(orderId, key) {
|
||||
const validatedId = this.validateId_(orderId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
|
||||
const keyPath = `metadata.${key}`
|
||||
return this.orderModel_
|
||||
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ClaimItemService
|
||||
@@ -0,0 +1,575 @@
|
||||
import _ from "lodash"
|
||||
import { Validator, MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { Brackets } from "typeorm"
|
||||
|
||||
class ClaimService extends BaseService {
|
||||
static Events = {
|
||||
CREATED: "claim.created",
|
||||
UPDATED: "claim.updated",
|
||||
CANCELED: "claim.canceled",
|
||||
FULFILLMENT_CREATED: "claim.fulfillment_created",
|
||||
SHIPMENT_CREATED: "claim.shipment_created",
|
||||
REFUND_PROCESSED: "claim.refund_processed",
|
||||
}
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
claimRepository,
|
||||
fulfillmentProviderService,
|
||||
fulfillmentService,
|
||||
lineItemService,
|
||||
totalsService,
|
||||
paymentProviderService,
|
||||
returnService,
|
||||
shippingOptionService,
|
||||
claimItemService,
|
||||
regionService,
|
||||
eventBusService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @constant {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @constant {ClaimRepository} */
|
||||
this.claimRepository_ = claimRepository
|
||||
|
||||
/** @private @constant {FulfillmentProviderService} */
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
|
||||
/** @private @constant {PaymentProviderService} */
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
|
||||
/** @private @constant {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @constant {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @constant {ReturnService} */
|
||||
this.returnService_ = returnService
|
||||
|
||||
/** @private @constant {FulfillmentService} */
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
|
||||
/** @private @constant {ClaimItemService} */
|
||||
this.claimItemService_ = claimItemService
|
||||
|
||||
/** @private @constant {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @constant {EventBus} */
|
||||
this.eventBus_ = eventBusService
|
||||
|
||||
/** @private @constant {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
}
|
||||
|
||||
withTransaction(manager) {
|
||||
if (!manager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ClaimService({
|
||||
manager,
|
||||
claimRepository: this.claimRepository_,
|
||||
fulfillmentProviderService: this.fulfillmentProviderService_,
|
||||
fulfillmentService: this.fulfillmentService_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
regionService: this.regionService_,
|
||||
returnService: this.returnService_,
|
||||
claimItemService: this.claimItemService_,
|
||||
eventBusService: this.eventBus_,
|
||||
totalsService: this.totalsService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = manager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
update(id, data) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const claim = await this.retrieve(id, { relations: ["shipping_methods"] })
|
||||
|
||||
const { claim_items, shipping_methods, metadata } = data
|
||||
|
||||
if (metadata) {
|
||||
claim.metadata = this.setMetadata_(claim, update.metadata)
|
||||
await claimRepo.save(claim)
|
||||
}
|
||||
|
||||
if (shipping_methods) {
|
||||
for (const m of claim.shipping_methods) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.updateShippingMethod(m.id, {
|
||||
claim_order_id: null,
|
||||
})
|
||||
}
|
||||
|
||||
for (const method of shipping_methods) {
|
||||
if (method.id) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.updateShippingMethod(method.id, {
|
||||
claim_order_id: claim.id,
|
||||
})
|
||||
} else {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.createShippingMethod(method.option_id, method.data, {
|
||||
claim_order_id: claim.id,
|
||||
price: method.price,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (claim_items) {
|
||||
for (const i of claim_items) {
|
||||
if (i.id) {
|
||||
await this.claimItemService_
|
||||
.withTransaction(manager)
|
||||
.update(i.id, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.UPDATED, {
|
||||
id: claim.id,
|
||||
})
|
||||
|
||||
return claim
|
||||
})
|
||||
}
|
||||
|
||||
create(data) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
|
||||
const {
|
||||
type,
|
||||
claim_items,
|
||||
order,
|
||||
return_shipping,
|
||||
additional_items,
|
||||
shipping_methods,
|
||||
refund_amount,
|
||||
...rest
|
||||
} = data
|
||||
|
||||
if (type !== "refund" && type !== "replace") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claim type must be one of "refund" or "replace".`
|
||||
)
|
||||
}
|
||||
|
||||
if (type === "replace" && !additional_items?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claims with type "replace" must have at least one additional item.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!claim_items?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claims must have at least one claim item.`
|
||||
)
|
||||
}
|
||||
|
||||
if (refund_amount && type !== "refund") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claim has type "${type}" but must be type "refund" to have a refund_amount.`
|
||||
)
|
||||
}
|
||||
|
||||
let toRefund = refund_amount
|
||||
if (type === "refund" && typeof refund_amount === "undefined") {
|
||||
const lines = claim_items.map(ci => {
|
||||
const orderItem = order.items.find(oi => oi.id === ci.item_id)
|
||||
return {
|
||||
...orderItem,
|
||||
quantity: ci.quantity,
|
||||
}
|
||||
})
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, lines)
|
||||
}
|
||||
|
||||
const newItems = await Promise.all(
|
||||
additional_items.map(i =>
|
||||
this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.generate(i.variant_id, order.region_id, i.quantity)
|
||||
)
|
||||
)
|
||||
|
||||
const created = claimRepo.create({
|
||||
shipping_address_id: order.shipping_address_id,
|
||||
payment_status: type === "refund" ? "not_refunded" : "na",
|
||||
...rest,
|
||||
refund_amount: toRefund,
|
||||
type,
|
||||
additional_items: newItems,
|
||||
order_id: order.id,
|
||||
})
|
||||
|
||||
const result = await claimRepo.save(created)
|
||||
|
||||
if (shipping_methods) {
|
||||
for (const method of shipping_methods) {
|
||||
if (method.id) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.updateShippingMethod(method.id, {
|
||||
claim_order_id: result.id,
|
||||
})
|
||||
} else {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.createShippingMethod(method.option_id, method.data, {
|
||||
claim_order_id: result.id,
|
||||
price: method.price,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const ci of claim_items) {
|
||||
await this.claimItemService_.withTransaction(manager).create({
|
||||
...ci,
|
||||
claim_order_id: result.id,
|
||||
})
|
||||
}
|
||||
|
||||
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.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.CREATED, {
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
createFulfillment(id, metadata = {}) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: [
|
||||
"additional_items",
|
||||
"shipping_methods",
|
||||
"shipping_address",
|
||||
"order",
|
||||
"order.billing_address",
|
||||
"order.discounts",
|
||||
"order.payments",
|
||||
],
|
||||
})
|
||||
|
||||
const order = claim.order
|
||||
|
||||
if (claim.fulfillment_status !== "not_fulfilled") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"The claim has already been fulfilled."
|
||||
)
|
||||
}
|
||||
|
||||
if (claim.type !== "replace") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Claims with the type "${claim.type}" can not be fulfilled.`
|
||||
)
|
||||
}
|
||||
|
||||
if (!claim.shipping_methods?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot fulfill a claim without a shipping method."
|
||||
)
|
||||
}
|
||||
|
||||
const fulfillments = await this.fulfillmentService_
|
||||
.withTransaction(manager)
|
||||
.createFulfillment(
|
||||
{
|
||||
...claim,
|
||||
email: order.email,
|
||||
payments: order.payments,
|
||||
discounts: order.discounts,
|
||||
currency_code: order.currency_code,
|
||||
tax_rate: order.tax_rate,
|
||||
region_id: order.region_id,
|
||||
display_id: order.display_id,
|
||||
billing_address: order.billing_address,
|
||||
items: claim.additional_items,
|
||||
shipping_methods: claim.shipping_methods,
|
||||
is_claim: true,
|
||||
},
|
||||
claim.additional_items.map(i => ({
|
||||
item_id: i.id,
|
||||
quantity: i.quantity,
|
||||
})),
|
||||
{ claim_order_id: id, metadata }
|
||||
)
|
||||
|
||||
let successfullyFulfilled = []
|
||||
for (const f of fulfillments) {
|
||||
successfullyFulfilled = successfullyFulfilled.concat(f.items)
|
||||
}
|
||||
|
||||
claim.fulfillment_status = "fulfilled"
|
||||
|
||||
for (const item of claim.additional_items) {
|
||||
const fulfillmentItem = successfullyFulfilled.find(
|
||||
f => item.id === f.item_id
|
||||
)
|
||||
|
||||
if (fulfillmentItem) {
|
||||
const fulfilledQuantity =
|
||||
(item.fulfilled_quantity || 0) + fulfillmentItem.quantity
|
||||
|
||||
// Update the fulfilled quantity
|
||||
await this.lineItemService_.withTransaction(manager).update(item.id, {
|
||||
fulfilled_quantity: fulfilledQuantity,
|
||||
})
|
||||
|
||||
if (item.quantity !== fulfilledQuantity) {
|
||||
claim.fulfillment_status = "requires_action"
|
||||
}
|
||||
} else {
|
||||
if (item.quantity !== item.fulfilled_quantity) {
|
||||
claim.fulfillment_status = "requires_action"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const result = await claimRepo.save(claim)
|
||||
|
||||
for (const fulfillment of fulfillments) {
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.FULFILLMENT_CREATED, {
|
||||
id: id,
|
||||
fulfillment_id: fulfillment.id,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async processRefund(id) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["order", "order.payments"],
|
||||
})
|
||||
|
||||
if (claim.type !== "refund") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Claim must have type "refund" to create a refund.`
|
||||
)
|
||||
}
|
||||
|
||||
if (claim.refund_amount) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
.refundPayment(claim.order.payments, claim.refund_amount, "claim")
|
||||
}
|
||||
|
||||
claim.payment_status = "refunded"
|
||||
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const result = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.REFUND_PROCESSED, {
|
||||
id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async createShipment(id, fulfillmentId, trackingNumbers, metadata = []) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["additional_items"],
|
||||
})
|
||||
|
||||
const shipment = await this.fulfillmentService_
|
||||
.withTransaction(manager)
|
||||
.createShipment(fulfillmentId, trackingNumbers, metadata)
|
||||
|
||||
claim.fulfillment_status = "shipped"
|
||||
|
||||
for (const i of claim.additional_items) {
|
||||
const shipped = shipment.items.find(si => si.item_id === i.id)
|
||||
if (shipped) {
|
||||
const shippedQty = (i.shipped_quantity || 0) + shipped.quantity
|
||||
await this.lineItemService_.withTransaction(manager).update(i.id, {
|
||||
shipped_quantity: shippedQty,
|
||||
})
|
||||
|
||||
if (shippedQty !== i.quantity) {
|
||||
claim.fulfillment_status = "partially_shipped"
|
||||
}
|
||||
} else {
|
||||
if (i.shipped_quantity !== i.quantity) {
|
||||
claim.fulfillment_status = "partially_shipped"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const result = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.SHIPMENT_CREATED, {
|
||||
id,
|
||||
fulfillment_id: shipment.id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async cancel(id) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["return_order", "fulfillments"],
|
||||
})
|
||||
|
||||
if (
|
||||
claim.fulfillment_status === "shipped" ||
|
||||
claim.fulfillment_status === "partially_shipped"
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Cannot cancel a claim that has been shipped.`
|
||||
)
|
||||
}
|
||||
|
||||
if (claim?.return_order?.status === "received") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Cannot cancel a claim that has a received return.`
|
||||
)
|
||||
}
|
||||
|
||||
if (claim.return_order) {
|
||||
await this.returnService_
|
||||
.withTransaction(manager)
|
||||
.cancel(claim.return_order.id)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
claim.fulfillments.map(f =>
|
||||
this.fulfillmentService_.withTransaction(manager).cancelFulfillment(f)
|
||||
)
|
||||
)
|
||||
|
||||
claim.fulfillment_status = "canceled"
|
||||
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const result = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.CANCELED, {
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 claimRepo = this.manager_.getCustomRepository(this.claimRepository_)
|
||||
const query = this.buildQuery_(selector, config)
|
||||
return claimRepo.find(query)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an order by id.
|
||||
* @param {string} orderId - id of order to retrieve
|
||||
* @return {Promise<Order>} the order document
|
||||
*/
|
||||
async retrieve(claimId, config = {}) {
|
||||
const claimRepo = this.manager_.getCustomRepository(this.claimRepository_)
|
||||
const validatedId = this.validateId_(claimId)
|
||||
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
const claim = await claimRepo.findOne(query)
|
||||
|
||||
if (!claim) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Claim with ${claimId} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return claim
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated method to delete metadata for an order.
|
||||
* @param {string} orderId - the order to delete metadata from.
|
||||
* @param {string} key - key for metadata field
|
||||
* @return {Promise} resolves to the updated result.
|
||||
*/
|
||||
async deleteMetadata(orderId, key) {
|
||||
const validatedId = this.validateId_(orderId)
|
||||
|
||||
if (typeof key !== "string") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
"Key type is invalid. Metadata keys must be strings"
|
||||
)
|
||||
}
|
||||
|
||||
const keyPath = `metadata.${key}`
|
||||
return this.orderModel_
|
||||
.updateOne({ _id: validatedId }, { $unset: { [keyPath]: "" } })
|
||||
.catch(err => {
|
||||
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ClaimService
|
||||
@@ -311,6 +311,7 @@ class ReturnService extends BaseService {
|
||||
"shipping_method",
|
||||
"shipping_method.shipping_option",
|
||||
"swap",
|
||||
"claim_order",
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -177,6 +177,10 @@ class ShippingOptionService extends BaseService {
|
||||
method.order_id = update.order_id
|
||||
}
|
||||
|
||||
if ("claim_order_id" in update) {
|
||||
method.claim_order_id = update.claim_order_id
|
||||
}
|
||||
|
||||
return methodRepo.save(method)
|
||||
})
|
||||
}
|
||||
@@ -246,6 +250,10 @@ class ShippingOptionService extends BaseService {
|
||||
toCreate.order_id = config.order_id
|
||||
}
|
||||
|
||||
if (config.claim_order_id) {
|
||||
toCreate.claim_order_id = config.claim_order_id
|
||||
}
|
||||
|
||||
const method = await methodRepo.create(toCreate)
|
||||
|
||||
const created = await methodRepo.save(method)
|
||||
|
||||
Reference in New Issue
Block a user