mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-04-14 19:56:23 +00:00
Merge branch 'develop' into greenkeeper/update-to-node-10
This commit is contained in:
commit
b7e601be16
13 changed files with 989 additions and 1206 deletions
1686
package-lock.json
generated
1686
package-lock.json
generated
File diff suppressed because it is too large
Load diff
46
package.json
46
package.json
|
|
@ -9,26 +9,26 @@
|
|||
"amazon-payments": "^0.2.6",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"autoprefixer": "^8.2.0",
|
||||
"aws-sdk": "^2.224.1",
|
||||
"autoprefixer": "^8.3.0",
|
||||
"aws-sdk": "^2.229.1",
|
||||
"axios": "^0.18.0",
|
||||
"axios-progress-bar": "^1.1.8",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-eslint": "^8.2.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.16.0",
|
||||
"babel-plugin-transform-regenerator": "^6.16.1",
|
||||
"babel-polyfill": "^6.6.1",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-register": "^6.6.0",
|
||||
"babel-runtime": "^6.11.6",
|
||||
"bcrypt": "^1.0.2",
|
||||
"bcrypt": "^2.0.0",
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "^4.1.0",
|
||||
"bootstrap-vue": "^2.0.0-rc.6",
|
||||
"bootstrap-vue": "^2.0.0-rc.9",
|
||||
"compression": "^1.7.2",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
|
|
@ -38,7 +38,7 @@
|
|||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"express": "^4.16.3",
|
||||
"express-basic-auth": "^1.1.4",
|
||||
"express-basic-auth": "^1.1.5",
|
||||
"express-validator": "^5.1.2",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"glob": "^7.1.2",
|
||||
|
|
@ -52,21 +52,20 @@
|
|||
"hellojs": "^1.15.1",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"image-size": "^0.6.2",
|
||||
"in-app-purchase": "^1.9.0",
|
||||
"intro.js": "^2.6.0",
|
||||
"in-app-purchase": "^1.9.2",
|
||||
"intro.js": "^2.9.3",
|
||||
"jquery": ">=3.0.0",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"memwatch-next": "^0.3.0",
|
||||
"lodash": "^4.17.10",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.22.0",
|
||||
"moment": "^2.22.1",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^5.0.14",
|
||||
"mongoose": "^5.0.16",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^0.14.4",
|
||||
"node-sass": "^4.8.3",
|
||||
"node-sass": "^4.9.0",
|
||||
"nodemailer": "^4.6.4",
|
||||
"ora": "^2.0.0",
|
||||
"pageres": "^4.1.1",
|
||||
|
|
@ -107,7 +106,7 @@
|
|||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
"webpack": "^3.11.0",
|
||||
"webpack-merge": "^4.0.0",
|
||||
"winston": "^2.4.1",
|
||||
"winston": "^2.4.2",
|
||||
"winston-loggly-bulk": "^2.0.2",
|
||||
"xml2js": "^0.4.4"
|
||||
},
|
||||
|
|
@ -141,13 +140,13 @@
|
|||
"apidoc": "gulp apidoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/test-utils": "^1.0.0-beta.13",
|
||||
"@vue/test-utils": "^1.0.0-beta.15",
|
||||
"babel-plugin-istanbul": "^4.1.6",
|
||||
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chalk": "^2.3.2",
|
||||
"chromedriver": "^2.37.0",
|
||||
"chalk": "^2.4.1",
|
||||
"chromedriver": "^2.38.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^3.0.0",
|
||||
"cross-spawn": "^6.0.5",
|
||||
|
|
@ -161,7 +160,7 @@
|
|||
"expect.js": "^0.3.1",
|
||||
"http-proxy-middleware": "^0.18.0",
|
||||
"istanbul": "^1.1.0-alpha.1",
|
||||
"karma": "^2.0.0",
|
||||
"karma": "^2.0.2",
|
||||
"karma-babel-preprocessor": "^7.0.0",
|
||||
"karma-chai-plugins": "^0.9.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
|
|
@ -174,9 +173,9 @@
|
|||
"karma-spec-reporter": "0.0.32",
|
||||
"karma-webpack": "^3.0.0",
|
||||
"lcov-result-merger": "^2.0.0",
|
||||
"mocha": "^5.0.5",
|
||||
"mocha": "^5.1.1",
|
||||
"monk": "^6.0.5",
|
||||
"nightwatch": "^0.9.20",
|
||||
"nightwatch": "^0.9.21",
|
||||
"puppeteer": "^1.3.0",
|
||||
"require-again": "^2.0.0",
|
||||
"selenium-server": "^3.11.0",
|
||||
|
|
@ -185,9 +184,10 @@
|
|||
"sinon-stub-promise": "^4.0.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-dev-middleware": "^2.0.5",
|
||||
"webpack-hot-middleware": "^2.22.0"
|
||||
"webpack-hot-middleware": "^2.22.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"memwatch-next": "^0.3.0",
|
||||
"node-rdkafka": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
141
test/common/ops/buy/buyGem.js
Normal file
141
test/common/ops/buy/buyGem.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
/* eslint-disable camelcase */
|
||||
|
||||
import sinon from 'sinon'; // eslint-disable-line no-shadow
|
||||
import {
|
||||
generateUser,
|
||||
} from '../../../helpers/common.helper';
|
||||
import {
|
||||
BadRequest, NotAuthorized,
|
||||
} from '../../../../website/common/script/libs/errors';
|
||||
import i18n from '../../../../website/common/script/i18n';
|
||||
import {BuyGemOperation} from '../../../../website/common/script/ops/buy/buyGem';
|
||||
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
|
||||
|
||||
function buyGem (user, req, analytics) {
|
||||
let buyOp = new BuyGemOperation(user, req, analytics);
|
||||
|
||||
return buyOp.purchase();
|
||||
}
|
||||
|
||||
describe('shared.ops.buyGem', () => {
|
||||
let user;
|
||||
let analytics = {track () {}};
|
||||
let goldPoints = 40;
|
||||
let gemsBought = 40;
|
||||
let userGemAmount = 10;
|
||||
|
||||
beforeEach(() => {
|
||||
user = generateUser({
|
||||
stats: { gp: goldPoints },
|
||||
balance: userGemAmount,
|
||||
purchased: {
|
||||
plan: {
|
||||
gemsBought: 0,
|
||||
customerId: 'costumer-id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
sinon.stub(analytics, 'track');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
analytics.track.restore();
|
||||
});
|
||||
|
||||
context('Gems', () => {
|
||||
it('purchases gems', () => {
|
||||
let [, message] = buyGem(user, {params: {type: 'gems', key: 'gem'}}, analytics);
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', {count: 1}));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases gems with a different language than the default', () => {
|
||||
let [, message] = buyGem(user, {params: {type: 'gems', key: 'gem'}, language: 'de'});
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', {count: 1}, 'de'));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
});
|
||||
|
||||
it('makes bulk purchases of gems', () => {
|
||||
let [, message] = buyGem(user, {
|
||||
params: {type: 'gems', key: 'gem'},
|
||||
quantity: 2,
|
||||
});
|
||||
|
||||
expect(message).to.equal(i18n.t('plusGem', {count: 2}));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.50);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(2);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
|
||||
});
|
||||
|
||||
|
||||
context('Failure conditions', () => {
|
||||
it('returns an error when key is not provided', (done) => {
|
||||
try {
|
||||
buyGem(user, {params: {type: 'gems'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('missingKeyParam'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents unsubscribed user from buying gems', (done) => {
|
||||
delete user.purchased.plan.customerId;
|
||||
|
||||
try {
|
||||
buyGem(user, {params: {type: 'gems', key: 'gem'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('mustSubscribeToPurchaseGems'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents user with not enough gold from buying gems', (done) => {
|
||||
user.stats.gp = 15;
|
||||
|
||||
try {
|
||||
buyGem(user, {params: {type: 'gems', key: 'gem'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents user that have reached the conversion cap from buying gems', (done) => {
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = gemsBought;
|
||||
|
||||
try {
|
||||
buyGem(user, {params: {type: 'gems', key: 'gem'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('reachedGoldToGemCap', {convCap: planGemLimits.convCap}));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents user from buying an invalid quantity', (done) => {
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = gemsBought;
|
||||
|
||||
try {
|
||||
buyGem(user, {params: {type: 'gems', key: 'gem'}, quantity: 'a'});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import purchase from '../../../../website/common/script/ops/buy/purchase';
|
||||
import pinnedGearUtils from '../../../../website/common/script/ops/pinnedGearUtils';
|
||||
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
|
|
@ -17,7 +16,6 @@ describe('shared.ops.purchase', () => {
|
|||
const SEASONAL_FOOD = 'Meat';
|
||||
let user;
|
||||
let goldPoints = 40;
|
||||
let gemsBought = 40;
|
||||
let analytics = {track () {}};
|
||||
|
||||
before(() => {
|
||||
|
|
@ -45,63 +43,6 @@ describe('shared.ops.purchase', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('returns an error when key is not provided', (done) => {
|
||||
try {
|
||||
purchase(user, {params: {type: 'gems'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('keyRequired'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents unsubscribed user from buying gems', (done) => {
|
||||
try {
|
||||
purchase(user, {params: {type: 'gems', key: 'gem'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('mustSubscribeToPurchaseGems'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents user with not enough gold from buying gems', (done) => {
|
||||
user.purchased.plan.customerId = 'customer-id';
|
||||
|
||||
try {
|
||||
purchase(user, {params: {type: 'gems', key: 'gem'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('messageNotEnoughGold'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents user that have reached the conversion cap from buying gems', (done) => {
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = gemsBought;
|
||||
|
||||
try {
|
||||
purchase(user, {params: {type: 'gems', key: 'gem'}});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('reachedGoldToGemCap', {convCap: planGemLimits.convCap}));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents user from buying an invalid quantity', (done) => {
|
||||
user.stats.gp = goldPoints;
|
||||
user.purchased.plan.gemsBought = gemsBought;
|
||||
|
||||
try {
|
||||
purchase(user, {params: {type: 'gems', key: 'gem'}, quantity: 'a'});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns error when unknown type is provided', (done) => {
|
||||
try {
|
||||
|
|
@ -185,25 +126,6 @@ describe('shared.ops.purchase', () => {
|
|||
user.pinnedItems.push({type: 'bundles', key: 'featheredFriends'});
|
||||
});
|
||||
|
||||
it('purchases gems', () => {
|
||||
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}}, analytics);
|
||||
|
||||
expect(message).to.equal(i18n.t('plusOneGem'));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.25);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(1);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
|
||||
expect(analytics.track).to.be.calledOnce;
|
||||
});
|
||||
|
||||
it('purchases gems with a different language than the default', () => {
|
||||
let [, message] = purchase(user, {params: {type: 'gems', key: 'gem'}, language: 'de'});
|
||||
|
||||
expect(message).to.equal(i18n.t('plusOneGem', 'de'));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.5);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(2);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
|
||||
});
|
||||
|
||||
it('purchases eggs', () => {
|
||||
let type = 'eggs';
|
||||
let key = 'Wolf';
|
||||
|
|
@ -307,18 +229,6 @@ describe('shared.ops.purchase', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('makes bulk purchases of gems', () => {
|
||||
let [, message] = purchase(user, {
|
||||
params: {type: 'gems', key: 'gem'},
|
||||
quantity: 2,
|
||||
});
|
||||
|
||||
expect(message).to.equal(i18n.t('plusOneGem'));
|
||||
expect(user.balance).to.equal(userGemAmount + 0.50);
|
||||
expect(user.purchased.plan.gemsBought).to.equal(2);
|
||||
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate * 2);
|
||||
});
|
||||
|
||||
it('makes bulk purchases of eggs', () => {
|
||||
let type = 'eggs';
|
||||
let key = 'TigerCub';
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ describe('shared.ops.sell', () => {
|
|||
sell(user, {params: { type } });
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(BadRequest);
|
||||
expect(err.message).to.equal(i18n.t('keyRequired'));
|
||||
expect(err.message).to.equal(i18n.t('missingKeyParam'));
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@
|
|||
.row.title-row
|
||||
.col-12.col-md-6
|
||||
h3(v-if='userLevel100Plus', v-once, v-html="$t('noMoreAllocate')")
|
||||
h3(v-if='user.stats.points || userLevel100Plus')
|
||||
h3
|
||||
| {{$t('pointsAvailable')}}
|
||||
.counter.badge(v-if='user.stats.points || userLevel100Plus')
|
||||
| {{user.stats.points}}
|
||||
|
|
@ -131,16 +131,21 @@
|
|||
v-model='user.preferences.automaticAllocation',
|
||||
@change='setAutoAllocate()'
|
||||
)
|
||||
|
||||
.row
|
||||
.col-12.col-md-3(v-for='(statInfo, stat) in allocateStatsList')
|
||||
.box.white.row.col-12
|
||||
.col-12
|
||||
.col-12.col-md-8
|
||||
div(:class='stat') {{ $t(stats[stat].title) }}
|
||||
.number {{ user.stats[stat] }}
|
||||
.points {{$t('pts')}}
|
||||
.col-12.col-md-4
|
||||
.up(v-if='user.stats.points', @click='allocate(stat)')
|
||||
div
|
||||
.up(v-if='user.stats.points', @click='allocate(stat)')
|
||||
div
|
||||
.down(@click='deallocate(stat)')
|
||||
.row.save-row
|
||||
.col-12.col-md-6.offset-md-3.text-center
|
||||
button.btn.btn-primary(@click='saveAttributes()', :disabled='loading') {{ this.loading ? $t('loading') : $t('save') }}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
@ -169,6 +174,7 @@
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
equipTypes: {
|
||||
eyewear: this.$t('eyewear'),
|
||||
head: this.$t('headgearCapitalized'),
|
||||
|
|
@ -206,10 +212,18 @@
|
|||
popover: 'perText',
|
||||
},
|
||||
},
|
||||
|
||||
statsAtStart: {
|
||||
str: 0,
|
||||
int: 0,
|
||||
con: 0,
|
||||
per: 0,
|
||||
},
|
||||
content: Content,
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.statsAtStart = Object.assign({}, this.user.stats);
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
flatGear: 'content.gear.flat',
|
||||
|
|
@ -271,14 +285,32 @@
|
|||
|
||||
return display;
|
||||
},
|
||||
|
||||
formatOutOfTotalDisplay (stat, totalStat) {
|
||||
let display = `${stat}/${totalStat}`;
|
||||
return display;
|
||||
},
|
||||
allocate (stat) {
|
||||
allocate(this.user, {query: { stat }});
|
||||
axios.post(`/api/v3/user/allocate?stat=${stat}`);
|
||||
},
|
||||
deallocate (stat) {
|
||||
if (this.user.stats[stat] === 0) return;
|
||||
this.user.stats[stat] -= 1;
|
||||
this.user.stats.points += 1;
|
||||
},
|
||||
async saveAttributes () {
|
||||
this.loading = true;
|
||||
|
||||
const statUpdates = {};
|
||||
['str', 'int', 'per', 'con'].forEach(stat => {
|
||||
const diff = this.user.stats[stat] - this.statsAtStart[stat];
|
||||
statUpdates[stat] = diff;
|
||||
});
|
||||
|
||||
await axios.post('/api/v3/user/allocate-bulk', {
|
||||
stats: statUpdates,
|
||||
});
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
allocateNow () {
|
||||
autoAllocate(this.user);
|
||||
|
|
@ -384,20 +416,27 @@
|
|||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.up {
|
||||
.up, .down {
|
||||
border: solid #a5a1ac;
|
||||
border-width: 0 3px 3px 0;
|
||||
display: inline-block;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.up:hover, .down:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.up {
|
||||
transform: rotate(-135deg);
|
||||
-webkit-transform: rotate(-135deg);
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.up:hover {
|
||||
cursor: pointer;
|
||||
.down {
|
||||
transform: rotate(45deg);
|
||||
-webkit-transform: rotate(45deg);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -457,4 +496,7 @@
|
|||
margin-top: -0.2em !important;
|
||||
}
|
||||
|
||||
.save-row {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -93,10 +93,9 @@
|
|||
"mustPurchaseToSet": "Must purchase <%= val %> to set it on <%= key %>.",
|
||||
"typeRequired": "Type is required",
|
||||
"positiveAmountRequired": "Positive amount is required",
|
||||
"keyRequired": "Key is required",
|
||||
"notAccteptedType": "Type must be in [eggs, hatchingPotions, premiumHatchingPotions, food, quests, gear]",
|
||||
"contentKeyNotFound": "Key not found for Content <%= type %>",
|
||||
"plusOneGem": "+1 Gem",
|
||||
"plusGem": "+<%= count %> Gem",
|
||||
"typeNotSellable": "Type is not sellable. Must be one of the following <%= acceptedTypes %>",
|
||||
"userItemsKeyNotFound": "Key not found for user.items <%= type %>",
|
||||
"userItemsNotEnough": "You do not have enough <%= type %>",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"buyGemsGoldText": "Alexander the Merchant will sell you Gems at a cost of 20 Gold per Gem. His monthly shipments are initially capped at 25 Gems per month, but for every 3 consecutive months that you are subscribed, this cap increases by 5 Gems, up to a maximum of 50 Gems per month!",
|
||||
"mustSubscribeToPurchaseGems": "Must subscribe to purchase gems with GP",
|
||||
"reachedGoldToGemCap": "You've reached the Gold=>Gem conversion cap <%= convCap %> for this month. We have this to prevent abuse / farming. The cap resets within the first three days of each month.",
|
||||
"reachedGoldToGemCapQuantity": "Your requested amount <%= quantity %> exceeds the Gold=>Gem conversion cap <%= convCap %> for this month. We have this to prevent abuse / farming. The cap resets within the first three days of each month.",
|
||||
"retainHistory": "Retain additional history entries",
|
||||
"retainHistoryText": "Makes completed To-Dos and task history available for longer.",
|
||||
"doubleDrops": "Daily drop caps doubled",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ export class AbstractBuyOperation {
|
|||
return resultObj;
|
||||
}
|
||||
|
||||
analyticsLabel () {
|
||||
return 'acquire item';
|
||||
}
|
||||
|
||||
sendToAnalytics (additionalData = {}) {
|
||||
// spread-operator produces an "unexpected token" error
|
||||
let analyticsData = _merge(additionalData, {
|
||||
|
|
@ -87,7 +91,7 @@ export class AbstractBuyOperation {
|
|||
analyticsData.quantityPurchased = this.quantity;
|
||||
}
|
||||
|
||||
this.analytics.track('acquire item', analyticsData);
|
||||
this.analytics.track(this.analyticsLabel(), analyticsData);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +104,10 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
|||
return item.value;
|
||||
}
|
||||
|
||||
getIemKey (item) {
|
||||
return item.key;
|
||||
}
|
||||
|
||||
canUserPurchase (user, item) {
|
||||
this.item = item;
|
||||
let itemValue = this.getItemValue(item);
|
||||
|
|
@ -110,20 +118,20 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
|
|||
throw new NotAuthorized(this.i18n('messageNotEnoughGold'));
|
||||
}
|
||||
|
||||
if (item.canOwn && !item.canOwn(user)) {
|
||||
if (item && item.canOwn && !item.canOwn(user)) {
|
||||
throw new NotAuthorized(this.i18n('cannotBuyItem'));
|
||||
}
|
||||
}
|
||||
|
||||
subtractCurrency (user, item, quantity = 1) {
|
||||
subtractCurrency (user, item) {
|
||||
let itemValue = this.getItemValue(item);
|
||||
|
||||
user.stats.gp -= itemValue * quantity;
|
||||
user.stats.gp -= itemValue * this.quantity;
|
||||
}
|
||||
|
||||
analyticsData () {
|
||||
return {
|
||||
itemKey: this.item.key,
|
||||
itemKey: this.getIemKey(this.item),
|
||||
itemType: 'Market',
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: this.getItemValue(this.item),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {BuyQuestWithGoldOperation} from './buyQuest';
|
|||
import buySpecialSpell from './buySpecialSpell';
|
||||
import purchaseOp from './purchase';
|
||||
import hourglassPurchase from './hourglassPurchase';
|
||||
import {BuyGemOperation} from './buyGem';
|
||||
|
||||
// @TODO: remove the req option style. Dependency on express structure is an anti-pattern
|
||||
// We should either have more parms or a set structure validated by a Type checker
|
||||
|
|
@ -45,13 +46,18 @@ module.exports = function buy (user, req = {}, analytics) {
|
|||
buyRes = buyOp.purchase();
|
||||
break;
|
||||
}
|
||||
case 'gems': {
|
||||
const buyOp = new BuyGemOperation(user, req, analytics);
|
||||
|
||||
buyRes = buyOp.purchase();
|
||||
break;
|
||||
}
|
||||
case 'eggs':
|
||||
case 'hatchingPotions':
|
||||
case 'food':
|
||||
case 'quests':
|
||||
case 'gear':
|
||||
case 'bundles':
|
||||
case 'gems':
|
||||
buyRes = purchaseOp(user, req, analytics);
|
||||
break;
|
||||
case 'pets':
|
||||
|
|
|
|||
81
website/common/script/ops/buy/buyGem.js
Normal file
81
website/common/script/ops/buy/buyGem.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import pick from 'lodash/pick';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import {AbstractGoldItemOperation} from './abstractBuyOperation';
|
||||
import get from 'lodash/get';
|
||||
import planGemLimits from '../../libs/planGemLimits';
|
||||
|
||||
export class BuyGemOperation extends AbstractGoldItemOperation {
|
||||
constructor (user, req, analytics) {
|
||||
super(user, req, analytics);
|
||||
}
|
||||
|
||||
multiplePurchaseAllowed () {
|
||||
return true;
|
||||
}
|
||||
|
||||
getItemValue () {
|
||||
return planGemLimits.convRate;
|
||||
}
|
||||
|
||||
getIemKey () {
|
||||
return 'gem';
|
||||
}
|
||||
|
||||
extractAndValidateParams (user, req) {
|
||||
let key = this.key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(this.i18n('missingKeyParam'));
|
||||
|
||||
let convCap = planGemLimits.convCap;
|
||||
convCap += user.purchased.plan.consecutive.gemCapExtra;
|
||||
|
||||
// todo better name?
|
||||
this.convCap = convCap;
|
||||
|
||||
this.canUserPurchase(user);
|
||||
}
|
||||
|
||||
canUserPurchase (user, item) {
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
|
||||
throw new NotAuthorized(this.i18n('mustSubscribeToPurchaseGems'));
|
||||
}
|
||||
|
||||
super.canUserPurchase(user, item);
|
||||
|
||||
if (user.purchased.plan.gemsBought >= this.convCap) {
|
||||
throw new NotAuthorized(this.i18n('reachedGoldToGemCap', {convCap: this.convCap}));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.gemsBought + this.quantity >= this.convCap) {
|
||||
throw new NotAuthorized(this.i18n('reachedGoldToGemCapQuantity', {
|
||||
convCap: this.convCap,
|
||||
quantity: this.quantity,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
executeChanges (user, item) {
|
||||
user.balance += 0.25 * this.quantity;
|
||||
user.purchased.plan.gemsBought += this.quantity;
|
||||
|
||||
this.subtractCurrency(user, item);
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('stats balance')),
|
||||
this.i18n('plusGem', {count: this.quantity}),
|
||||
];
|
||||
}
|
||||
|
||||
analyticsLabel () {
|
||||
return 'purchase gems';
|
||||
}
|
||||
|
||||
analyticsData () {
|
||||
let data = super.analyticsData();
|
||||
data.itemKey = 'gem';
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import get from 'lodash/get';
|
|||
import pick from 'lodash/pick';
|
||||
import forEach from 'lodash/forEach';
|
||||
import splitWhitespace from '../../libs/splitWhitespace';
|
||||
import planGemLimits from '../../libs/planGemLimits';
|
||||
import {
|
||||
NotFound,
|
||||
NotAuthorized,
|
||||
|
|
@ -14,48 +13,6 @@ import {
|
|||
import { removeItemByPath } from '../pinnedGearUtils';
|
||||
import getItemInfo from '../../libs/getItemInfo';
|
||||
|
||||
function buyGems (user, analytics, req, key) {
|
||||
let convRate = planGemLimits.convRate;
|
||||
let convCap = planGemLimits.convCap;
|
||||
convCap += user.purchased.plan.consecutive.gemCapExtra;
|
||||
|
||||
// Some groups limit their members ability to obtain gems
|
||||
// The check is async so it's done on the server (in server/controllers/api-v3/user#purchase)
|
||||
// only and not on the client,
|
||||
// resulting in a purchase that will seem successful until the request hit the server.
|
||||
if (!user.purchased || !user.purchased.plan || !user.purchased.plan.customerId) {
|
||||
throw new NotAuthorized(i18n.t('mustSubscribeToPurchaseGems', req.language));
|
||||
}
|
||||
|
||||
if (user.stats.gp < convRate) {
|
||||
throw new NotAuthorized(i18n.t('messageNotEnoughGold', req.language));
|
||||
}
|
||||
|
||||
if (user.purchased.plan.gemsBought >= convCap) {
|
||||
throw new NotAuthorized(i18n.t('reachedGoldToGemCap', {convCap}, req.language));
|
||||
}
|
||||
|
||||
user.balance += 0.25;
|
||||
user.purchased.plan.gemsBought++;
|
||||
user.stats.gp -= convRate;
|
||||
|
||||
if (analytics) {
|
||||
analytics.track('purchase gems', {
|
||||
uuid: user._id,
|
||||
itemKey: key,
|
||||
acquireMethod: 'Gold',
|
||||
goldCost: convRate,
|
||||
category: 'behavior',
|
||||
headers: req.headers,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
pick(user, splitWhitespace('stats balance')),
|
||||
i18n.t('plusOneGem', req.language),
|
||||
];
|
||||
}
|
||||
|
||||
function getItemAndPrice (user, type, key, req) {
|
||||
let item;
|
||||
let price;
|
||||
|
|
@ -120,15 +77,7 @@ module.exports = function purchase (user, req = {}, analytics) {
|
|||
}
|
||||
|
||||
if (!key) {
|
||||
throw new BadRequest(i18n.t('keyRequired', req.language));
|
||||
}
|
||||
|
||||
if (type === 'gems' && key === 'gem') {
|
||||
let gemResponse;
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
gemResponse = buyGems(user, analytics, req, key);
|
||||
}
|
||||
return gemResponse;
|
||||
throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
}
|
||||
|
||||
if (!acceptedTypes.includes(type)) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ module.exports = function sell (user, req = {}) {
|
|||
}
|
||||
|
||||
if (!key) {
|
||||
throw new BadRequest(i18n.t('keyRequired', req.language));
|
||||
throw new BadRequest(i18n.t('missingKeyParam', req.language));
|
||||
}
|
||||
|
||||
if (ACCEPTEDTYPES.indexOf(type) === -1) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue