feat: Allow backorder on swaps (#404)
This commit is contained in:
@@ -48,6 +48,9 @@ import { defaultFields, defaultRelations } from "./"
|
||||
* no_notification:
|
||||
* description: If set to true no notification will be send related to this Swap.
|
||||
* type: boolean
|
||||
* allow_backorder:
|
||||
* description: If true, swaps can be completed with items out of stock
|
||||
* type: boolean
|
||||
* tags:
|
||||
* - Order
|
||||
* responses:
|
||||
@@ -83,6 +86,7 @@ export default async (req, res) => {
|
||||
quantity: Validator.number().required(),
|
||||
}),
|
||||
no_notification: Validator.boolean().optional(),
|
||||
allow_backorder: Validator.boolean().default(true),
|
||||
})
|
||||
|
||||
const { value, error } = schema.validate(req.body)
|
||||
@@ -141,6 +145,7 @@ export default async (req, res) => {
|
||||
{
|
||||
idempotency_key: idempotencyKey.idempotency_key,
|
||||
no_notification: value.no_notification,
|
||||
allow_backorder: value.allow_backorder,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -138,18 +138,36 @@ export default async (req, res) => {
|
||||
// If cart is part of swap, we register swap as complete
|
||||
switch (cart.type) {
|
||||
case "swap": {
|
||||
const swapId = cart.metadata?.swap_id
|
||||
let swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.registerCartCompletion(swapId)
|
||||
try {
|
||||
const swapId = cart.metadata?.swap_id
|
||||
let swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.registerCartCompletion(swapId)
|
||||
|
||||
swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.retrieve(swap.id, { relations: ["shipping_address"] })
|
||||
swap = await swapService
|
||||
.withTransaction(manager)
|
||||
.retrieve(swap.id, { relations: ["shipping_address"] })
|
||||
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { data: swap, type: "swap" },
|
||||
return {
|
||||
response_code: 200,
|
||||
response_body: { data: swap, type: "swap" },
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
) {
|
||||
return {
|
||||
response_code: 409,
|
||||
response_body: {
|
||||
message: error.message,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
// case "payment_link":
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import {MigrationInterface, QueryRunner} from "typeorm";
|
||||
|
||||
export class allowBackorderSwaps1630505790603 implements MigrationInterface {
|
||||
name = 'allowBackorderSwaps1630505790603'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "swap" ADD "allow_backorder" boolean NOT NULL DEFAULT false`);
|
||||
await queryRunner.query(`ALTER TABLE "cart" ADD "payment_authorized_at" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`ALTER TYPE "swap_payment_status_enum" RENAME TO "swap_payment_status_enum_old"`);
|
||||
await queryRunner.query(`CREATE TYPE "swap_payment_status_enum" AS ENUM('not_paid', 'awaiting', 'captured', 'confirmed', 'canceled', 'difference_refunded', 'partially_refunded', 'refunded', 'requires_action')`);
|
||||
await queryRunner.query(`ALTER TABLE "swap" ALTER COLUMN "payment_status" TYPE "swap_payment_status_enum" USING "payment_status"::"text"::"swap_payment_status_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "swap_payment_status_enum_old"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TYPE "swap_payment_status_enum_old" AS ENUM('not_paid', 'awaiting', 'captured', 'canceled', 'difference_refunded', 'partially_refunded', 'refunded', 'requires_action')`);
|
||||
await queryRunner.query(`ALTER TABLE "swap" ALTER COLUMN "payment_status" TYPE "swap_payment_status_enum_old" USING "payment_status"::"text"::"swap_payment_status_enum_old"`);
|
||||
await queryRunner.query(`DROP TYPE "swap_payment_status_enum"`);
|
||||
await queryRunner.query(`ALTER TYPE "swap_payment_status_enum_old" RENAME TO "swap_payment_status_enum"`);
|
||||
await queryRunner.query(`ALTER TABLE "cart" DROP COLUMN "payment_authorized_at"`);
|
||||
await queryRunner.query(`ALTER TABLE "swap" DROP COLUMN "allow_backorder"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -230,6 +230,9 @@ export class Cart {
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
completed_at: Date
|
||||
|
||||
@Column({ type: resolveDbType("timestamptz"), nullable: true })
|
||||
payment_authorized_at: Date
|
||||
|
||||
@CreateDateColumn({ type: resolveDbType("timestamptz") })
|
||||
created_at: Date
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export enum PaymentStatus {
|
||||
NOT_PAID = "not_paid",
|
||||
AWAITING = "awaiting",
|
||||
CAPTURED = "captured",
|
||||
CONFIRMED = "confirmed",
|
||||
CANCELED = "canceled",
|
||||
DIFFERENCE_REFUNDED = "difference_refunded",
|
||||
PARTIALLY_REFUNDED = "partially_refunded",
|
||||
@@ -137,6 +138,9 @@ export class Swap {
|
||||
@Column({ type: "boolean", nullable: true })
|
||||
no_notification: Boolean
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
allow_backorder: Boolean
|
||||
|
||||
@DbAwareColumn({ type: "jsonb", nullable: true })
|
||||
metadata: any
|
||||
|
||||
@@ -219,6 +223,9 @@ export class Swap {
|
||||
* cart_id:
|
||||
* description: "The id of the Cart that the Customer will use to confirm the Swap."
|
||||
* type: string
|
||||
* allow_backorder:
|
||||
* description: "If true, swaps can be completed with items out of stock"
|
||||
* type: boolean
|
||||
* confirmed_at:
|
||||
* description: "The date with timezone at which the Swap was confirmed by the Customer."
|
||||
* type: string
|
||||
|
||||
@@ -203,6 +203,7 @@ describe("OrderService", () => {
|
||||
}
|
||||
|
||||
orderService.cartService_.retrieve = jest.fn(() => Promise.resolve(cart))
|
||||
orderService.cartService_.update = jest.fn(() => Promise.resolve())
|
||||
|
||||
await orderService.createFromCart("cart_id")
|
||||
const order = {
|
||||
@@ -305,6 +306,7 @@ describe("OrderService", () => {
|
||||
orderService.cartService_.retrieve = () => {
|
||||
return Promise.resolve(cart)
|
||||
}
|
||||
orderService.cartService_.update = () => Promise.resolve()
|
||||
|
||||
await orderService.createFromCart("cart_id")
|
||||
const order = {
|
||||
@@ -453,6 +455,7 @@ describe("OrderService", () => {
|
||||
total: 100,
|
||||
}
|
||||
orderService.cartService_.retrieve = () => Promise.resolve(cart)
|
||||
orderService.cartService_.update = () => Promise.resolve()
|
||||
const res = orderService.createFromCart(cart)
|
||||
await expect(res).rejects.toThrow(
|
||||
"Variant with id: variant-1 does not have the required inventory"
|
||||
|
||||
@@ -741,6 +741,13 @@ describe("SwapService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const cartService = {
|
||||
update: jest.fn(),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const swapRepo = MockRepository({
|
||||
findOneWithRelations: () => Promise.resolve(existing),
|
||||
})
|
||||
@@ -752,6 +759,7 @@ describe("SwapService", () => {
|
||||
lineItemService,
|
||||
eventBusService,
|
||||
fulfillmentService,
|
||||
cartService,
|
||||
})
|
||||
|
||||
it("creates a shipment", async () => {
|
||||
@@ -831,6 +839,15 @@ describe("SwapService", () => {
|
||||
},
|
||||
}
|
||||
|
||||
const cartService = {
|
||||
update: () => {
|
||||
return Promise.resolve()
|
||||
},
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
}
|
||||
|
||||
const paymentProviderService = {
|
||||
getStatus: jest.fn(() => {
|
||||
return Promise.resolve("authorized")
|
||||
@@ -838,6 +855,9 @@ describe("SwapService", () => {
|
||||
updatePayment: jest.fn(() => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
cancelPayment: jest.fn(() => {
|
||||
return Promise.resolve()
|
||||
}),
|
||||
withTransaction: function() {
|
||||
return this
|
||||
},
|
||||
@@ -872,6 +892,7 @@ describe("SwapService", () => {
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
totalsService,
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
@@ -933,6 +954,7 @@ describe("SwapService", () => {
|
||||
eventBusService,
|
||||
swapRepository: swapRepo,
|
||||
totalsService,
|
||||
cartService,
|
||||
paymentProviderService,
|
||||
eventBusService,
|
||||
shippingOptionService,
|
||||
|
||||
@@ -682,6 +682,14 @@ class CartService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
if ("completed_at" in update) {
|
||||
cart.completed_at = update.completed_at
|
||||
}
|
||||
|
||||
if ("payment_authorized_at" in update) {
|
||||
cart.payment_authorized_at = update.payment_authorized_at
|
||||
}
|
||||
|
||||
const result = await cartRepo.save(cart)
|
||||
|
||||
if ("email" in update || "customer_id" in update) {
|
||||
@@ -1027,7 +1035,7 @@ class CartService extends BaseService {
|
||||
|
||||
// If cart total is 0, we don't perform anything payment related
|
||||
if (cart.total <= 0) {
|
||||
cart.completed_at = new Date()
|
||||
cart.payment_authorized_at = new Date()
|
||||
return cartRepository.save(cart)
|
||||
}
|
||||
|
||||
@@ -1046,7 +1054,7 @@ class CartService extends BaseService {
|
||||
.createPayment(freshCart)
|
||||
|
||||
freshCart.payment = payment
|
||||
freshCart.completed_at = new Date()
|
||||
freshCart.payment_authorized_at = new Date()
|
||||
}
|
||||
|
||||
const updated = await cartRepository.save(freshCart)
|
||||
@@ -1352,7 +1360,7 @@ class CartService extends BaseService {
|
||||
* @return {Promise} the result of the update operation
|
||||
*/
|
||||
async setRegion_(cart, regionId, countryCode) {
|
||||
if (cart.completed_at) {
|
||||
if (cart.completed_at || cart.payment_authorized_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Cannot change the region of a completed cart"
|
||||
@@ -1494,6 +1502,13 @@ class CartService extends BaseService {
|
||||
)
|
||||
}
|
||||
|
||||
if (cart.payment_authorized_at) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_ALLOWED,
|
||||
"Can't delete a cart with an authorized payment"
|
||||
)
|
||||
}
|
||||
|
||||
const cartRepo = manager.getCustomRepository(this.cartRepository_)
|
||||
return cartRepo.remove(cartId)
|
||||
})
|
||||
|
||||
@@ -477,6 +477,9 @@ class OrderService extends BaseService {
|
||||
.withTransaction(manager)
|
||||
.cancelPayment(payment)
|
||||
}
|
||||
await this.cartService_
|
||||
.withTransaction(manager)
|
||||
.update(cart.id, { payment_authorized_at: null })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
@@ -595,6 +598,10 @@ class OrderService extends BaseService {
|
||||
no_notification: result.no_notification,
|
||||
})
|
||||
|
||||
await this.cartService_
|
||||
.withTransaction(manager)
|
||||
.update(cart.id, { completed_at: new Date() })
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ class PaymentProviderService extends BaseService {
|
||||
payment.canceled_at = now.toISOString()
|
||||
|
||||
const paymentRepo = manager.getCustomRepository(this.paymentRepository_)
|
||||
return paymentRepo.save(payment)
|
||||
return await paymentRepo.save(payment)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -665,20 +665,33 @@ class SwapService extends BaseService {
|
||||
}
|
||||
|
||||
const cart = swap.cart
|
||||
const { payment } = cart
|
||||
|
||||
const items = swap.cart.items
|
||||
|
||||
for (const item of items) {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(item.variant_id, item.quantity)
|
||||
if (!swap.allow_backorder) {
|
||||
for (const item of items) {
|
||||
try {
|
||||
await this.inventoryService_
|
||||
.withTransaction(manager)
|
||||
.confirmInventory(item.variant_id, item.quantity)
|
||||
} catch (err) {
|
||||
if (payment) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
.cancelPayment(payment)
|
||||
}
|
||||
await this.cartService_
|
||||
.withTransaction(manager)
|
||||
.update(cart.id, { payment_authorized_at: null })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const total = await this.totalsService_.getTotal(cart)
|
||||
|
||||
if (total > 0) {
|
||||
const { payment } = cart
|
||||
|
||||
if (!payment) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_ARGUMENT,
|
||||
@@ -717,7 +730,7 @@ class SwapService extends BaseService {
|
||||
swap.shipping_address_id = cart.shipping_address_id
|
||||
swap.shipping_methods = cart.shipping_methods
|
||||
swap.confirmed_at = now.toISOString()
|
||||
swap.payment_status = total === 0 ? "difference_refunded" : "awaiting"
|
||||
swap.payment_status = total === 0 ? "confirmed" : "awaiting"
|
||||
|
||||
const swapRepo = manager.getCustomRepository(this.swapRepository_)
|
||||
const result = await swapRepo.save(swap)
|
||||
@@ -737,6 +750,10 @@ class SwapService extends BaseService {
|
||||
no_notification: swap.no_notification,
|
||||
})
|
||||
|
||||
await this.cartService_
|
||||
.withTransaction(manager)
|
||||
.update(cart.id, { completed_at: new Date() })
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user