feat(medusa): adds support for gift cards (#132)
* fix(medusa-plugin-brightpearl): adds gift cards to gift card nominal code * fix: allow orders with 0 total to be created * fix: gift card ready * tests passing * docs: brightpearl comment * docs: clearer REMEMBER note
This commit is contained in:
@@ -13,8 +13,9 @@ Sends orders to Brightpearl, listens for stock movements, handles returns.
|
|||||||
default_status_id: [the status id to assign new orders with] (optional: defaults to 1)
|
default_status_id: [the status id to assign new orders with] (optional: defaults to 1)
|
||||||
payment_method_code: [the method code to register payments with] (optional: defaults to 1220)
|
payment_method_code: [the method code to register payments with] (optional: defaults to 1220)
|
||||||
sales_account_code: [nominal code to assign line items to] (optional: defaults to 4000)
|
sales_account_code: [nominal code to assign line items to] (optional: defaults to 4000)
|
||||||
shipping_account_code: [nominal code to assign shipping line to] (optional: defaults to 4040)jk
|
shipping_account_code: [nominal code to assign shipping line to] (optional: defaults to 4040)
|
||||||
discount_account_code: [nominal code to use for Discount-type refunds] (optional)
|
discount_account_code: [nominal code to use for Discount-type refunds] (optional)
|
||||||
|
gift_card_account_code: [nominal code to use for gift card products and redeems] (optional: default to 4000)
|
||||||
inventory_sync_cron: [cron pattern for inventory sync, if left out the job will not be created] (default: false)
|
inventory_sync_cron: [cron pattern for inventory sync, if left out the job will not be created] (default: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ class BrightpearlService extends BaseService {
|
|||||||
({ discount_rule }) => discount_rule.type !== "free_shipping"
|
({ discount_rule }) => discount_rule.type !== "free_shipping"
|
||||||
)
|
)
|
||||||
let lineDiscounts = []
|
let lineDiscounts = []
|
||||||
if (discount) {
|
if (discount && !discount.is_giftcard) {
|
||||||
lineDiscounts = this.totalsService_.getLineDiscounts(fromOrder, discount)
|
lineDiscounts = this.totalsService_.getLineDiscounts(fromOrder, discount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +511,7 @@ class BrightpearlService extends BaseService {
|
|||||||
item.content.variant.sku
|
item.content.variant.sku
|
||||||
)
|
)
|
||||||
|
|
||||||
const discount = lineDiscounts.find((l) => item._id === l.item._id) || {
|
const ld = lineDiscounts.find((l) => item._id === l.item._id) || {
|
||||||
amount: 0,
|
amount: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -522,7 +522,7 @@ class BrightpearlService extends BaseService {
|
|||||||
row.name = item.title
|
row.name = item.title
|
||||||
}
|
}
|
||||||
row.net = this.totalsService_.rounded(
|
row.net = this.totalsService_.rounded(
|
||||||
item.content.unit_price * item.quantity - discount.amount
|
item.content.unit_price * item.quantity - ld.amount
|
||||||
)
|
)
|
||||||
row.tax = this.totalsService_.rounded(row.net * fromOrder.tax_rate)
|
row.tax = this.totalsService_.rounded(row.net * fromOrder.tax_rate)
|
||||||
row.quantity = item.quantity
|
row.quantity = item.quantity
|
||||||
@@ -530,10 +530,34 @@ class BrightpearlService extends BaseService {
|
|||||||
row.externalRef = item._id
|
row.externalRef = item._id
|
||||||
row.nominalCode = this.options.sales_account_code || "4000"
|
row.nominalCode = this.options.sales_account_code || "4000"
|
||||||
|
|
||||||
|
if (item.is_giftcard) {
|
||||||
|
row.nominalCode = this.options.gift_card_account_code || "4000"
|
||||||
|
}
|
||||||
|
|
||||||
return row
|
return row
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If a gift card was applied to the order we reduce the order amount
|
||||||
|
// correspondingly. This reduces the amount payable, while debiting the
|
||||||
|
// gift card account that was previously credited, when the gift card was
|
||||||
|
// purchased.
|
||||||
|
if (discount && discount.is_giftcard) {
|
||||||
|
const discountTotal = await this.totalsService_.getDiscountTotal(
|
||||||
|
fromOrder
|
||||||
|
)
|
||||||
|
lines.push({
|
||||||
|
name: `Gift Card: ${discount.code}`,
|
||||||
|
net: -1 * discountTotal,
|
||||||
|
tax: this.totalsService_.rounded(
|
||||||
|
-1 * discountTotal * fromOrder.tax_rate
|
||||||
|
),
|
||||||
|
quantity: 1,
|
||||||
|
taxCode: region.tax_code,
|
||||||
|
nominalCode: this.options.gift_card_account_code || "4000",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const shippingTotal = this.totalsService_.getShippingTotal(fromOrder)
|
const shippingTotal = this.totalsService_.getShippingTotal(fromOrder)
|
||||||
const shippingMethods = fromOrder.shipping_methods
|
const shippingMethods = fromOrder.shipping_methods
|
||||||
if (shippingMethods.length > 0) {
|
if (shippingMethods.length > 0) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ describe("GET /admin/discounts", () => {
|
|||||||
let subject
|
let subject
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
subject = await request("GET", `/admin/discounts`, {
|
subject = await request("GET", `/admin/discounts`, {
|
||||||
adminSession: {
|
adminSession: {
|
||||||
jwt: {
|
jwt: {
|
||||||
@@ -22,7 +23,61 @@ describe("GET /admin/discounts", () => {
|
|||||||
|
|
||||||
it("calls service retrieve", () => {
|
it("calls service retrieve", () => {
|
||||||
expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1)
|
expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1)
|
||||||
expect(DiscountServiceMock.list).toHaveBeenCalledWith()
|
expect(DiscountServiceMock.list).toHaveBeenCalledWith({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("is_giftcard filter", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
subject = await request("GET", `/admin/discounts?is_giftcard=true`, {
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls service retrieve", () => {
|
||||||
|
expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1)
|
||||||
|
expect(DiscountServiceMock.list).toHaveBeenCalledWith({
|
||||||
|
is_giftcard: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("expand region filter", () => {
|
||||||
|
let subject
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
subject = await request("GET", `/admin/discounts?expand_fields=regions`, {
|
||||||
|
adminSession: {
|
||||||
|
jwt: {
|
||||||
|
userId: IdMap.getId("admin_user"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns 200", () => {
|
||||||
|
expect(subject.status).toEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls service retrieve", () => {
|
||||||
|
expect(DiscountServiceMock.decorate).toHaveBeenCalledTimes(1)
|
||||||
|
expect(
|
||||||
|
DiscountServiceMock.decorate
|
||||||
|
).toHaveBeenCalledWith(expect.anything(), expect.anything(), ["regions"])
|
||||||
|
expect(DiscountServiceMock.list).toHaveBeenCalledTimes(1)
|
||||||
|
expect(DiscountServiceMock.list).toHaveBeenCalledWith({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,34 @@
|
|||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const selector = {}
|
||||||
|
|
||||||
const discountService = req.scope.resolve("discountService")
|
const discountService = req.scope.resolve("discountService")
|
||||||
const data = await discountService.list()
|
|
||||||
|
if ("is_giftcard" in req.query) {
|
||||||
|
selector.is_giftcard = req.query.is_giftcard === "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
let expandFields = []
|
||||||
|
if ("expand_fields" in req.query) {
|
||||||
|
expandFields = req.query.expand_fields.split(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
let includeFields = [
|
||||||
|
"usage_count",
|
||||||
|
"starts_at",
|
||||||
|
"ends_at",
|
||||||
|
"original_amount",
|
||||||
|
"created",
|
||||||
|
]
|
||||||
|
if ("fields" in req.query) {
|
||||||
|
includeFields = req.query.fields.split(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = await discountService.list(selector)
|
||||||
|
|
||||||
|
const data = await Promise.all(
|
||||||
|
raw.map(d => discountService.decorate(d, includeFields, expandFields))
|
||||||
|
)
|
||||||
|
|
||||||
res.status(200).json({ discounts: data })
|
res.status(200).json({ discounts: data })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export default async (req, res) => {
|
|||||||
"description",
|
"description",
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if ("is_giftcard" in req.query) {
|
||||||
|
query.is_giftcard = req.query.is_giftcard === "true"
|
||||||
|
}
|
||||||
|
|
||||||
const limit = parseInt(req.query.limit) || 0
|
const limit = parseInt(req.query.limit) || 0
|
||||||
const offset = parseInt(req.query.offset) || 0
|
const offset = parseInt(req.query.offset) || 0
|
||||||
|
|
||||||
@@ -40,7 +44,6 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
res.json({ products, total_count: numProducts })
|
res.json({ products, total_count: numProducts })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class DiscountModel extends BaseModel {
|
|||||||
starts_at: { type: Date },
|
starts_at: { type: Date },
|
||||||
ends_at: { type: Date },
|
ends_at: { type: Date },
|
||||||
regions: { type: [String], default: [] },
|
regions: { type: [String], default: [] },
|
||||||
|
original_amount: { type: Number },
|
||||||
|
created: { type: String, default: Date.now },
|
||||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ class OrderModel extends BaseModel {
|
|||||||
region_id: { type: String, required: true },
|
region_id: { type: String, required: true },
|
||||||
discounts: { type: [DiscountSchema], default: [] },
|
discounts: { type: [DiscountSchema], default: [] },
|
||||||
customer_id: { type: String },
|
customer_id: { type: String },
|
||||||
payment_method: { type: PaymentMethodSchema, required: true },
|
payment_method: { type: PaymentMethodSchema, default: {} },
|
||||||
shipping_methods: { type: [ShippingMethodSchema], required: true },
|
shipping_methods: { type: [ShippingMethodSchema], default: [] },
|
||||||
documents: { type: [String], default: [] },
|
documents: { type: [String], default: [] },
|
||||||
created: { type: String, default: Date.now },
|
created: { type: String, default: Date.now },
|
||||||
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
import mongoose from "mongoose"
|
import mongoose from "mongoose"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* REMEMBER: When updating this line you must also update the LineItemService's
|
||||||
|
* validate method too. Otherwise we cannot copy lines directly.
|
||||||
|
*/
|
||||||
export default new mongoose.Schema(
|
export default new mongoose.Schema(
|
||||||
{
|
{
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
import mongoose from "mongoose"
|
import mongoose from "mongoose"
|
||||||
|
|
||||||
export default new mongoose.Schema({
|
export default new mongoose.Schema({
|
||||||
provider_id: { type: String, required: true },
|
provider_id: { type: String },
|
||||||
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
data: { type: mongoose.Schema.Types.Mixed, default: {} },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const DiscountServiceMock = {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
list: jest.fn().mockImplementation(data => {
|
list: jest.fn().mockImplementation(data => {
|
||||||
return Promise.resolve([])
|
return Promise.resolve([{}])
|
||||||
}),
|
}),
|
||||||
decorate: jest.fn().mockImplementation(data => {
|
decorate: jest.fn().mockImplementation(data => {
|
||||||
return Promise.resolve(data)
|
return Promise.resolve(data)
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ describe("DiscountService", () => {
|
|||||||
expect(DiscountModelMock.create).toHaveBeenCalledWith({
|
expect(DiscountModelMock.create).toHaveBeenCalledWith({
|
||||||
code: expect.stringMatching(/(([A-Z0-9]){4}(-?)){4}/),
|
code: expect.stringMatching(/(([A-Z0-9]){4}(-?)){4}/),
|
||||||
is_giftcard: true,
|
is_giftcard: true,
|
||||||
|
original_amount: 100,
|
||||||
discount_rule: {
|
discount_rule: {
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
allocation: "total",
|
allocation: "total",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ describe("OrderService", () => {
|
|||||||
const orderService = new OrderService({
|
const orderService = new OrderService({
|
||||||
orderModel: OrderModelMock,
|
orderModel: OrderModelMock,
|
||||||
paymentProviderService: PaymentProviderServiceMock,
|
paymentProviderService: PaymentProviderServiceMock,
|
||||||
|
totalsService: TotalsServiceMock,
|
||||||
discountService: DiscountServiceMock,
|
discountService: DiscountServiceMock,
|
||||||
regionService: RegionServiceMock,
|
regionService: RegionServiceMock,
|
||||||
eventBusService: EventBusServiceMock,
|
eventBusService: EventBusServiceMock,
|
||||||
@@ -56,7 +57,10 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("calls order model functions", async () => {
|
it("calls order model functions", async () => {
|
||||||
await orderService.createFromCart(carts.completeCart)
|
await orderService.createFromCart({
|
||||||
|
...carts.completeCart,
|
||||||
|
total: 100,
|
||||||
|
})
|
||||||
|
|
||||||
const order = {
|
const order = {
|
||||||
...carts.completeCart,
|
...carts.completeCart,
|
||||||
@@ -74,8 +78,34 @@ describe("OrderService", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("creates cart with 0 total", async () => {
|
||||||
|
await orderService.createFromCart({
|
||||||
|
...carts.completeCart,
|
||||||
|
total: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const order = {
|
||||||
|
...carts.completeCart,
|
||||||
|
payment_method: {},
|
||||||
|
currency_code: "eur",
|
||||||
|
cart_id: carts.completeCart._id,
|
||||||
|
tax_rate: 0.25,
|
||||||
|
metadata: {},
|
||||||
|
}
|
||||||
|
delete order._id
|
||||||
|
delete order.payment_sessions
|
||||||
|
|
||||||
|
expect(OrderModelMock.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(OrderModelMock.create).toHaveBeenCalledWith([order], {
|
||||||
|
session: expect.anything(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it("creates cart with gift card", async () => {
|
it("creates cart with gift card", async () => {
|
||||||
await orderService.createFromCart(carts.withGiftCard)
|
await orderService.createFromCart({
|
||||||
|
...carts.withGiftCard,
|
||||||
|
total: 100,
|
||||||
|
})
|
||||||
|
|
||||||
const order = {
|
const order = {
|
||||||
...carts.withGiftCard,
|
...carts.withGiftCard,
|
||||||
@@ -105,7 +135,6 @@ describe("OrderService", () => {
|
|||||||
description: "Gift card line",
|
description: "Gift card line",
|
||||||
thumbnail: "test-img-yeah.com/thumb",
|
thumbnail: "test-img-yeah.com/thumb",
|
||||||
metadata: {
|
metadata: {
|
||||||
giftcard: IdMap.getId("gift_card_id"),
|
|
||||||
name: "Test Name",
|
name: "Test Name",
|
||||||
},
|
},
|
||||||
is_giftcard: true,
|
is_giftcard: true,
|
||||||
@@ -130,24 +159,6 @@ describe("OrderService", () => {
|
|||||||
delete order._id
|
delete order._id
|
||||||
delete order.payment_sessions
|
delete order.payment_sessions
|
||||||
|
|
||||||
expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(2)
|
|
||||||
expect(EventBusServiceMock.emit).toHaveBeenCalledWith(
|
|
||||||
"order.gift_card_created",
|
|
||||||
{
|
|
||||||
currency_code: "eur",
|
|
||||||
tax_rate: 0.25,
|
|
||||||
email: "test",
|
|
||||||
giftcard: expect.any(Object),
|
|
||||||
line_item: expect.any(Object),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(DiscountServiceMock.generateGiftCard).toHaveBeenCalledTimes(1)
|
|
||||||
expect(DiscountServiceMock.generateGiftCard).toHaveBeenCalledWith(
|
|
||||||
100,
|
|
||||||
IdMap.getId("region-france")
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(OrderModelMock.create).toHaveBeenCalledTimes(1)
|
expect(OrderModelMock.create).toHaveBeenCalledTimes(1)
|
||||||
expect(OrderModelMock.create).toHaveBeenCalledWith([order], {
|
expect(OrderModelMock.create).toHaveBeenCalledWith([order], {
|
||||||
session: expect.anything(),
|
session: expect.anything(),
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ class DiscountService extends BaseService {
|
|||||||
discount_rule: discountRule,
|
discount_rule: discountRule,
|
||||||
is_giftcard: true,
|
is_giftcard: true,
|
||||||
regions: [region._id],
|
regions: [region._id],
|
||||||
|
original_amount: value,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,13 +445,42 @@ class DiscountService extends BaseService {
|
|||||||
* @return {Discount} return the decorated discount.
|
* @return {Discount} return the decorated discount.
|
||||||
*/
|
*/
|
||||||
async decorate(discount, fields = [], expandFields = []) {
|
async decorate(discount, fields = [], expandFields = []) {
|
||||||
const requiredFields = ["_id", "metadata"]
|
const requiredFields = [
|
||||||
|
"_id",
|
||||||
|
"code",
|
||||||
|
"regions",
|
||||||
|
"discount_rule",
|
||||||
|
"is_dynamic",
|
||||||
|
"is_giftcard",
|
||||||
|
"disabled",
|
||||||
|
"metadata",
|
||||||
|
]
|
||||||
const decorated = _.pick(discount, fields.concat(requiredFields))
|
const decorated = _.pick(discount, fields.concat(requiredFields))
|
||||||
|
|
||||||
if (expandFields.includes("valid_for")) {
|
if (expandFields.includes("valid_for")) {
|
||||||
|
let prods = {}
|
||||||
decorated.discount_rule.valid_for = await Promise.all(
|
decorated.discount_rule.valid_for = await Promise.all(
|
||||||
decorated.discount_rule.valid_for.map(async p => {
|
decorated.discount_rule.valid_for.map(async p => {
|
||||||
return this.productService_.retrieve(p)
|
if (p in prods) {
|
||||||
|
return prods[p]
|
||||||
|
}
|
||||||
|
const next = await this.productService_.retrieve(p)
|
||||||
|
prods[p] = next
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expandFields.includes("regions")) {
|
||||||
|
let regions = {}
|
||||||
|
decorated.regions = await Promise.all(
|
||||||
|
decorated.regions.map(async r => {
|
||||||
|
if (r in regions) {
|
||||||
|
return regions[r]
|
||||||
|
}
|
||||||
|
const next = await this.regionService_.retrieve(r)
|
||||||
|
regions[r] = next
|
||||||
|
return next
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,15 +37,17 @@ class LineItemService extends BaseService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const lineItemSchema = Validator.object({
|
const lineItemSchema = Validator.object({
|
||||||
|
_id: Validator.any().optional(),
|
||||||
title: Validator.string().required(),
|
title: Validator.string().required(),
|
||||||
is_giftcard: Validator.bool().optional(),
|
|
||||||
should_merge: Validator.bool().optional(),
|
|
||||||
description: Validator.string()
|
description: Validator.string()
|
||||||
.allow("")
|
.allow("")
|
||||||
.optional(),
|
.optional(),
|
||||||
thumbnail: Validator.string()
|
thumbnail: Validator.string()
|
||||||
.allow("")
|
.allow("")
|
||||||
.optional(),
|
.optional(),
|
||||||
|
is_giftcard: Validator.bool().optional(),
|
||||||
|
should_merge: Validator.bool().optional(),
|
||||||
|
has_shipping: Validator.bool().optional(),
|
||||||
content: Validator.alternatives()
|
content: Validator.alternatives()
|
||||||
.try(content, Validator.array().items(content))
|
.try(content, Validator.array().items(content))
|
||||||
.required(),
|
.required(),
|
||||||
@@ -53,6 +55,18 @@ class LineItemService extends BaseService {
|
|||||||
.integer()
|
.integer()
|
||||||
.min(1)
|
.min(1)
|
||||||
.required(),
|
.required(),
|
||||||
|
returned: Validator.bool().optional(),
|
||||||
|
fulfilled: Validator.bool().optional(),
|
||||||
|
shipped: Validator.bool().optional(),
|
||||||
|
fulfilled_quantity: Validator.number()
|
||||||
|
.integer()
|
||||||
|
.optional(),
|
||||||
|
returned_quantity: Validator.number()
|
||||||
|
.integer()
|
||||||
|
.optional(),
|
||||||
|
shipped_quantity: Validator.number()
|
||||||
|
.integer()
|
||||||
|
.optional(),
|
||||||
metadata: Validator.object().default({}),
|
metadata: Validator.object().default({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -300,92 +300,78 @@ class OrderService extends BaseService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Throw if payment method does not exist
|
const total = await this.totalsService_.getTotal(cart)
|
||||||
if (!cart.payment_method) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
|
||||||
"Cart does not contain a payment method"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let paymentSession = {}
|
||||||
|
let paymentData = {}
|
||||||
const { payment_method, payment_sessions } = cart
|
const { payment_method, payment_sessions } = cart
|
||||||
|
|
||||||
if (!payment_sessions || !payment_sessions.length) {
|
// Would be the case if a discount code is applied that covers the item
|
||||||
throw new MedusaError(
|
// total
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
if (total !== 0) {
|
||||||
"cart must have payment sessions"
|
// Throw if payment method does not exist
|
||||||
|
if (!payment_method) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
"Cart does not contain a payment method"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!payment_sessions || !payment_sessions.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
"cart must have payment sessions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentSession = payment_sessions.find(
|
||||||
|
ps => ps.provider_id === payment_method.provider_id
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
let paymentSession = payment_sessions.find(
|
// Throw if payment method does not exist
|
||||||
ps => ps.provider_id === payment_method.provider_id
|
if (!paymentSession) {
|
||||||
)
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
"Cart does not have an authorized payment session"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Throw if payment method does not exist
|
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
||||||
if (!paymentSession) {
|
paymentSession.provider_id
|
||||||
throw new MedusaError(
|
)
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
const paymentStatus = await paymentProvider.getStatus(
|
||||||
"Cart does not have an authorized payment session"
|
paymentSession.data
|
||||||
|
)
|
||||||
|
|
||||||
|
// If payment status is not authorized, we throw
|
||||||
|
if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
"Payment method is not authorized"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentData = await paymentProvider.retrievePayment(
|
||||||
|
paymentSession.data
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const region = await this.regionService_.retrieve(cart.region_id)
|
const region = await this.regionService_.retrieve(cart.region_id)
|
||||||
const paymentProvider = this.paymentProviderService_.retrieveProvider(
|
|
||||||
paymentSession.provider_id
|
|
||||||
)
|
|
||||||
const paymentStatus = await paymentProvider.getStatus(
|
|
||||||
paymentSession.data
|
|
||||||
)
|
|
||||||
|
|
||||||
// If payment status is not authorized, we throw
|
let payment = {}
|
||||||
if (paymentStatus !== "authorized" && paymentStatus !== "succeeded") {
|
if (paymentSession.provider_id) {
|
||||||
throw new MedusaError(
|
payment = {
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
provider_id: paymentSession.provider_id,
|
||||||
"Payment method is not authorized"
|
data: paymentData,
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentData = await paymentProvider.retrievePayment(
|
|
||||||
paymentSession.data
|
|
||||||
)
|
|
||||||
|
|
||||||
// Generate gift cards if in cart
|
|
||||||
const items = await Promise.all(
|
|
||||||
cart.items.map(async i => {
|
|
||||||
if (i.is_giftcard) {
|
|
||||||
const giftcard = await this.discountService_
|
|
||||||
.generateGiftCard(i.content.unit_price, region._id)
|
|
||||||
.then(result => {
|
|
||||||
this.eventBus_.emit(OrderService.Events.GIFT_CARD_CREATED, {
|
|
||||||
line_item: i,
|
|
||||||
currency_code: region.currency_code,
|
|
||||||
tax_rate: region.tax_rate,
|
|
||||||
giftcard: result,
|
|
||||||
email: cart.email,
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
...i,
|
|
||||||
metadata: {
|
|
||||||
...i.metadata,
|
|
||||||
giftcard: giftcard._id,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const o = {
|
const o = {
|
||||||
display_id: await this.counterService_.getNext("orders"),
|
display_id: await this.counterService_.getNext("orders"),
|
||||||
payment_method: {
|
payment_method: payment,
|
||||||
provider_id: paymentSession.provider_id,
|
|
||||||
data: paymentData,
|
|
||||||
},
|
|
||||||
discounts: cart.discounts,
|
discounts: cart.discounts,
|
||||||
shipping_methods: cart.shipping_methods,
|
shipping_methods: cart.shipping_methods,
|
||||||
items,
|
items: cart.items,
|
||||||
shipping_address: cart.shipping_address,
|
shipping_address: cart.shipping_address,
|
||||||
billing_address: cart.shipping_address,
|
billing_address: cart.shipping_address,
|
||||||
region_id: cart.region_id,
|
region_id: cart.region_id,
|
||||||
@@ -397,9 +383,11 @@ class OrderService extends BaseService {
|
|||||||
metadata: cart.metadata || {},
|
metadata: cart.metadata || {},
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderDocument = await this.orderModel_.create([o], {
|
const orderDocument = await this.orderModel_
|
||||||
session: dbSession,
|
.create([o], {
|
||||||
})
|
session: dbSession,
|
||||||
|
})
|
||||||
|
.catch(err => console.log(err))
|
||||||
|
|
||||||
// Emit and return
|
// Emit and return
|
||||||
this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0])
|
this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0])
|
||||||
@@ -1264,6 +1252,7 @@ class OrderService extends BaseService {
|
|||||||
o.refunded_total = await this.totalsService_.getRefundedTotal(order)
|
o.refunded_total = await this.totalsService_.getRefundedTotal(order)
|
||||||
o.refundable_amount = o.total - o.refunded_total
|
o.refundable_amount = o.total - o.refunded_total
|
||||||
o.created = order._id.getTimestamp()
|
o.created = order._id.getTimestamp()
|
||||||
|
|
||||||
if (expandFields.includes("region")) {
|
if (expandFields.includes("region")) {
|
||||||
o.region = await this.regionService_.retrieve(order.region_id)
|
o.region = await this.regionService_.retrieve(order.region_id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,10 @@ class TotalsService extends BaseService {
|
|||||||
if (type === "percentage") {
|
if (type === "percentage") {
|
||||||
percentage = value / 100
|
percentage = value / 100
|
||||||
} else if (type === "fixed") {
|
} else if (type === "fixed") {
|
||||||
percentage = value / subtotal
|
// If the fixed discount exceeds the subtotal we should
|
||||||
|
// calculate a 100% discount
|
||||||
|
const nominator = Math.min(value, subtotal)
|
||||||
|
percentage = nominator / subtotal
|
||||||
}
|
}
|
||||||
|
|
||||||
return cart.items.map(item => {
|
return cart.items.map(item => {
|
||||||
@@ -275,7 +278,7 @@ class TotalsService extends BaseService {
|
|||||||
if (type === "percentage" && allocation === "total") {
|
if (type === "percentage" && allocation === "total") {
|
||||||
toReturn = (subtotal / 100) * value
|
toReturn = (subtotal / 100) * value
|
||||||
} else if (type === "percentage" && allocation === "item") {
|
} else if (type === "percentage" && allocation === "item") {
|
||||||
const itemPercentageDiscounts = await this.getAllocationItemDiscounts(
|
const itemPercentageDiscounts = this.getAllocationItemDiscounts(
|
||||||
discount,
|
discount,
|
||||||
cart,
|
cart,
|
||||||
"percentage"
|
"percentage"
|
||||||
@@ -284,7 +287,7 @@ class TotalsService extends BaseService {
|
|||||||
} else if (type === "fixed" && allocation === "total") {
|
} else if (type === "fixed" && allocation === "total") {
|
||||||
toReturn = value
|
toReturn = value
|
||||||
} else if (type === "fixed" && allocation === "item") {
|
} else if (type === "fixed" && allocation === "item") {
|
||||||
const itemFixedDiscounts = await this.getAllocationItemDiscounts(
|
const itemFixedDiscounts = this.getAllocationItemDiscounts(
|
||||||
discount,
|
discount,
|
||||||
cart,
|
cart,
|
||||||
"fixed"
|
"fixed"
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
class OrderSubscriber {
|
class OrderSubscriber {
|
||||||
constructor({
|
constructor({
|
||||||
paymentProviderService,
|
paymentProviderService,
|
||||||
cartService,
|
|
||||||
customerService,
|
customerService,
|
||||||
eventBusService,
|
eventBusService,
|
||||||
discountService,
|
discountService,
|
||||||
totalsService,
|
totalsService,
|
||||||
|
orderService,
|
||||||
|
regionService,
|
||||||
}) {
|
}) {
|
||||||
this.totalsService_ = totalsService
|
this.totalsService_ = totalsService
|
||||||
|
|
||||||
@@ -15,7 +16,9 @@ class OrderSubscriber {
|
|||||||
|
|
||||||
this.discountService_ = discountService
|
this.discountService_ = discountService
|
||||||
|
|
||||||
this.cartService_ = cartService
|
this.orderService_ = orderService
|
||||||
|
|
||||||
|
this.regionService_ = regionService
|
||||||
|
|
||||||
this.eventBus_ = eventBusService
|
this.eventBus_ = eventBusService
|
||||||
|
|
||||||
@@ -31,6 +34,8 @@ class OrderSubscriber {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.eventBus_.subscribe("order.placed", this.handleDiscounts)
|
this.eventBus_.subscribe("order.placed", this.handleDiscounts)
|
||||||
|
|
||||||
|
this.eventBus_.subscribe("order.placed", this.handleGiftCards)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDiscounts = async order => {
|
handleDiscounts = async order => {
|
||||||
@@ -58,6 +63,40 @@ class OrderSubscriber {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleGiftCards = async order => {
|
||||||
|
const region = await this.regionService_.retrieve(order.region_id)
|
||||||
|
const items = await Promise.all(
|
||||||
|
order.items.map(async i => {
|
||||||
|
if (i.is_giftcard) {
|
||||||
|
const giftcard = await this.discountService_
|
||||||
|
.generateGiftCard(i.content.unit_price, region._id)
|
||||||
|
.then(result => {
|
||||||
|
this.eventBus_.emit("order.gift_card_created", {
|
||||||
|
line_item: i,
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
tax_rate: region.tax_rate,
|
||||||
|
giftcard: result,
|
||||||
|
email: order.email,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...i,
|
||||||
|
metadata: {
|
||||||
|
...i.metadata,
|
||||||
|
giftcard: giftcard._id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.orderService_.update(order._id, {
|
||||||
|
items,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default OrderSubscriber
|
export default OrderSubscriber
|
||||||
|
|||||||
Reference in New Issue
Block a user