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:
Sebastian Rindom
2021-02-03 09:49:12 +01:00
committed by GitHub
parent 0e0dfea571
commit 690d339667
60 changed files with 9429 additions and 189 deletions
@@ -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
}
}
-24
View File
@@ -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()
+39
View File
@@ -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"
+14 -6
View File
@@ -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)))`
)
}
}
+52
View File
@@ -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}`
}
}
+111
View File
@@ -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}`
}
}
+147
View File
@@ -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}`
}
}
+43
View File
@@ -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}`
}
}
+11
View File
@@ -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
+12
View File
@@ -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
+8
View File
@@ -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,
+1
View File
@@ -19,6 +19,7 @@ export enum RefundReason {
DISCOUNT = "discount",
RETURN = "return",
SWAP = "swap",
CLAIM = "claim",
OTHER = "other",
}
+11
View File
@@ -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 },
})
+10 -1
View File
@@ -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."
)
})
})
})
+271
View File
@@ -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
+575
View File
@@ -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
+1
View File
@@ -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)