Refactor: claim service to TS + refactoring (#1287)
This commit is contained in:
committed by
GitHub
parent
63bf684e66
commit
78bd61abe1
@@ -14,6 +14,12 @@ import { MedusaError } from "medusa-core-utils"
|
||||
import { defaultAdminOrdersFields, defaultAdminOrdersRelations } from "."
|
||||
import { AddressPayload } from "../../../../types/common"
|
||||
import { validator } from "../../../../utils/validator"
|
||||
import {
|
||||
ClaimItemReason,
|
||||
ClaimItemReasonValue,
|
||||
ClaimTypeValue,
|
||||
} from "../../../../types/claim"
|
||||
import { ClaimType } from "../../../../models"
|
||||
|
||||
/**
|
||||
* @oas [post] /order/{id}/claims
|
||||
@@ -332,26 +338,10 @@ export default async (req, res) => {
|
||||
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
|
||||
}
|
||||
|
||||
enum ClaimTypeEnum {
|
||||
replace = "replace",
|
||||
refund = "refund",
|
||||
}
|
||||
|
||||
type ClaimType = `${ClaimTypeEnum}`
|
||||
|
||||
enum ClaimItemReasonEnum {
|
||||
missing_item = "missing_item",
|
||||
wrong_item = "wrong_item",
|
||||
production_failure = "production_failure",
|
||||
other = "other",
|
||||
}
|
||||
|
||||
type ClaimItemReasonType = `${ClaimItemReasonEnum}`
|
||||
|
||||
export class AdminPostOrdersOrderClaimsReq {
|
||||
@IsEnum(ClaimTypeEnum)
|
||||
@IsEnum(ClaimType)
|
||||
@IsNotEmpty()
|
||||
type: ClaimType
|
||||
type: ClaimTypeValue
|
||||
|
||||
@IsArray()
|
||||
@IsNotEmpty()
|
||||
@@ -432,9 +422,9 @@ class Item {
|
||||
@IsOptional()
|
||||
note?: string
|
||||
|
||||
@IsEnum(ClaimItemReasonEnum)
|
||||
@IsEnum(ClaimItemReason)
|
||||
@IsOptional()
|
||||
reason?: ClaimItemReasonType
|
||||
reason?: ClaimItemReasonValue
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
|
||||
@@ -65,7 +65,7 @@ export default async (req, res) => {
|
||||
export class AdminPostOrdersOrderClaimsClaimFulfillmentsReq {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
@@ -133,7 +133,7 @@ export class AdminPostOrdersOrderClaimsClaimReq {
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
metadata?: object
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
class ShippingMethod {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ShippingMethod {
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
claim_order_id: string
|
||||
claim_order_id: string | null
|
||||
|
||||
@ManyToOne(() => ClaimOrder)
|
||||
@JoinColumn({ name: "claim_order_id" })
|
||||
|
||||
@@ -58,6 +58,10 @@ describe("ClaimService", () => {
|
||||
create: (d) => ({ id: "claim_134", ...d }),
|
||||
})
|
||||
|
||||
const lineItemRepository = MockRepository({
|
||||
create: (d) => ({ id: "claim_item_134", ...d }),
|
||||
})
|
||||
|
||||
const taxProviderService = {
|
||||
createTaxLines: jest.fn(),
|
||||
withTransaction: function () {
|
||||
@@ -103,6 +107,7 @@ describe("ClaimService", () => {
|
||||
const claimService = new ClaimService({
|
||||
manager: MockManager,
|
||||
claimRepository: claimRepo,
|
||||
lineItemRepository: lineItemRepository,
|
||||
taxProviderService,
|
||||
totalsService,
|
||||
returnService,
|
||||
|
||||
@@ -974,7 +974,6 @@ describe("SwapService", () => {
|
||||
|
||||
const swapService = new SwapService({
|
||||
manager: MockManager,
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
|
||||
@@ -1,753 +0,0 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
|
||||
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,
|
||||
addressRepository,
|
||||
claimItemService,
|
||||
claimRepository,
|
||||
eventBusService,
|
||||
fulfillmentProviderService,
|
||||
fulfillmentService,
|
||||
inventoryService,
|
||||
lineItemService,
|
||||
paymentProviderService,
|
||||
regionService,
|
||||
returnService,
|
||||
shippingOptionService,
|
||||
taxProviderService,
|
||||
totalsService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @constant {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
this.addressRepo_ = addressRepository
|
||||
this.claimItemService_ = claimItemService
|
||||
this.claimRepository_ = claimRepository
|
||||
this.eventBus_ = eventBusService
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.lineItemService_ = lineItemService
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.regionService_ = regionService
|
||||
this.returnService_ = returnService
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.totalsService_ = totalsService
|
||||
}
|
||||
|
||||
withTransaction(manager) {
|
||||
if (!manager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new ClaimService({
|
||||
manager,
|
||||
addressRepository: this.addressRepo_,
|
||||
claimItemService: this.claimItemService_,
|
||||
claimRepository: this.claimRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
fulfillmentProviderService: this.fulfillmentProviderService_,
|
||||
fulfillmentService: this.fulfillmentService_,
|
||||
inventoryService: this.inventoryService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
paymentProviderService: this.paymentProviderService_,
|
||||
regionService: this.regionService_,
|
||||
returnService: this.returnService_,
|
||||
shippingOptionService: this.shippingOptionService_,
|
||||
totalsService: this.totalsService_,
|
||||
taxProviderService: this.taxProviderService_,
|
||||
})
|
||||
|
||||
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"] })
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be updated"
|
||||
)
|
||||
}
|
||||
|
||||
const { claim_items, shipping_methods, metadata, no_notification } = data
|
||||
|
||||
if (metadata) {
|
||||
claim.metadata = this.setMetadata_(claim, 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 (no_notification !== undefined) {
|
||||
claim.no_notification = no_notification
|
||||
await claimRepo.save(claim)
|
||||
}
|
||||
|
||||
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,
|
||||
no_notification: claim.no_notification,
|
||||
})
|
||||
|
||||
return claim
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Claim on an Order. Claims consists of items that are claimed and
|
||||
* optionally items to be sent as replacement for the claimed items. The
|
||||
* shipping address that the new items will be shipped to
|
||||
* @param {Object} data - the object containing all data required to create a claim
|
||||
* @return {Object} created 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,
|
||||
shipping_address,
|
||||
shipping_address_id,
|
||||
no_notification,
|
||||
...rest
|
||||
} = data
|
||||
|
||||
for (const item of claim_items) {
|
||||
const line = await this.lineItemService_.retrieve(item.item_id, {
|
||||
relations: ["order", "swap", "claim_order", "tax_lines"],
|
||||
})
|
||||
|
||||
if (
|
||||
line.order?.canceled_at ||
|
||||
line.swap?.canceled_at ||
|
||||
line.claim_order?.canceled_at
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot create a claim on a canceled item.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let addressId = shipping_address_id || order.shipping_address_id
|
||||
if (shipping_address) {
|
||||
const addressRepo = manager.getCustomRepository(this.addressRepo_)
|
||||
const created = addressRepo.create(shipping_address)
|
||||
const saved = await addressRepo.save(created)
|
||||
addressId = saved.id
|
||||
}
|
||||
|
||||
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 allOrderItems = order.items
|
||||
|
||||
if (order.swaps?.length) {
|
||||
for (const swap of order.swaps) {
|
||||
swap.additional_items.forEach((it) => {
|
||||
if (
|
||||
it.shipped_quantity ||
|
||||
it.shipped_quantity === it.fulfilled_quantity
|
||||
) {
|
||||
allOrderItems.push(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (order.claims?.length) {
|
||||
for (const claim of order.claims) {
|
||||
claim.additional_items.forEach((it) => {
|
||||
if (
|
||||
it.shipped_quantity ||
|
||||
it.shipped_quantity === it.fulfilled_quantity
|
||||
) {
|
||||
allOrderItems.push(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const orderItem = allOrderItems.find((oi) => oi.id === ci.item_id)
|
||||
return {
|
||||
...orderItem,
|
||||
quantity: ci.quantity,
|
||||
}
|
||||
})
|
||||
toRefund = await this.totalsService_.getRefundTotal(order, lines)
|
||||
}
|
||||
|
||||
let newItems = []
|
||||
if (typeof additional_items !== "undefined") {
|
||||
for (const item of additional_items) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(item.variant_id, item.quantity)
|
||||
}
|
||||
|
||||
newItems = await Promise.all(
|
||||
additional_items.map((i) =>
|
||||
this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.generate(i.variant_id, order.region_id, i.quantity)
|
||||
)
|
||||
)
|
||||
|
||||
for (const newItem of newItems) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.adjustInventory(newItem.variant_id, -newItem.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
const evaluatedNoNotification =
|
||||
no_notification !== undefined ? no_notification : order.no_notification
|
||||
|
||||
const created = claimRepo.create({
|
||||
shipping_address_id: addressId,
|
||||
payment_status: type === "refund" ? "not_refunded" : "na",
|
||||
...rest,
|
||||
refund_amount: toRefund,
|
||||
type,
|
||||
additional_items: newItems,
|
||||
order_id: order.id,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
|
||||
const result = await claimRepo.save(created)
|
||||
|
||||
if (result.additional_items && result.additional_items.length) {
|
||||
const calcContext = this.totalsService_.getCalculationContext(order)
|
||||
const lineItems = await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.list({
|
||||
id: result.additional_items.map((i) => i.id),
|
||||
})
|
||||
await this.taxProviderService_
|
||||
.withTransaction(manager)
|
||||
.createTaxLines(lineItems, calcContext)
|
||||
}
|
||||
|
||||
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({
|
||||
order_id: order.id,
|
||||
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,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
}
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.CREATED, {
|
||||
id: result.id,
|
||||
no_notification: result.no_notification,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @param {string} id - the object containing all data required to create a claim
|
||||
* @param {Object} config - config object
|
||||
* @param {Object | undefined} config.metadata - config metadata
|
||||
* @param {boolean|undefined} config.no_notification - config no notification
|
||||
* @return {Claim} created claim
|
||||
*/
|
||||
createFulfillment(
|
||||
id,
|
||||
config = {
|
||||
metadata: {},
|
||||
no_notification: undefined,
|
||||
}
|
||||
) {
|
||||
const { metadata, no_notification } = config
|
||||
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: [
|
||||
"additional_items",
|
||||
"additional_items.tax_lines",
|
||||
"shipping_methods",
|
||||
"shipping_methods.tax_lines",
|
||||
"shipping_address",
|
||||
"order",
|
||||
"order.billing_address",
|
||||
"order.discounts",
|
||||
"order.discounts.rule",
|
||||
"order.payments",
|
||||
],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be fulfilled"
|
||||
)
|
||||
}
|
||||
|
||||
const order = claim.order
|
||||
|
||||
if (
|
||||
claim.fulfillment_status !== "not_fulfilled" &&
|
||||
claim.fulfillment_status !== "canceled"
|
||||
) {
|
||||
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 evaluatedNoNotification =
|
||||
no_notification !== undefined ? no_notification : claim.no_notification
|
||||
|
||||
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,
|
||||
no_notification: evaluatedNoNotification,
|
||||
},
|
||||
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,
|
||||
no_notification: claim.no_notification,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async cancelFulfillment(fulfillmentId) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const canceled = await this.fulfillmentService_
|
||||
.withTransaction(manager)
|
||||
.cancelFulfillment(fulfillmentId)
|
||||
|
||||
if (!canceled.claim_order_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Fufillment not related to a claim`
|
||||
)
|
||||
}
|
||||
|
||||
const claim = await this.retrieve(canceled.claim_order_id)
|
||||
|
||||
claim.fulfillment_status = "canceled"
|
||||
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const updated = await claimRepo.save(claim)
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
async processRefund(id) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["order", "order.payments"],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be processed"
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
no_notification: result.no_notification,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async createShipment(
|
||||
id,
|
||||
fulfillmentId,
|
||||
trackingLinks,
|
||||
config = {
|
||||
metadata: {},
|
||||
no_notification: undefined,
|
||||
}
|
||||
) {
|
||||
const { metadata, no_notification } = config
|
||||
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["additional_items"],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be fulfilled as shipped"
|
||||
)
|
||||
}
|
||||
const evaluatedNoNotification =
|
||||
no_notification !== undefined ? no_notification : claim.no_notification
|
||||
|
||||
const shipment = await this.fulfillmentService_
|
||||
.withTransaction(manager)
|
||||
.createShipment(fulfillmentId, trackingLinks, {
|
||||
metadata,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
|
||||
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,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async cancel(id) {
|
||||
return this.atomicPhase_(async (manager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["return_order", "fulfillments", "order", "order.refunds"],
|
||||
})
|
||||
if (claim.refund_amount) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Claim with a refund cannot be canceled"
|
||||
)
|
||||
}
|
||||
|
||||
if (claim.fulfillments) {
|
||||
for (const f of claim.fulfillments) {
|
||||
if (!f.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"All fulfillments must be canceled before the claim can be canceled"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (claim.return_order && claim.return_order.status !== "canceled") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Return must be canceled before the claim can be canceled"
|
||||
)
|
||||
}
|
||||
|
||||
claim.fulfillment_status = "canceled"
|
||||
claim.canceled_at = new Date()
|
||||
|
||||
const claimRepo = manager.getCustomRepository(this.claimRepository_)
|
||||
const result = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(ClaimService.Events.CANCELED, {
|
||||
id: result.id,
|
||||
no_notification: result.no_notification,
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} selector - the query object for find
|
||||
* @param {Object} config - the config object containing query settings
|
||||
* @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} claimId - id of order to retrieve
|
||||
* @param {Object} config - the config object containing query settings
|
||||
* @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
|
||||
@@ -0,0 +1,858 @@
|
||||
import ClaimItemService from "./claim-item"
|
||||
import EventBusService from "./event-bus"
|
||||
import FulfillmentProviderService from "./fulfillment-provider"
|
||||
import FulfillmentService from "./fulfillment"
|
||||
import InventoryService from "./inventory"
|
||||
import LineItemService from "./line-item"
|
||||
import PaymentProviderService from "./payment-provider"
|
||||
import RegionService from "./region"
|
||||
import ReturnService from "./return"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
import TaxProviderService from "./tax-provider"
|
||||
import TotalsService from "./totals"
|
||||
import { AddressRepository } from "../repositories/address"
|
||||
import {
|
||||
ClaimFulfillmentStatus,
|
||||
ClaimOrder,
|
||||
ClaimPaymentStatus,
|
||||
ClaimType,
|
||||
FulfillmentItem,
|
||||
LineItem,
|
||||
} from "../models"
|
||||
import { ClaimRepository } from "../repositories/claim"
|
||||
import { DeepPartial, EntityManager } from "typeorm"
|
||||
import { LineItemRepository } from "../repositories/line-item"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { ShippingMethodRepository } from "../repositories/shipping-method"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { FindConfig } from "../types/common"
|
||||
import { CreateClaimInput, UpdateClaimInput } from "../types/claim"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
addressRepository: typeof AddressRepository
|
||||
shippingMethodRepository: typeof ShippingMethodRepository
|
||||
lineItemRepository: typeof LineItemRepository
|
||||
claimRepository: typeof ClaimRepository
|
||||
claimItemService: ClaimItemService
|
||||
eventBusService: EventBusService
|
||||
fulfillmentProviderService: FulfillmentProviderService
|
||||
fulfillmentService: FulfillmentService
|
||||
inventoryService: InventoryService
|
||||
lineItemService: LineItemService
|
||||
paymentProviderService: PaymentProviderService
|
||||
regionService: RegionService
|
||||
returnService: ReturnService
|
||||
shippingOptionService: ShippingOptionService
|
||||
taxProviderService: TaxProviderService
|
||||
totalsService: TotalsService
|
||||
}
|
||||
|
||||
export default class ClaimService extends TransactionBaseService<
|
||||
ClaimService,
|
||||
InjectedDependencies
|
||||
> {
|
||||
static readonly 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",
|
||||
}
|
||||
|
||||
protected manager_: EntityManager
|
||||
protected transactionManager_: EntityManager | undefined
|
||||
|
||||
protected readonly addressRepository_: typeof AddressRepository
|
||||
protected readonly claimRepository_: typeof ClaimRepository
|
||||
protected readonly shippingMethodRepository_: typeof ShippingMethodRepository
|
||||
protected readonly lineItemRepository_: typeof LineItemRepository
|
||||
protected readonly claimItemService_: ClaimItemService
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly fulfillmentProviderService_: FulfillmentProviderService
|
||||
protected readonly fulfillmentService_: FulfillmentService
|
||||
protected readonly inventoryService_: InventoryService
|
||||
protected readonly lineItemService_: LineItemService
|
||||
protected readonly paymentProviderService_: PaymentProviderService
|
||||
protected readonly regionService_: RegionService
|
||||
protected readonly returnService_: ReturnService
|
||||
protected readonly shippingOptionService_: ShippingOptionService
|
||||
protected readonly taxProviderService_: TaxProviderService
|
||||
protected readonly totalsService_: TotalsService
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
addressRepository,
|
||||
claimRepository,
|
||||
shippingMethodRepository,
|
||||
lineItemRepository,
|
||||
claimItemService,
|
||||
eventBusService,
|
||||
fulfillmentProviderService,
|
||||
fulfillmentService,
|
||||
inventoryService,
|
||||
lineItemService,
|
||||
paymentProviderService,
|
||||
regionService,
|
||||
returnService,
|
||||
shippingOptionService,
|
||||
taxProviderService,
|
||||
totalsService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
|
||||
this.manager_ = manager
|
||||
|
||||
this.addressRepository_ = addressRepository
|
||||
this.claimRepository_ = claimRepository
|
||||
this.shippingMethodRepository_ = shippingMethodRepository
|
||||
this.lineItemRepository_ = lineItemRepository
|
||||
this.claimItemService_ = claimItemService
|
||||
this.eventBus_ = eventBusService
|
||||
this.fulfillmentProviderService_ = fulfillmentProviderService
|
||||
this.fulfillmentService_ = fulfillmentService
|
||||
this.inventoryService_ = inventoryService
|
||||
this.lineItemService_ = lineItemService
|
||||
this.paymentProviderService_ = paymentProviderService
|
||||
this.regionService_ = regionService
|
||||
this.returnService_ = returnService
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
this.taxProviderService_ = taxProviderService
|
||||
this.totalsService_ = totalsService
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateClaimInput): Promise<ClaimOrder> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["shipping_methods"],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be updated"
|
||||
)
|
||||
}
|
||||
|
||||
const { claim_items, shipping_methods, metadata, no_notification } =
|
||||
data
|
||||
|
||||
if (metadata) {
|
||||
claim.metadata = setMetadata(claim, metadata)
|
||||
await claimRepo.save(claim)
|
||||
}
|
||||
|
||||
if (shipping_methods) {
|
||||
for (const m of claim.shipping_methods) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.updateShippingMethod(m.id, {
|
||||
claim_order_id: null,
|
||||
})
|
||||
}
|
||||
|
||||
for (const method of shipping_methods) {
|
||||
if (method.id) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.updateShippingMethod(method.id, {
|
||||
claim_order_id: claim.id,
|
||||
})
|
||||
} else {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.createShippingMethod(
|
||||
method.option_id as string,
|
||||
(method as any).data,
|
||||
{
|
||||
claim_order_id: claim.id,
|
||||
price: method.price,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (no_notification !== undefined) {
|
||||
claim.no_notification = no_notification
|
||||
await claimRepo.save(claim)
|
||||
}
|
||||
|
||||
if (claim_items) {
|
||||
for (const i of claim_items) {
|
||||
if (i.id) {
|
||||
await this.claimItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.update(i.id, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(ClaimService.Events.UPDATED, {
|
||||
id: claim.id,
|
||||
no_notification: claim.no_notification,
|
||||
})
|
||||
|
||||
return claim
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Claim on an Order. Claims consists of items that are claimed and
|
||||
* optionally items to be sent as replacement for the claimed items. The
|
||||
* shipping address that the new items will be shipped to
|
||||
* @param data - the object containing all data required to create a claim
|
||||
* @return created claim
|
||||
*/
|
||||
async create(data: CreateClaimInput): Promise<ClaimOrder> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
|
||||
const {
|
||||
type,
|
||||
claim_items,
|
||||
order,
|
||||
return_shipping,
|
||||
additional_items,
|
||||
shipping_methods,
|
||||
refund_amount,
|
||||
shipping_address,
|
||||
shipping_address_id,
|
||||
no_notification,
|
||||
...rest
|
||||
} = data
|
||||
|
||||
for (const item of claim_items) {
|
||||
const line = await this.lineItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.retrieve(item.item_id, {
|
||||
relations: ["order", "swap", "claim_order", "tax_lines"],
|
||||
})
|
||||
|
||||
if (
|
||||
line.order?.canceled_at ||
|
||||
line.swap?.canceled_at ||
|
||||
line.claim_order?.canceled_at
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Cannot create a claim on a canceled item.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let addressId = shipping_address_id || order.shipping_address_id
|
||||
if (shipping_address) {
|
||||
const addressRepo = transactionManager.getCustomRepository(
|
||||
this.addressRepository_
|
||||
)
|
||||
const created = addressRepo.create(shipping_address)
|
||||
const saved = await addressRepo.save(created)
|
||||
addressId = saved.id
|
||||
}
|
||||
|
||||
if (type !== ClaimType.REFUND && type !== ClaimType.REPLACE) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Claim type must be one of "refund" or "replace".`
|
||||
)
|
||||
}
|
||||
|
||||
if (type === ClaimType.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 !== ClaimType.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 === ClaimType.REFUND && typeof refund_amount === "undefined") {
|
||||
const lines = claim_items.map((ci) => {
|
||||
const allOrderItems = order.items
|
||||
|
||||
if (order.swaps?.length) {
|
||||
for (const swap of order.swaps) {
|
||||
swap.additional_items.forEach((it) => {
|
||||
if (
|
||||
it.shipped_quantity ||
|
||||
it.shipped_quantity === it.fulfilled_quantity
|
||||
) {
|
||||
allOrderItems.push(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (order.claims?.length) {
|
||||
for (const claim of order.claims) {
|
||||
claim.additional_items.forEach((it) => {
|
||||
if (
|
||||
it.shipped_quantity ||
|
||||
it.shipped_quantity === it.fulfilled_quantity
|
||||
) {
|
||||
allOrderItems.push(it)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const orderItem = allOrderItems.find((oi) => oi.id === ci.item_id)
|
||||
return {
|
||||
...orderItem,
|
||||
quantity: ci.quantity,
|
||||
}
|
||||
})
|
||||
toRefund = await this.totalsService_.getRefundTotal(
|
||||
order,
|
||||
lines as LineItem[]
|
||||
)
|
||||
}
|
||||
|
||||
let newItems: LineItem[] = []
|
||||
if (typeof additional_items !== "undefined") {
|
||||
for (const item of additional_items) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(transactionManager)
|
||||
.confirmInventory(item.variant_id, item.quantity)
|
||||
}
|
||||
|
||||
newItems = await Promise.all(
|
||||
additional_items.map((i) =>
|
||||
this.lineItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.generate(i.variant_id, order.region_id, i.quantity)
|
||||
)
|
||||
)
|
||||
|
||||
for (const newItem of newItems) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(transactionManager)
|
||||
.adjustInventory(newItem.variant_id, -newItem.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
const evaluatedNoNotification =
|
||||
no_notification !== undefined
|
||||
? no_notification
|
||||
: order.no_notification
|
||||
|
||||
const created = claimRepo.create({
|
||||
shipping_address_id: addressId,
|
||||
payment_status: type === ClaimType.REFUND ? "not_refunded" : "na",
|
||||
refund_amount: toRefund,
|
||||
type,
|
||||
additional_items: newItems,
|
||||
order_id: order.id,
|
||||
no_notification: evaluatedNoNotification,
|
||||
...rest,
|
||||
} as DeepPartial<ClaimOrder>)
|
||||
|
||||
const result: ClaimOrder = await claimRepo.save(created)
|
||||
|
||||
if (result.additional_items && result.additional_items.length) {
|
||||
const calcContext = this.totalsService_.getCalculationContext(order)
|
||||
const lineItems = await this.lineItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.list({
|
||||
id: result.additional_items.map((i) => i.id),
|
||||
})
|
||||
await this.taxProviderService_
|
||||
.withTransaction(transactionManager)
|
||||
.createTaxLines(lineItems, calcContext)
|
||||
}
|
||||
|
||||
if (shipping_methods) {
|
||||
for (const method of shipping_methods) {
|
||||
if (method.id) {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.updateShippingMethod(method.id, {
|
||||
claim_order_id: result.id,
|
||||
})
|
||||
} else {
|
||||
await this.shippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.createShippingMethod(
|
||||
method.option_id as string,
|
||||
(method as any).data,
|
||||
{
|
||||
claim_order_id: result.id,
|
||||
price: method.price,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const ci of claim_items) {
|
||||
await this.claimItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.create({
|
||||
...ci,
|
||||
claim_order_id: result.id,
|
||||
})
|
||||
}
|
||||
|
||||
if (return_shipping) {
|
||||
await this.returnService_.withTransaction(transactionManager).create({
|
||||
order_id: order.id,
|
||||
claim_order_id: result.id,
|
||||
items: claim_items.map((ci) => ({
|
||||
item_id: ci.item_id,
|
||||
quantity: ci.quantity,
|
||||
metadata: (ci as any).metadata,
|
||||
})),
|
||||
shipping_method: return_shipping,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
}
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(ClaimService.Events.CREATED, {
|
||||
id: result.id,
|
||||
no_notification: result.no_notification,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id - the object containing all data required to create a claim
|
||||
* @param config - config object
|
||||
* @param config.metadata - config metadata
|
||||
* @param config.no_notification - config no notification
|
||||
* @return created claim
|
||||
*/
|
||||
async createFulfillment(
|
||||
id: string,
|
||||
config: {
|
||||
metadata?: Record<string, unknown>
|
||||
no_notification?: boolean
|
||||
} = {
|
||||
metadata: {},
|
||||
}
|
||||
): Promise<ClaimOrder> {
|
||||
const { metadata, no_notification } = config
|
||||
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: [
|
||||
"additional_items",
|
||||
"additional_items.tax_lines",
|
||||
"shipping_methods",
|
||||
"shipping_methods.tax_lines",
|
||||
"shipping_address",
|
||||
"order",
|
||||
"order.billing_address",
|
||||
"order.discounts",
|
||||
"order.discounts.rule",
|
||||
"order.payments",
|
||||
],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be fulfilled"
|
||||
)
|
||||
}
|
||||
|
||||
const order = claim.order
|
||||
|
||||
if (
|
||||
claim.fulfillment_status !== "not_fulfilled" &&
|
||||
claim.fulfillment_status !== "canceled"
|
||||
) {
|
||||
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 evaluatedNoNotification =
|
||||
no_notification !== undefined
|
||||
? no_notification
|
||||
: claim.no_notification
|
||||
|
||||
const fulfillments = await this.fulfillmentService_
|
||||
.withTransaction(transactionManager)
|
||||
.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,
|
||||
no_notification: evaluatedNoNotification,
|
||||
},
|
||||
claim.additional_items.map((i) => ({
|
||||
item_id: i.id,
|
||||
quantity: i.quantity,
|
||||
})),
|
||||
{ claim_order_id: id, metadata }
|
||||
)
|
||||
|
||||
let successfullyFulfilledItems: FulfillmentItem[] = []
|
||||
for (const fulfillment of fulfillments) {
|
||||
successfullyFulfilledItems = successfullyFulfilledItems.concat(
|
||||
fulfillment.items
|
||||
)
|
||||
}
|
||||
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.FULFILLED
|
||||
|
||||
for (const item of claim.additional_items) {
|
||||
const fulfillmentItem = successfullyFulfilledItems.find(
|
||||
(successfullyFulfilledItem) => {
|
||||
return successfullyFulfilledItem.item_id === item.id
|
||||
}
|
||||
)
|
||||
|
||||
if (fulfillmentItem) {
|
||||
const fulfilledQuantity =
|
||||
(item.fulfilled_quantity || 0) + fulfillmentItem.quantity
|
||||
|
||||
// Update the fulfilled quantity
|
||||
await this.lineItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.update(item.id, {
|
||||
fulfilled_quantity: fulfilledQuantity,
|
||||
})
|
||||
|
||||
if (item.quantity !== fulfilledQuantity) {
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.REQUIRES_ACTION
|
||||
}
|
||||
} else if (item.quantity !== item.fulfilled_quantity) {
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.REQUIRES_ACTION
|
||||
}
|
||||
}
|
||||
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
const claimOrder = await claimRepo.save(claim)
|
||||
|
||||
for (const fulfillment of fulfillments) {
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(ClaimService.Events.FULFILLMENT_CREATED, {
|
||||
id: id,
|
||||
fulfillment_id: fulfillment.id,
|
||||
no_notification: claim.no_notification,
|
||||
})
|
||||
}
|
||||
|
||||
return claimOrder
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async cancelFulfillment(fulfillmentId: string): Promise<ClaimOrder> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const canceled = await this.fulfillmentService_
|
||||
.withTransaction(transactionManager)
|
||||
.cancelFulfillment(fulfillmentId)
|
||||
|
||||
if (!canceled.claim_order_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
`Fufillment not related to a claim`
|
||||
)
|
||||
}
|
||||
|
||||
const claim = await this.retrieve(canceled.claim_order_id)
|
||||
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.CANCELED
|
||||
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
return claimRepo.save(claim)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async processRefund(id: string): Promise<ClaimOrder> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["order", "order.payments"],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be processed"
|
||||
)
|
||||
}
|
||||
|
||||
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(transactionManager)
|
||||
.refundPayment(claim.order.payments, claim.refund_amount, "claim")
|
||||
}
|
||||
|
||||
claim.payment_status = ClaimPaymentStatus.REFUNDED
|
||||
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
const claimOrder = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(ClaimService.Events.REFUND_PROCESSED, {
|
||||
id,
|
||||
no_notification: claimOrder.no_notification,
|
||||
})
|
||||
|
||||
return claimOrder
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async createShipment(
|
||||
id: string,
|
||||
fulfillmentId: string,
|
||||
trackingLinks: { tracking_number: string }[] = [],
|
||||
config = {
|
||||
metadata: {},
|
||||
no_notification: undefined,
|
||||
}
|
||||
): Promise<ClaimOrder> {
|
||||
const { metadata, no_notification } = config
|
||||
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["additional_items"],
|
||||
})
|
||||
|
||||
if (claim.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Canceled claim cannot be fulfilled as shipped"
|
||||
)
|
||||
}
|
||||
const evaluatedNoNotification =
|
||||
no_notification !== undefined
|
||||
? no_notification
|
||||
: claim.no_notification
|
||||
|
||||
const shipment = await this.fulfillmentService_
|
||||
.withTransaction(transactionManager)
|
||||
.createShipment(fulfillmentId, trackingLinks, {
|
||||
metadata,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.SHIPPED
|
||||
|
||||
for (const additionalItem of claim.additional_items) {
|
||||
const shipped = shipment.items.find(
|
||||
(si) => si.item_id === additionalItem.id
|
||||
)
|
||||
if (shipped) {
|
||||
const shippedQty =
|
||||
(additionalItem.shipped_quantity || 0) + shipped.quantity
|
||||
await this.lineItemService_
|
||||
.withTransaction(transactionManager)
|
||||
.update(additionalItem.id, {
|
||||
shipped_quantity: shippedQty,
|
||||
})
|
||||
|
||||
if (shippedQty !== additionalItem.quantity) {
|
||||
claim.fulfillment_status =
|
||||
ClaimFulfillmentStatus.PARTIALLY_SHIPPED
|
||||
}
|
||||
} else if (
|
||||
additionalItem.shipped_quantity !== additionalItem.quantity
|
||||
) {
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.PARTIALLY_SHIPPED
|
||||
}
|
||||
}
|
||||
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
const claimOrder = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(ClaimService.Events.SHIPMENT_CREATED, {
|
||||
id,
|
||||
fulfillment_id: shipment.id,
|
||||
no_notification: evaluatedNoNotification,
|
||||
})
|
||||
|
||||
return claimOrder
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async cancel(id: string): Promise<ClaimOrder> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claim = await this.retrieve(id, {
|
||||
relations: ["return_order", "fulfillments", "order", "order.refunds"],
|
||||
})
|
||||
if (claim.refund_amount) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Claim with a refund cannot be canceled"
|
||||
)
|
||||
}
|
||||
|
||||
if (claim.fulfillments) {
|
||||
for (const f of claim.fulfillments) {
|
||||
if (!f.canceled_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"All fulfillments must be canceled before the claim can be canceled"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (claim.return_order && claim.return_order.status !== "canceled") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Return must be canceled before the claim can be canceled"
|
||||
)
|
||||
}
|
||||
|
||||
claim.fulfillment_status = ClaimFulfillmentStatus.CANCELED
|
||||
claim.canceled_at = new Date()
|
||||
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
const claimOrder = await claimRepo.save(claim)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(transactionManager)
|
||||
.emit(ClaimService.Events.CANCELED, {
|
||||
id: claimOrder.id,
|
||||
no_notification: claimOrder.no_notification,
|
||||
})
|
||||
|
||||
return claimOrder
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param selector - the query object for find
|
||||
* @param config - the config object containing query settings
|
||||
* @return the result of the find operation
|
||||
*/
|
||||
async list(
|
||||
selector,
|
||||
config: FindConfig<ClaimOrder> = {
|
||||
skip: 0,
|
||||
take: 50,
|
||||
order: { created_at: "DESC" },
|
||||
}
|
||||
): Promise<ClaimOrder[]> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
const query = buildQuery<ClaimOrder>(selector, config)
|
||||
return await claimRepo.find(query)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an order by id.
|
||||
* @param id - id of the claim order to retrieve
|
||||
* @param config - the config object containing query settings
|
||||
* @return the order document
|
||||
*/
|
||||
async retrieve(
|
||||
id: string,
|
||||
config: FindConfig<ClaimOrder> = {}
|
||||
): Promise<ClaimOrder> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const claimRepo = transactionManager.getCustomRepository(
|
||||
this.claimRepository_
|
||||
)
|
||||
|
||||
const query = buildQuery<ClaimOrder>({ id }, config)
|
||||
const claim = await claimRepo.findOne(query)
|
||||
|
||||
if (!claim) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Claim with ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return claim
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { ClaimType, Order } from "../models"
|
||||
import { AddressPayload } from "./common"
|
||||
|
||||
export type ClaimTypeValue = `${ClaimType}`
|
||||
|
||||
export enum ClaimItemReason {
|
||||
missing_item = "missing_item",
|
||||
wrong_item = "wrong_item",
|
||||
production_failure = "production_failure",
|
||||
other = "other",
|
||||
}
|
||||
|
||||
export type ClaimItemReasonValue = `${ClaimItemReason}`
|
||||
|
||||
/* CREATE INPUT */
|
||||
|
||||
export type CreateClaimInput = {
|
||||
type: ClaimTypeValue
|
||||
claim_items: CreateClaimItemInput[]
|
||||
return_shipping?: CreateClaimReturnShippingInput
|
||||
additional_items?: CreateClaimItemAdditionalItemInput[]
|
||||
shipping_methods?: CreateClaimShippingMethodInput[]
|
||||
refund_amount?: number
|
||||
shipping_address?: AddressPayload
|
||||
no_notification?: boolean
|
||||
metadata?: object
|
||||
order: Order
|
||||
claim_order_id?: string
|
||||
shipping_address_id?: string
|
||||
}
|
||||
|
||||
type CreateClaimReturnShippingInput = {
|
||||
option_id?: string
|
||||
price?: number
|
||||
}
|
||||
|
||||
type CreateClaimShippingMethodInput = {
|
||||
id?: string
|
||||
option_id?: string
|
||||
price?: number
|
||||
}
|
||||
|
||||
type CreateClaimItemInput = {
|
||||
item_id: string
|
||||
quantity: number
|
||||
note?: string
|
||||
reason?: ClaimItemReasonValue
|
||||
tags?: string[]
|
||||
images?: string[]
|
||||
}
|
||||
|
||||
type CreateClaimItemAdditionalItemInput = {
|
||||
variant_id: string
|
||||
quantity: number
|
||||
}
|
||||
|
||||
/* UPDATE INPUT */
|
||||
|
||||
export type UpdateClaimInput = {
|
||||
claim_items?: UpdateClaimItemInput[]
|
||||
shipping_methods?: UpdateClaimShippingMethodInput[]
|
||||
no_notification?: boolean
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type UpdateClaimShippingMethodInput = {
|
||||
id?: string
|
||||
option_id?: string
|
||||
price?: number
|
||||
}
|
||||
|
||||
type UpdateClaimItemInput = {
|
||||
id: string
|
||||
note?: string
|
||||
reason?: string
|
||||
images: UpdateClaimItemImageInput[]
|
||||
tags: UpdateClaimItemTagInput[]
|
||||
metadata?: object
|
||||
}
|
||||
|
||||
type UpdateClaimItemImageInput = {
|
||||
id?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
type UpdateClaimItemTagInput = {
|
||||
id?: string
|
||||
value?: string
|
||||
}
|
||||
Reference in New Issue
Block a user