Skip to content

Commit

Permalink
Discount on taxes (OriginProtocol#939)
Browse files Browse the repository at this point in the history
* Modeled an 'Exclude Taxes' checkbox based on 'Exclude Shipping Fees'. Added tooltips for both to explain their functions

* Frontend Changes: Added a button to make the already-existing 'Delete' option on the Edit Discount page visible. Fixed messaging error in tooltips for 'Exclude Shipping Fees' and 'Exclude Taxes' (see OriginProtocol#653). Backend change: the discount 'model' now has a field to recognize 'Exclude Taxes'

* Backend changes. Reference: OriginProtocol#762

* Adjusted logic that calculates the total tax to be paid. Added comments

* Added tests to discount.test.js, commented on the calculateCartTotal module, cleaned up files

* Substituted the use of an external component for one found in the existing codebase.

* file cleanup

Co-authored-by: Rajath <>
  • Loading branch information
phyninja authored Nov 22, 2021
1 parent 94ad99e commit 43904f9
Show file tree
Hide file tree
Showing 14 changed files with 304 additions and 42 deletions.
4 changes: 2 additions & 2 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ To run the full test suite:

To run a specific test or group of tests:

yarn run test -- -g "Discounts"
yarn run test -g "Discounts"

To run a single test, you can change it's function definition from it('blah blah...') to it.only('blah blah...') then run `yarn run test`
To run a single test, you can change its function definition from it('blah blah...') to it.only('blah blah...') then run `yarn run test`

Optionally, if you are going to run tests several times in a row (typically during development), you can speed up the test setup phase by starting the services in a separate terminal and leave them up and running.

Expand Down
12 changes: 12 additions & 0 deletions backend/db/migrations/202102141140-addExcludeTaxes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict'

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn('discounts', 'exclude_taxes', {
type: Sequelize.BOOLEAN
})
},
down: (queryInterface) => {
return queryInterface.removeColumn('discounts', 'exclude_taxes')
}
}
3 changes: 3 additions & 0 deletions backend/models/discount.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ module.exports = (sequelize, DataTypes) => {
excludeShipping: {
type: DataTypes.BOOLEAN
},
excludeTaxes: {
type: DataTypes.BOOLEAN
},
startTime: {
type: DataTypes.DATE
},
Expand Down
2 changes: 2 additions & 0 deletions backend/routes/discounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const safeDiscountProps = [
'maxUses',
'onePerCustomer',
'excludeShipping',
'excludeTaxes',
'startTime',
'endTime',
'data',
Expand All @@ -33,6 +34,7 @@ module.exports = function (router) {
'value',
'discountType',
'excludeShipping',
'excludeTaxes',
'maxDiscountValue',
'minCartValue'
])
Expand Down
146 changes: 146 additions & 0 deletions backend/test/discount.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
const { processor } = require('../queues/makeOfferProcessor')
const { Discount } = require('../models')
const { OrderPaymentStatuses, OrderPaymentTypes } = require('../utils/enums')
const calculateCartTotal = require('@origin/utils/calculateCartTotal')

describe('Discounts', () => {
let network,
Expand Down Expand Up @@ -376,4 +377,149 @@ describe('Discounts', () => {
expect(order.data).to.eql(data)
}
})

/* This test is to confirm that cart totals are being calculated as expected when the 'Exclude Shipping' and 'Exclude Taxes' checkboxes
are toggled during discount creation. In other words, this is a test for the interface between created discounts and the
'calculateCartTotal' module.
It also checks that user actions (of toggling) are being recorded in the discount object.
An overview of the discount names is below:
shipTax00 - Both 'Exclude Shipping' and 'Exclude Taxes' are unchecked
shipTax01 - 'Exclude Shipping' is unchecked.'Exclude Taxes' is checked
shipTax10 - 'Exclude Shipping' is checked.'Exclude Taxes' is unchecked
shipTax11 - Both 'Exclude Shipping' and 'Exclude Taxes' are checked
*/
it('Should calculate cart totals properly', async () => {
const carts = []

const shipTax00 = await Discount.create({
shopId: shop.id,
status: 'active',
code: '100OFF',
discountType: 'percentage',
value: 100, // 100% off
excludeShipping: false,
excludeTaxes: false,
startTime: Date.now(),
endTime: null,
minCartValue: 10,
maxDiscountValue: 100,
data: null
})

const shipTax01 = await Discount.create({
shopId: shop.id,
status: 'active',
code: '100OFF',
discountType: 'percentage',
value: 100, // 100% off
excludeShipping: false,
excludeTaxes: true,
startTime: Date.now(),
endTime: null,
minCartValue: 10,
maxDiscountValue: 100,
data: null
})

const shipTax10 = await Discount.create({
shopId: shop.id,
status: 'active',
code: '100OFF',
discountType: 'percentage',
value: 100, // 100% off
excludeShipping: true,
excludeTaxes: false,
startTime: Date.now(),
endTime: null,
minCartValue: 10,
maxDiscountValue: 100,
data: null
})

const shipTax11 = await Discount.create({
shopId: shop.id,
status: 'active',
code: '100OFF',
discountType: 'percentage',
value: 100, // 100% off
excludeShipping: true,
excludeTaxes: true,
startTime: Date.now(),
endTime: null,
minCartValue: 10 /* read $10 */,
maxDiscountValue: 100 /* read $100 */,
data: null
})

expect(percentageDiscount).to.have.property('excludeShipping')
expect(percentageDiscount).to.have.property('excludeTaxes')

expect(fixedDiscount).to.have.property('excludeShipping')
expect(fixedDiscount).to.have.property('excludeTaxes')

expect(shipTax00).to.have.property('excludeShipping', false)
expect(shipTax00).to.have.property('excludeTaxes', false)

expect(shipTax11).to.have.property('excludeShipping', true)
expect(shipTax11).to.have.property('excludeTaxes', true)

const discountsToTest = [shipTax00, shipTax01, shipTax10, shipTax11]
for (const discount of discountsToTest) {
const baseCart = {
items: [
{
product: 'iron-mask',
quantity: 1,
variant: 1234,
price: 8400 /*fixed point integer. Read $84.00*/
}
],
instructions: '',
currency: 'USD',
taxRate: 1800 /* 18% */,
shipping: {
id: 'STANDARD',
label: 'Flat Rate',
amount: 500 /* fixed point integer. Read $5.00 */
},
discountObj: discount,
userInfo: {
/*usually populated, but not necessary for testing*/
}
}
const calculations = calculateCartTotal(baseCart)
carts.push({
...baseCart,
total: calculations.total,
subTotal: calculations.subTotal,
discount: calculations.discount,
totalTaxes: calculations.totalTaxes
})
}

//Cart with the discount 'shipTax00'
expect(carts[0].subTotal).to.equal(8400)
expect(carts[0].totalTaxes).to.equal(1512) //18% of $84 = $15.12
expect(carts[0].discount).to.equal(10000) //Lesser of [100% (subtotal + shipping charges + taxes)] and 'maxDiscountValue'
expect(carts[0].total).to.equal(412) //(subtotal + shipping charges + taxes) - discount

//Cart with the discount 'shipTax01'
expect(carts[1].subTotal).to.equal(8400)
expect(carts[1].totalTaxes).to.equal(1512)
expect(carts[1].discount).to.equal(8900)
expect(carts[1].total).to.equal(1512)

//Cart with the discount 'shipTax10'
expect(carts[2].subTotal).to.equal(8400)
expect(carts[2].totalTaxes).to.equal(1512)
expect(carts[2].discount).to.equal(9912)
expect(carts[2].total).to.equal(500)

//Cart with the discount 'shipTax11'
expect(carts[3].subTotal).to.equal(8400)
expect(carts[3].totalTaxes).to.equal(1512)
expect(carts[3].discount).to.equal(8400)
expect(carts[3].total).to.equal(2012)
})
})
30 changes: 20 additions & 10 deletions packages/utils/calculateCartTotal.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ const get = require('lodash/get')

