add initital work on draft order
This commit is contained in:
155
packages/medusa/src/models/draft-order.ts
Normal file
155
packages/medusa/src/models/draft-order.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
Entity,
|
||||
Generated,
|
||||
BeforeInsert,
|
||||
Index,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
PrimaryColumn,
|
||||
OneToOne,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
OneToMany,
|
||||
} from "typeorm"
|
||||
import { ulid } from "ulid"
|
||||
import { Address } from "./address"
|
||||
|
||||
import { Cart } from "./cart"
|
||||
import { Customer } from "./customer"
|
||||
import { Discount } from "./discount"
|
||||
import { LineItem } from "./line-item"
|
||||
import { Order } from "./order"
|
||||
import { Payment } from "./payment"
|
||||
import { Region } from "./region"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
|
||||
enum DraftOrderStatus {
|
||||
OPEN = "open",
|
||||
AWAITING = "awaiting",
|
||||
COMPLETED = "completed",
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export class DraftOrder {
|
||||
@PrimaryColumn()
|
||||
id: string
|
||||
|
||||
@Column({ type: "enum", enum: DraftOrderStatus, default: "open" })
|
||||
status: DraftOrderStatus
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
@Generated("increment")
|
||||
display_id: number
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
cart_id: string
|
||||
|
||||
@OneToOne(() => Cart)
|
||||
@JoinColumn({ name: "cart_id" })
|
||||
cart: Cart
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
order_id: string
|
||||
|
||||
@OneToOne(() => Order)
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
customer_id: string
|
||||
|
||||
@ManyToOne(() => Customer, { cascade: ["insert"] })
|
||||
@JoinColumn({ name: "customer_id" })
|
||||
customer: Customer
|
||||
|
||||
@OneToMany(
|
||||
() => LineItem,
|
||||
lineItem => lineItem.draft_order,
|
||||
{ cascade: ["insert", "remove"] }
|
||||
)
|
||||
items: LineItem[]
|
||||
|
||||
@Column()
|
||||
email: string
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
billing_address_id: string
|
||||
|
||||
@ManyToOne(() => Address, { cascade: ["insert"] })
|
||||
@JoinColumn({ name: "billing_address_id" })
|
||||
billing_address: Address
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
shipping_address_id: string
|
||||
|
||||
@ManyToOne(() => Address, { cascade: ["insert"] })
|
||||
@JoinColumn({ name: "shipping_address_id" })
|
||||
shipping_address: Address
|
||||
|
||||
@Index()
|
||||
@Column()
|
||||
region_id: string
|
||||
|
||||
@ManyToOne(() => Region)
|
||||
@JoinColumn({ name: "region_id" })
|
||||
region: Region
|
||||
|
||||
@ManyToMany(() => Discount, { cascade: ["insert"] })
|
||||
@JoinTable({
|
||||
name: "draft_order_discounts",
|
||||
joinColumn: {
|
||||
name: "draft_order_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "discount_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
})
|
||||
discounts: Discount[]
|
||||
|
||||
@OneToMany(
|
||||
() => ShippingMethod,
|
||||
m => m.draft_order,
|
||||
{ cascade: ["soft-remove", "remove"] }
|
||||
)
|
||||
shipping_methods: ShippingMethod[]
|
||||
|
||||
@OneToMany(
|
||||
() => Payment,
|
||||
p => p.draft_order,
|
||||
{ cascade: ["insert"] }
|
||||
)
|
||||
payments: Payment[]
|
||||
|
||||
@Column({ nullable: true, type: "timestamptz" })
|
||||
canceled_at: Date
|
||||
|
||||
@CreateDateColumn({ type: "timestamptz" })
|
||||
created_at: Date
|
||||
|
||||
@UpdateDateColumn({ type: "timestamptz" })
|
||||
updated_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 = `dorder_${id}`
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { Cart } from "./cart"
|
||||
import { Order } from "./order"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { ProductVariant } from "./product-variant"
|
||||
import { DraftOrder } from "./draft-order"
|
||||
|
||||
@Check(`"fulfilled_quantity" <= "quantity"`)
|
||||
@Check(`"shipped_quantity" <= "fulfilled_quantity"`)
|
||||
@@ -48,6 +49,17 @@ export class LineItem {
|
||||
)
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
draft_order_id: string
|
||||
|
||||
@ManyToOne(
|
||||
() => DraftOrder,
|
||||
dorder => dorder.items
|
||||
)
|
||||
@JoinColumn({ name: "draft_order_id" })
|
||||
draft_order: DraftOrder
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Refund } from "./refund"
|
||||
import { Swap } from "./swap"
|
||||
import { ClaimOrder } from "./claim-order"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
import { DraftOrder } from "./draft-order"
|
||||
|
||||
export enum OrderStatus {
|
||||
PENDING = "pending",
|
||||
@@ -212,6 +213,13 @@ export class Order {
|
||||
)
|
||||
swaps: Swap[]
|
||||
|
||||
@Column({ nullable: true })
|
||||
draft_order_id: string
|
||||
|
||||
@OneToOne(() => DraftOrder)
|
||||
@JoinColumn({ name: "draft_order_id" })
|
||||
draft_order: DraftOrder
|
||||
|
||||
@OneToMany(
|
||||
() => LineItem,
|
||||
lineItem => lineItem.order,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Swap } from "./swap"
|
||||
import { Currency } from "./currency"
|
||||
import { Cart } from "./cart"
|
||||
import { Order } from "./order"
|
||||
import { DraftOrder } from "./draft-order"
|
||||
|
||||
@Entity()
|
||||
export class Payment {
|
||||
@@ -49,6 +50,14 @@ export class Payment {
|
||||
@JoinColumn({ name: "order_id" })
|
||||
order: Order
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
draft_order_id: string
|
||||
|
||||
@OneToOne(() => DraftOrder)
|
||||
@JoinColumn({ name: "draft_order_id" })
|
||||
draft_order: DraftOrder
|
||||
|
||||
@Column({ type: "int" })
|
||||
amount: number
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Cart } from "./cart"
|
||||
import { Swap } from "./swap"
|
||||
import { Return } from "./return"
|
||||
import { ShippingOption } from "./shipping-option"
|
||||
import { DraftOrder } from "./draft-order"
|
||||
|
||||
@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`
|
||||
@@ -67,6 +68,14 @@ export class ShippingMethod {
|
||||
@Column({ nullable: true })
|
||||
return_id: string
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: true })
|
||||
draft_order_id: string
|
||||
|
||||
@ManyToOne(() => DraftOrder)
|
||||
@JoinColumn({ name: "draft_order_id" })
|
||||
draft_order: DraftOrder
|
||||
|
||||
@OneToOne(
|
||||
() => Return,
|
||||
ret => ret.shipping_method
|
||||
|
||||
5
packages/medusa/src/repositories/draft-order.ts
Normal file
5
packages/medusa/src/repositories/draft-order.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EntityRepository, Repository } from "typeorm"
|
||||
import { DraftOrder } from "../models/draft-order"
|
||||
|
||||
@EntityRepository(DraftOrder)
|
||||
export class DraftOrderRepository extends Repository<DraftOrder> {}
|
||||
361
packages/medusa/src/services/draft-order.js
Normal file
361
packages/medusa/src/services/draft-order.js
Normal file
@@ -0,0 +1,361 @@
|
||||
import _ from "lodash"
|
||||
import { BaseService } from "medusa-interfaces"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
|
||||
/**
|
||||
* Handles swaps
|
||||
* @implements BaseService
|
||||
*/
|
||||
class DraftOrderService extends BaseService {
|
||||
static Events = {
|
||||
CREATED: "draft_order.created",
|
||||
}
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
draftOrderRepository,
|
||||
eventBusService,
|
||||
addressRepository,
|
||||
cartService,
|
||||
totalsService,
|
||||
lineItemService,
|
||||
productVariantService,
|
||||
shippingOptionService,
|
||||
regionService,
|
||||
}) {
|
||||
super()
|
||||
|
||||
/** @private @const {EntityManager} */
|
||||
this.manager_ = manager
|
||||
|
||||
/** @private @const {SwapModel} */
|
||||
this.draftOrderRepository_ = draftOrderRepository
|
||||
|
||||
/** @private @const {TotalsService} */
|
||||
this.totalsService_ = totalsService
|
||||
|
||||
/** @private @const {AddressRepository} */
|
||||
this.addressRepository_ = addressRepository
|
||||
|
||||
/** @private @const {LineItemService} */
|
||||
this.lineItemService_ = lineItemService
|
||||
|
||||
/** @private @const {ReturnService} */
|
||||
this.returnService_ = returnService
|
||||
|
||||
/** @private @const {CartService} */
|
||||
this.cartService_ = cartService
|
||||
|
||||
/** @private @const {RegionService} */
|
||||
this.regionService_ = regionService
|
||||
|
||||
/** @private @const {ProductVariantService} */
|
||||
this.productVariantService_ = productVariantService
|
||||
|
||||
/** @private @const {ShippingOptionService} */
|
||||
this.shippingOptionService_ = shippingOptionService
|
||||
|
||||
/** @private @const {EventBusService} */
|
||||
this.eventBus_ = eventBusService
|
||||
}
|
||||
|
||||
withTransaction(transactionManager) {
|
||||
if (!transactionManager) {
|
||||
return this
|
||||
}
|
||||
|
||||
const cloned = new DraftOrderService({
|
||||
manager: transactionManager,
|
||||
draftOrderRepository: this.draftOrderRepository_,
|
||||
eventBusService: this.eventBus_,
|
||||
cartService: this.cartService_,
|
||||
totalsService: this.totalsService_,
|
||||
productVariantService: this.productVariantService_,
|
||||
lineItemService: this.lineItemService_,
|
||||
})
|
||||
|
||||
cloned.transactionManager_ = transactionManager
|
||||
|
||||
return cloned
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a draft order with the given id.
|
||||
* @param {string} id - id of the draft order to retrieve
|
||||
* @return {Promise<DraftOrder>} the draft order
|
||||
*/
|
||||
async retrieve(id, config = {}) {
|
||||
const draftOrderRepo = this.manager_.getCustomRepository(
|
||||
this.draftOrderRepository_
|
||||
)
|
||||
|
||||
const validatedId = this.validateId_(id)
|
||||
|
||||
const query = this.buildQuery_({ id: validatedId }, config)
|
||||
|
||||
const draftOrder = await draftOrderRepo.findOne(query)
|
||||
|
||||
if (!draftOrder) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Draft order with id: ${id} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return draftOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a draft order based on its associated cart id
|
||||
* @param {string} cartId - cart id that the draft orders's cart has
|
||||
* @return {Promise<DraftOrder>} the draft order
|
||||
*/
|
||||
async retrieveByCartId(cartId, relations = []) {
|
||||
const draftOrderRepo = this.manager_.getCustomRepository(
|
||||
this.draftOrderRepository_
|
||||
)
|
||||
|
||||
const draftOrder = await draftOrderRepo.findOne({
|
||||
where: {
|
||||
cart_id: cartId,
|
||||
},
|
||||
relations,
|
||||
})
|
||||
|
||||
if (!draftOrder) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Draft order was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return draftOrder
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists draft orders
|
||||
* @param {Object} selector - query object for find
|
||||
* @param {Object} config - configurable attributes for find
|
||||
* @return {Promise<Array<DraftOrder>>} list of draft orders
|
||||
*/
|
||||
async list(
|
||||
selector,
|
||||
config = { skip: 0, take: 50, order: { created_at: "DESC" } }
|
||||
) {
|
||||
const draftOrderRepo = this.manager_.getCustomRepository(
|
||||
this.draftOrderRepository_
|
||||
)
|
||||
|
||||
const query = this.buildQuery_(selector, config)
|
||||
|
||||
return draftOrderRepo.find(query)
|
||||
}
|
||||
|
||||
async setAddress_(region, address) {
|
||||
const addressRepo = this.manager_.getCustomRepository(
|
||||
this.addressRepository_
|
||||
)
|
||||
|
||||
const regCountries = region.countries.map(({ iso_2 }) => iso_2)
|
||||
|
||||
if (!address.country_code) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Address is missing country code`
|
||||
)
|
||||
}
|
||||
|
||||
if (!regCountries.includes(address.country_code)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Country ${address.country_code} is not in region ${region.name}`
|
||||
)
|
||||
}
|
||||
|
||||
const created = addressRepo.create(adress)
|
||||
return created
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms if the contents of a line item is covered by the inventory.
|
||||
* To be covered a variant must either not have its inventory managed or it
|
||||
* must allow backorders or it must have enough inventory to cover the request.
|
||||
* If the content is made up of multiple variants it will return true if all
|
||||
* variants can be covered. If the content consists of a single variant it will
|
||||
* return true if the variant is covered.
|
||||
* @param {(LineItemContent | LineItemContentArray)} - the content of the line
|
||||
* item
|
||||
* @param {number} - the quantity of the line item
|
||||
* @return {boolean} true if the inventory covers the line item.
|
||||
*/
|
||||
async confirmInventory_(variantId, quantity) {
|
||||
// If the line item is not stock tracked we don't have double check it
|
||||
if (!variantId) {
|
||||
return true
|
||||
}
|
||||
|
||||
return this.productVariantService_.canCoverQuantity(variantId, quantity)
|
||||
}
|
||||
|
||||
async addLineItem(doId, lineItem) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const draftOrder = await this.retrieve(doId, {
|
||||
relations: [
|
||||
"shipping_methods",
|
||||
"items",
|
||||
"payment_sessions",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
],
|
||||
})
|
||||
|
||||
if (draftOrder.status !== "open") {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"You are not allowed to add items to a draft order with status awaiting or completed"
|
||||
)
|
||||
}
|
||||
|
||||
let currentItem
|
||||
if (lineItem.should_merge) {
|
||||
currentItem = draftOrder.items.find(line => {
|
||||
if (line.should_merge && line.variant_id === lineItem.variant_id) {
|
||||
return _.isEqual(line.metadata, lineItem.metadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// If content matches one of the line items currently in the cart we can
|
||||
// simply update the quantity of the existing line item
|
||||
if (currentItem) {
|
||||
const newQuantity = currentItem.quantity + lineItem.quantity
|
||||
|
||||
// Confirm inventory
|
||||
const hasInventory = await this.confirmInventory_(
|
||||
lineItem.variant_id,
|
||||
newQuantity
|
||||
)
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory doesn't cover the desired quantity"
|
||||
)
|
||||
}
|
||||
|
||||
await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.update(currentItem.id, {
|
||||
quantity: newQuantity,
|
||||
})
|
||||
} else {
|
||||
// Confirm inventory
|
||||
const hasInventory = await this.confirmInventory_(
|
||||
lineItem.variant_id,
|
||||
lineItem.quantity
|
||||
)
|
||||
|
||||
if (!hasInventory) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Inventory doesn't cover the desired quantity"
|
||||
)
|
||||
}
|
||||
|
||||
await this.lineItemService_.withTransaction(manager).create({
|
||||
...lineItem,
|
||||
has_shipping: false,
|
||||
cart_id: cartId,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a draft order.
|
||||
* @param {Object} data - data to create draft order from
|
||||
* @return {Promise<DraftOrder>} the created draft order
|
||||
*/
|
||||
async create(data) {
|
||||
return this.atomicPhase_(async manager => {
|
||||
const draftOrderRepo = manager.getCustomRepository(
|
||||
this.draftOrderRepository_
|
||||
)
|
||||
|
||||
if (!data.region_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`region_id is required to create a draft order`
|
||||
)
|
||||
}
|
||||
|
||||
const region = await this.regionService_
|
||||
.withTransaction(manager)
|
||||
.retrieve(data.region_id, {
|
||||
relations: ["countries"],
|
||||
})
|
||||
|
||||
if (!data.shipping_address && !data.shipping_address_id) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping addresss is required to create a draft order`
|
||||
)
|
||||
}
|
||||
|
||||
if (data.shipping_address && !data.shipping_address_id) {
|
||||
data.shipping_address = await this.setAddress_(
|
||||
region,
|
||||
data.shipping_address
|
||||
)
|
||||
}
|
||||
|
||||
if (data.billing_address && !data.billing_address_id) {
|
||||
data.billing_address = await this.setAddress_(
|
||||
region,
|
||||
data.billing_address
|
||||
)
|
||||
}
|
||||
|
||||
const { items, shipping_methods, ...rest } = data
|
||||
|
||||
const draftOrder = draftOrderRepo.create(rest)
|
||||
const result = await draftOrderRepo.save(draftOrder)
|
||||
|
||||
await this.eventBus_
|
||||
.withTransaction(manager)
|
||||
.emit(DraftOrderService.Events.CREATED, {
|
||||
id: result.id,
|
||||
})
|
||||
|
||||
let shippingMethods = []
|
||||
for (const method of shipping_methods) {
|
||||
const m = await this.shippingOptionService_
|
||||
.withTransaction(manager)
|
||||
.createShippingMethod(method.option_id, method.data, {
|
||||
draft_order_id: draftOrder.id,
|
||||
})
|
||||
|
||||
shippingMethods.push(m)
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const line = await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.generate(
|
||||
item.variant_id,
|
||||
cart.region_id,
|
||||
item.quantity,
|
||||
item.metadata
|
||||
)
|
||||
|
||||
await this.lineItemService_
|
||||
.withTransaction(manager)
|
||||
.create({ draft_order_id: draftOrder.id, ...line })
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default DraftOrderService
|
||||
@@ -254,6 +254,10 @@ class ShippingOptionService extends BaseService {
|
||||
toCreate.claim_order_id = config.claim_order_id
|
||||
}
|
||||
|
||||
if (config.draft_order_id) {
|
||||
toCreate.draft_order_id = config.draft_order_id
|
||||
}
|
||||
|
||||
const method = await methodRepo.create(toCreate)
|
||||
|
||||
const created = await methodRepo.save(method)
|
||||
|
||||
Reference in New Issue
Block a user