Adds dynamic coupon codes and segment plugin

This commit is contained in:
Sebastian Rindom
2020-07-17 14:06:55 +02:00
parent ec6a538877
commit 3710f7f80a
41 changed files with 11784 additions and 109 deletions
@@ -37,20 +37,6 @@ class KlarnaProviderService extends PaymentService {
async lineItemsToOrderLines_(cart, taxRate) {
let order_lines = []
// Find the discount, that is not free shipping
const discount = cart.discounts.find(
({ discount_rule }) => discount_rule.type !== "free_shipping"
)
let itemDiscounts = []
if (discount) {
// If the discount has an item specific allocation method,
// we need to fetch the discount for each item
itemDiscounts = await this.totalsService_.getAllocationItemDiscounts(
discount,
cart
)
}
cart.items.forEach((item) => {
// For bundles, we create an order line for each item in the bundle
@@ -62,8 +48,6 @@ class KlarnaProviderService extends PaymentService {
order_lines.push({
name: item.title,
unit_price: c.unit_price,
// Medusa does not allow discount on bundles
total_discount_amount: 0,
quantity: c.quantity,
tax_rate: taxRate * 10000,
total_amount,
@@ -71,17 +55,10 @@ class KlarnaProviderService extends PaymentService {
})
})
} else {
// Find the discount for current item and default to 0
const itemDiscount =
(itemDiscounts &&
itemDiscounts.find((el) => el.lineItem._id === item._id)) ||
0
// Withdraw discount from the total item amount
const quantity = item.quantity
const unit_price = item.content.unit_price * 100 * (taxRate + 1)
const total_discount_amount = itemDiscount * (taxRate + 1) * 100
const total_amount = unit_price * quantity - total_discount_amount
const total_amount = unit_price * quantity
const total_tax_amount = total_amount * (taxRate / (1 + taxRate))
order_lines.push({
@@ -89,7 +66,6 @@ class KlarnaProviderService extends PaymentService {
tax_rate: taxRate * 10000,
quantity,
unit_price,
total_discount_amount,
total_amount,
total_tax_amount,
})
@@ -127,6 +103,20 @@ class KlarnaProviderService extends PaymentService {
order.order_lines = await this.lineItemsToOrderLines_(cart, tax_rate)
const discount = (await this.totalsService_.getDiscountTotal(cart)) * 100
if (discount) {
order.order_lines.push({
name: `Discount`,
quantity: 1,
type: "discount",
unit_price: 0,
total_discount_amount: discount * (1 + tax_rate),
tax_rate: tax_rate * 10000,
total_amount: - discount * (1 + tax_rate),
total_tax_amount: - discount * tax_rate
})
}
if (!_.isEmpty(cart.billing_address)) {
order.billing_address = {
email: cart.email,
@@ -145,7 +135,6 @@ class KlarnaProviderService extends PaymentService {
// Defaults to Sweden
order.purchase_country = "SE"
}
order.order_amount = (await this.totalsService_.getTotal(cart)) * 100
order.order_tax_amount = (await this.totalsService_.getTaxTotal(cart)) * 100
// TODO: Check if currency matches ISO
@@ -179,7 +168,6 @@ class KlarnaProviderService extends PaymentService {
}
// If the cart does have shipping methods, set the selected shipping method
order.shipping_options = shippingOptions.map((so) => ({
id: so._id,
name: so.name,
@@ -190,6 +178,7 @@ class KlarnaProviderService extends PaymentService {
}))
}
console.log(order)
return order
}
@@ -0,0 +1,13 @@
{
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-instanceof",
"@babel/plugin-transform-classes"
],
"presets": ["@babel/preset-env"],
"env": {
"test": {
"plugins": ["@babel/plugin-transform-runtime"]
}
}
}
@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}
@@ -0,0 +1,15 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
!jest.config.js
/dist
/api
/services
/models
/subscribers
@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock
@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}
@@ -0,0 +1,3 @@
module.exports = {
testEnvironment: "node",
}
@@ -0,0 +1,39 @@
{
"name": "medusa-plugin-discount-generator",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"author": "Sebastian Rindom",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-plugin-discount-generator"
},
"devDependencies": {
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/node": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-transform-instanceof": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.5",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.9.6",
"client-sessions": "^0.8.0",
"cross-env": "^5.2.1",
"eslint": "^6.8.0",
"jest": "^25.5.2"
},
"scripts": {
"build": "babel src -d .",
"prepare": "cross-env NODE_ENV=production npm run build",
"watch": "babel -w src --out-dir . --ignore **/__tests__",
"test": "jest"
},
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"medusa-core-utils": "^0.1.27",
"randomatic": "^3.1.1"
}
}
@@ -0,0 +1,29 @@
import { Router } from "express"
import bodyParser from "body-parser"
import { Validator, MedusaError } from "medusa-core-utils"
export default (container) => {
const route = Router()
route.post("/discount-code", bodyParser.json(), async (req, res) => {
const schema = Validator.object().keys({
discount_code: Validator.string().required(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
const discountGenerator = req.scope.resolve("discountGeneratorService")
const code = await discountGenerator.generateDiscount(value.discount_code)
res.json({
code
})
})
return route
}
@@ -0,0 +1,20 @@
import randomize from "randomatic"
import { BaseService } from "medusa-interfaces"
class DiscountGeneratorService extends BaseService {
constructor({ discountService }) {
super()
this.discountService_ = discountService
}
async generateDiscount(dynamicDiscountCode) {
const discount = await this.discountService_.retrieveByCode(dynamicDiscountCode)
const data = {
code: randomize("A0", 10)
}
return this.discountService_.createDynamicCode(discount._id, data)
}
}
export default DiscountGeneratorService
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-instanceof",
"@babel/plugin-transform-classes"
],
"presets": ["@babel/preset-env"],
"env": {
"test": {
"plugins": ["@babel/plugin-transform-runtime"]
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"plugins": ["prettier"],
"extends": ["prettier"],
"rules": {
"prettier/prettier": "error",
"semi": "error",
"no-unused-expressions": "true"
}
}
+15
View File
@@ -0,0 +1,15 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
!jest.config.js
/dist
/api
/services
/models
/subscribers
@@ -0,0 +1,9 @@
/lib
node_modules
.DS_store
.env*
/*.js
!index.js
yarn.lock
@@ -0,0 +1,7 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5"
}
+1
View File
@@ -0,0 +1 @@
// noop
@@ -0,0 +1,3 @@
module.exports = {
testEnvironment: "node",
}
@@ -0,0 +1,43 @@
{
"name": "medusa-plugin-segment",
"version": "0.3.0",
"description": "Segment Analytics",
"main": "index.js",
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/medusa-plugin-segment"
},
"author": "Sebastian Rindom",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/node": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-transform-classes": "^7.9.5",
"@babel/plugin-transform-instanceof": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.5",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.9.6",
"cross-env": "^5.2.1",
"eslint": "^6.8.0",
"jest": "^25.5.2"
},
"scripts": {
"build": "babel src -d .",
"prepare": "cross-env NODE_ENV=production npm run build",
"watch": "babel -w src --out-dir . --ignore **/__tests__",
"test": "jest"
},
"dependencies": {
"analytics-node": "^3.4.0-beta.1",
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"express": "^4.17.1",
"medusa-core-utils": "^0.3.0",
"medusa-interfaces": "^0.3.0",
"medusa-test-utils": "^0.3.0"
}
}
@@ -0,0 +1,110 @@
import Analytics from "analytics-node"
import axios from "axios"
import { BaseService } from "medusa-interfaces"
class SegmentService extends BaseService {
/**
* @param {Object} options - options defined in `medusa-config.js`
* e.g.
* {
* write_key: Segment write key given in Segment dashboard
* }
*/
constructor({totalsService}, options) {
super()
this.options_ = options
this.totalsService_ = totalsService
this.analytics_ = new Analytics(options.write_key)
}
/**
* Wrapper around segment's identify call
*/
identify(data) {
return this.analytics_.identify(data)
}
track(data) {
return this.analytics_.track(data)
}
async getReportingValue(fromCurrency, value) {
const date = "latest"
const toCurrency =
(this.options_.reporting_currency &&
this.options_.reporting_currency.toUpperCase()) ||
"EUR"
if (fromCurrency === toCurrency) return value.toFixed(2)
const exchangeRate = await axios
.get(`https://api.exchangeratesapi.io/${date}?symbols=${fromCurrency}&base=${toCurrency}`)
.then(({ data }) => {
return data.rates[fromCurrency]
})
return (value / exchangeRate).toFixed(2)
}
async buildOrder(order) {
console.log("build", order)
const subtotal = await this.totalsService_.getSubtotal(order)
const total = await this.totalsService_.getTotal(order)
const tax = await this.totalsService_.getTaxTotal(order)
const discount = await this.totalsService_.getDiscountTotal(order)
const shipping = await this.totalsService_.getShippingTotal(order)
const revenue = total - tax
let coupon
if (order.discounts && order.discounts.length) {
coupon = order.discounts[0].code
}
const orderData = {
checkout_id: order.cart_id,
order_id: order._id,
email: order.email,
reporting_total: await this.getReportingValue(order.currency_code, total),
reporting_subtotal: await this.getReportingValue(order.currency_code, subtotal),
reporting_revenue: await this.getReportingValue(order.currency_code, revenue),
reporting_shipping: await this.getReportingValue(order.currency_code, shipping),
reporting_tax: await this.getReportingValue(order.currency_code, tax),
reporting_discount: await this.getReportingValue(order.currency_code, discount),
total,
subtotal,
revenue,
shipping,
tax,
discount,
coupon,
currency: order.currency_code,
products: await Promise.all(
order.items.map(async item => {
let name = item.title
let variant = item.description
const unit_price = item.content.unit_price
const line_total = unit_price * item.content.quantity * item.quantity
const revenue = await this.getReportingValue(order.currency_code, line_total)
return {
name,
variant,
price: unit_price,
reporting_revenue: revenue,
product_id: `${item.content.product._id}`,
sku: item.content.variant.sku,
quantity: item.quantity,
}
})
),
}
return orderData
}
}
export default SegmentService
@@ -0,0 +1,18 @@
class OrderSubscriber {
constructor({ segmentService, eventBusService }) {
eventBusService.subscribe("order.placed", async (order) => {
const date = new Date(parseInt(order.created))
const orderData = await segmentService.buildOrder(order)
const orderEvent = {
event: "Order Completed",
userId: order.customer_id,
properties: orderData,
timestamp: date,
}
segmentService.track(orderEvent)
})
}
}
export default OrderSubscriber
File diff suppressed because it is too large Load Diff
@@ -37,6 +37,7 @@ describe("POST /admin/discounts", () => {
value: 10,
allocation: "total",
},
is_dynamic: false
})
})
})
@@ -43,6 +43,7 @@ describe("POST /admin/discounts", () => {
value: 10,
allocation: "total",
},
is_dynamic: false,
}
)
})
@@ -3,6 +3,7 @@ import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const schema = Validator.object().keys({
code: Validator.string().required(),
is_dynamic: Validator.boolean().default(false),
discount_rule: Validator.object()
.keys({
description: Validator.string().optional(),
@@ -0,0 +1,25 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { discount_id } = req.params
const schema = Validator.object().keys({
code: Validator.string().required(),
metadata: Validator.object().optional(),
})
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const discountService = req.scope.resolve("discountService")
await discountService.createDynamicCode(discount_id, value)
const data = await discountService.retrieve(dicsount_id)
res.status(200).json({ discount: data })
} catch (err) {
throw err
}
}
@@ -0,0 +1,21 @@
import { MedusaError, Validator } from "medusa-core-utils"
export default async (req, res) => {
const { discount_id, code } = req.params
const { value, error } = schema.validate(req.body)
if (error) {
throw new MedusaError(MedusaError.Types.INVALID_DATA, error.details)
}
try {
const discountService = req.scope.resolve("discountService")
await discountService.deleteDynamicCode(discount_id, code)
const data = await discountService.retrieve(dicsount_id)
res.status(200).json({ discount: data })
} catch (err) {
throw err
}
}
@@ -22,6 +22,16 @@ export default app => {
middlewares.wrap(require("./delete-discount").default)
)
// Dynamic codes
route.post(
"/:discount_id/dynamic-codes",
middlewares.wrap(require("./create-dynamic-code").default)
)
route.delete(
"/:discount_id/dynamic-codes/:code",
middlewares.wrap(require("./delete-dynamic-code").default)
)
// Discount valid variants management
route.post(
"/:discount_id/variants/:variant_id",
@@ -4,6 +4,7 @@ export default async (req, res) => {
const { discount_id } = req.params
const schema = Validator.object().keys({
code: Validator.string().required(),
is_dynamic: Validator.boolean().default(false),
discount_rule: Validator.object()
.keys({
description: Validator.string().optional(),
@@ -13,6 +13,7 @@ export default async (req, res) => {
res.status(200).json({ cart })
} catch (err) {
console.log(err.response.data)
throw err
}
}
@@ -34,7 +34,6 @@ export default async (req, res) => {
res.status(200).json({ order })
} catch (err) {
console.log(err)
// If something fails it might be because the order has already been created
// if it has we find it from the cart id
const orderService = req.scope.resolve("orderService")
@@ -1,6 +1,17 @@
import { IdMap } from "medusa-test-utils"
export const discounts = {
dynamic: {
_id: IdMap.getId("dynamic"),
code: "Something",
is_dynamic: true,
discount_rule: {
type: "percentage",
allocation: "total",
value: 10,
},
regions: [IdMap.getId("region-france")],
},
total10Percent: {
_id: IdMap.getId("total10"),
code: "10%OFF",
@@ -117,6 +128,9 @@ export const DiscountModelMock = {
}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query._id === IdMap.getId("dynamic")) {
return Promise.resolve(discounts.dynamic)
}
if (query._id === IdMap.getId("total10")) {
return Promise.resolve(discounts.total10Percent)
}
@@ -0,0 +1,25 @@
import { IdMap } from "medusa-test-utils"
export const dynamicDiscounts = {
dynamicOff: {
_id: IdMap.getId("dynamicOff"),
discount_id: IdMap.getId("dynamic"),
code: "DYNAMICOFF",
disabled: false,
usage_count: 0,
},
}
export const DynamicDiscountCodeModelMock = {
create: jest.fn().mockReturnValue(Promise.resolve()),
updateOne: jest.fn().mockImplementation((query, update) => {
return Promise.resolve()
}),
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
findOne: jest.fn().mockImplementation(query => {
if (query.code === "DYNAMICOFF") {
return Promise.resolve(dynamicDiscounts.dynamicOff)
}
return Promise.resolve(undefined)
}),
}
+1
View File
@@ -7,6 +7,7 @@ class DiscountModel extends BaseModel {
static schema = {
code: { type: String, required: true, unique: true },
is_dynamic: { type: Boolean, default: false },
discount_rule: { type: DiscountRule, required: true },
usage_count: { type: Number, default: 0 },
disabled: { type: Boolean, default: false },
@@ -0,0 +1,16 @@
import mongoose from "mongoose"
import { BaseModel } from "medusa-interfaces"
class DynamicDiscountCode extends BaseModel {
static modelName = "DynamicDiscountCode"
static schema = {
code: { type: String, required: true, unique: true },
discount_id: { type: String, required: true },
usage_count: { type: Number, default: 0 },
disabled: { type: Boolean, default: false },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
}
}
export default DynamicDiscountCode
+1
View File
@@ -28,6 +28,7 @@ class OrderModel extends BaseModel {
customer_id: { type: String },
payment_method: { type: PaymentMethodSchema, required: true },
shipping_methods: { type: [ShippingMethodSchema], required: true },
created: { type: String, default: Date.now },
metadata: { type: mongoose.Schema.Types.Mixed, default: {} },
}
}
@@ -1,5 +1,9 @@
import DiscountService from "../discount"
import { DiscountModelMock, discounts } from "../../models/__mocks__/discount"
import {
DynamicDiscountCodeModelMock,
dynamicDiscounts,
} from "../../models/__mocks__/dynamic-discount-code"
import { IdMap } from "medusa-test-utils"
import { ProductVariantServiceMock } from "../__mocks__/product-variant"
import { RegionServiceMock } from "../__mocks__/region"
@@ -61,6 +65,46 @@ describe("DiscountService", () => {
})
})
describe("retrieveByCode", () => {
let res
const discountService = new DiscountService({
discountModel: DiscountModelMock,
dynamicDiscountCodeModel: DynamicDiscountCodeModelMock,
})
beforeEach(() => {
jest.clearAllMocks()
})
it("calls model layer findOne", async () => {
res = await discountService.retrieveByCode("10%off")
expect(DiscountModelMock.findOne).toHaveBeenCalledTimes(1)
expect(DiscountModelMock.findOne).toHaveBeenCalledWith({
code: "10%OFF",
})
expect(res).toEqual(discounts.total10Percent)
})
it("finds dynamic code", async () => {
res = await discountService.retrieveByCode("dynamicoff")
expect(DiscountModelMock.findOne).toHaveBeenCalledTimes(2)
expect(DiscountModelMock.findOne).toHaveBeenCalledWith({
_id: IdMap.getId("dynamic"),
})
expect(DiscountModelMock.findOne).toHaveBeenCalledWith({
code: "DYNAMICOFF",
})
expect(DynamicDiscountCodeModelMock.findOne).toHaveBeenCalledTimes(1)
expect(DynamicDiscountCodeModelMock.findOne).toHaveBeenCalledWith({
code: "DYNAMICOFF",
})
expect(res).toEqual({
...discounts.dynamic,
code: "DYNAMICOFF",
})
})
})
describe("update", () => {
const discountService = new DiscountService({
discountModel: DiscountModelMock,
+8
View File
@@ -610,6 +610,14 @@ class CartService extends BaseService {
async applyDiscount(cartId, discountCode) {
const cart = await this.retrieve(cartId)
const discount = await this.discountService_.retrieveByCode(discountCode)
if (discount.disabled) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"The discount code is disabled"
)
}
if (!discount.regions.includes(cart.region_id)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
+72 -5
View File
@@ -9,6 +9,7 @@ import _ from "lodash"
class DiscountService extends BaseService {
constructor({
discountModel,
dynamicDiscountCodeModel,
totalsService,
productVariantService,
regionService,
@@ -18,6 +19,9 @@ class DiscountService extends BaseService {
/** @private @const {DiscountModel} */
this.discountModel_ = discountModel
/** @private @const {DynamicDiscountCodeModel} */
this.dynamicCodeModel_ = dynamicDiscountCodeModel
/** @private @const {TotalsService} */
this.totalsService_ = totalsService
@@ -144,18 +148,38 @@ class DiscountService extends BaseService {
*/
async retrieveByCode(discountCode) {
discountCode = this.normalizeDiscountCode_(discountCode)
const discount = await this.discountModel_
let discount = await this.discountModel_
.findOne({ code: discountCode })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
if (!discount) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Discount with code ${discountCode} was not found`
)
const dynamicCode = await this.dynamicCodeModel_
.findOne({ code: discountCode })
.catch(err => {
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
})
if (!dynamicCode) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Discount with code ${discountCode} was not found`
)
}
discount = await this.retrieve(dynamicCode.discount_id)
if (!discount) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Discount with code ${discountCode} was not found`
)
}
discount.code = discountCode
discount.disabled = dynamicCode.disabled
}
return discount
}
@@ -186,6 +210,49 @@ class DiscountService extends BaseService {
)
}
/**
* Creates a dynamic code for a discount id.
* @param {string} discountId - the id of the discount to create a code for
* @param {string} code - the code to identify the discount by
* @return {Promise} the newly created dynamic code
*/
async createDynamicCode(discountId, data) {
const discount = await this.retrieve(discountId)
if (!discount.is_dynamic) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount must be set to dynamic"
)
}
const code = this.normalizeDiscountCode_(data.code)
return this.dynamicCodeModel_.create({
...data,
discount_id: discount._id,
code,
})
}
/**
* Creates a dynamic code for a discount id.
* @param {string} discountId - the id of the discount to create a code for
* @param {string} code - the code to identify the discount by
* @return {Promise} the newly created dynamic code
*/
async deleteDynamicCode(discountId, code) {
const discont = await this.retrieve(discountId)
if (!discount.is_dynamic) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
"Discount must be set to dynamic"
)
}
return this.dynamicCodeModel_.deleteOne({
code,
})
}
/**
* Adds a valid variant to the discount rule valid_for array.
* @param {string} discountId - id of discount
+76 -72
View File
@@ -245,89 +245,93 @@ class OrderService extends BaseService {
const dbSession = await this.orderModel_.startSession()
// Initialize DB transaction
return dbSession.withTransaction(async () => {
// Check if order from cart already exists
// If so, this function throws
const exists = await this.existsByCartId(cart._id)
if (exists) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Order from cart already exists"
return dbSession
.withTransaction(async () => {
// Check if order from cart already exists
// If so, this function throws
const exists = await this.existsByCartId(cart._id)
if (exists) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Order from cart already exists"
)
}
// Throw if payment method does not exist
if (!cart.payment_method) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Cart does not contain a payment method"
)
}
const { payment_method, payment_sessions } = cart
if (!payment_sessions || !payment_sessions.length) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"cart must have payment sessions"
)
}
let paymentSession = payment_sessions.find(
ps => ps.provider_id === payment_method.provider_id
)
}
// Throw if payment method does not exist
if (!cart.payment_method) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Cart does not contain a payment method"
// Throw if payment method does not exist
if (!paymentSession) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Cart does not have an authorized payment session"
)
}
const region = await this.regionService_.retrieve(cart.region_id)
const paymentProvider = this.paymentProviderService_.retrieveProvider(
paymentSession.provider_id
)
}
const { payment_method, payment_sessions } = cart
if (!payment_sessions || !payment_sessions.length) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"cart must have payment sessions"
const paymentStatus = await paymentProvider.getStatus(
paymentSession.data
)
}
let paymentSession = payment_sessions.find(
ps => ps.provider_id === payment_method.provider_id
)
// 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"
)
}
// Throw if payment method does not exist
if (!paymentSession) {
throw new MedusaError(
MedusaError.Types.INVALID_ARGUMENT,
"Cart does not have an authorized payment session"
const paymentData = await paymentProvider.retrievePayment(
paymentSession.data
)
}
const region = await this.regionService_.retrieve(cart.region_id)
const paymentProvider = this.paymentProviderService_.retrieveProvider(
paymentSession.provider_id
)
const paymentStatus = await paymentProvider.getStatus(paymentSession.data)
const o = {
payment_method: {
provider_id: paymentSession.provider_id,
data: paymentData,
},
discounts: cart.discounts,
shipping_methods: cart.shipping_methods,
items: cart.items,
shipping_address: cart.shipping_address,
billing_address: cart.shipping_address,
region_id: cart.region_id,
email: cart.email,
customer_id: cart.customer_id,
cart_id: cart._id,
currency_code: region.currency_code,
}
// 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"
)
}
const orderDocument = await this.orderModel_.create([o], {
session: dbSession,
})
const paymentData = await paymentProvider.retrievePayment(
paymentSession.data
)
const o = {
payment_method: {
provider_id: paymentSession.provider_id,
data: paymentData,
},
discounts: cart.discounts,
shipping_methods: cart.shipping_methods,
items: cart.items,
shipping_address: cart.shipping_address,
billing_address: cart.shipping_address,
region_id: cart.region_id,
email: cart.email,
customer_id: cart.customer_id,
cart_id: cart._id,
currency_code: region.currency_code,
}
const orderDocument = await this.orderModel_.create([o], {
session: dbSession,
// Emit and return
this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0])
return orderDocument[0].toObject()
})
// Emit and return
this.eventBus_.emit(OrderService.Events.PLACED, orderDocument[0])
return orderDocument[0]
})
.then(() => this.orderModel_.findOne({ cart_id: cart._id }))
}
/**
+5 -4
View File
@@ -22,10 +22,10 @@ class TotalsService extends BaseService {
* @return {int} the calculated subtotal
*/
async getTotal(object) {
const subtotal = this.getSubtotal(object)
const subtotal = await this.getSubtotal(object)
const taxTotal = await this.getTaxTotal(object)
const discountTotal = await this.getDiscountTotal(object)
const shippingTotal = this.getShippingTotal(object)
const shippingTotal = await this.getShippingTotal(object)
return subtotal + taxTotal + shippingTotal - discountTotal
}
@@ -74,9 +74,10 @@ class TotalsService extends BaseService {
async getTaxTotal(object) {
const subtotal = this.getSubtotal(object)
const shippingTotal = this.getShippingTotal(object)
const discountTotal = await this.getDiscountTotal(object)
const region = await this.regionService_.retrieve(object.region_id)
const { tax_rate } = region
return (subtotal + shippingTotal) * tax_rate
return (subtotal - discountTotal + shippingTotal) * tax_rate
}
/**
@@ -260,7 +261,7 @@ class TotalsService extends BaseService {
async getDiscountTotal(cart) {
let subtotal = this.getSubtotal(cart)
if (!cart.discounts) {
if (!cart.discounts || !cart.discounts.length) {
return 0
}