/**
* Accepts cart data and does all the subtotal, taxes,
* disocunt and total calculations
* discount and total calculations
*
* @param {Object} cart Cart data object
*
* @returns {{
* total {Number}
* subTotal {Number}
* shipping {Number}
* discount {Number}
* donation {Number}
* totalTaxes {Number}
* total {Number},
* subTotal {Number},
* shipping {Number},
* discount {Number},
* donation {Number},
* totalTaxes {Number},
* taxRate {Number}
* }}
*
* NOTE: total, subTotal, shipping, discount, and totalTaxes are fixed point integers. For ex: $13.89 => 1389
*/
const calculateCartTotal = (cart) => {
const cartItems = get(cart, 'items', [])
Expand All @@ -24,20 +26,29 @@ const calculateCartTotal = (cart) => {
}, 0)

const shipping = get(cart, 'shipping.amount', 0)

//The attribute 'taxRate' of the 'cart' object contains the tax rate as a fixed point number with a "scaling factor" of 1/100
//In other words, this number will need to be divided by 100 to get the tax rate in percentage.
//[Sources: 'dshop/shop/src/pages/admin/settings/checkout/_CountryTaxEntry.js', 'dshop/shop/src/utils/formHelpers.js']
const taxRate = parseFloat(get(cart, 'taxRate', 0))
const totalTaxes = Math.ceil(((taxRate / 100) * subTotal) / 100)

const discountObj = get(cart, 'discountObj', {})
const {
minCartValue,
maxDiscountValue,
discountType,
excludeShipping
excludeShipping,
excludeTaxes
} = discountObj

let discount = 0

const preDiscountTotal =
get(cart, 'subTotal', 0) + (excludeShipping ? 0 : shipping)
subTotal +
(excludeShipping ? 0 : shipping) +
(excludeTaxes ? 0 : totalTaxes)

if (!minCartValue || preDiscountTotal > minCartValue * 100) {
// Calculate discounts only if minCartValue constraint is met

Expand Down Expand Up @@ -68,7 +79,6 @@ const calculateCartTotal = (cart) => {

const donation = get(cart, 'donation', 0)

const totalTaxes = Math.ceil(taxRate * subTotal)
const total = Math.max(
0,
subTotal + shipping - discount + donation + totalTaxes
Expand Down
2 changes: 1 addition & 1 deletion shop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@origin/ipfs": "^0.1.0",
"@origin/services": "^0.1.0",
"@origin/utils": "^0.1.0",
"@popperjs/core": "^2.6.0",
"@popperjs/core": "^2.9.1",
"@uphold/uphold-sdk-javascript": "^2.4.0",
"@web3-react/core": "^6.1.1",
"@web3-react/injected-connector": "^6.0.7",
Expand Down
Loading

0 comments on commit 43904f9

Please sign in to comment.