From ee8a7f6f59bc2940e207427f10314f9fca868eda Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 25 Mar 2020 17:42:20 +0100 Subject: [PATCH 1/9] add: AIP#6 implementation & tests --- .DS_Store | Bin 0 -> 6148 bytes dump.rdb | Bin 0 -> 769 bytes services/sentry/lib/getPayout.js | 56 +++++++++++++++-- test/index.js | 105 +++++++++++++++++++++++++++++-- 4 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 .DS_Store create mode 100644 dump.rdb diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a2eb533abf551d042629249e6fe5fb378b3b5ee0 GIT binary patch literal 6148 zcmeH~u?oUK42Bc!Ah>jNyu}Cb4Gz&K=nFU~E>c0O^F6wMazU^xC+1b{XuyJ79K1T}(S!u1*@b}wNMJ-@TJzTK|1JE}{6A`8N&+PC zX9Tp_belC^D(=>|*R%RAsLdlOoPH4`nu|ZNZi? zUHTmaagm=jr*mNuC-VI5rx(wrm0>Q$W+IdGlgBH{^cqj2%|Rx467=Mm@oIICBce`p@LS>E~FfVk8t4EHb>Z%sC zG+hmR-{K1@Rdt}6H7RYElXTc`?v%-_O0<6SkoRKos9(fAY%>BXW6Xq^-h3cKY>!T= z&G%PxY(E`+D6NGO3@>mE2^!M~^EC@NpqDG2MZo~yq-M}|9aW<4m0^aStm1>B7}$+Q zFAaI`Ak7OK2}MI2rASJ&fnJyJxIR%jRnH=ZPJN?I7!S2j6Mp1+J`h!cuR1zWcAia_7z zN7%c!>+Llw=nmR`V|N#q$KyIaii-o0@guZ1AE#o!;8`tNU|>*27_0Tl0ORq`S6|;% J8mFI6e*q8(-a-HX literal 0 HcmV?d00001 diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 0d90b419..9a39b4d3 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -1,18 +1,64 @@ +/* eslint-disable no-nested-ternary */ const BN = require('bn.js') const toBalancesKey = require('../toBalancesKey') function getPayout(channel, ev) { if (ev.type === 'IMPRESSION' && ev.publisher) { // add the minimum price for the event to the current amount - return [toBalancesKey(ev.publisher), new BN(channel.spec.minPerImpression || 1)] + const minPrice = new BN(channel.spec.minPerImpression || 1) + const maxPrice = new BN(channel.spec.maxPerImpression || 1) + const price = channel.spec.priceMultiplicationRules + ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) + : new BN(channel.spec.minPerImpression || 1) + return [toBalancesKey(ev.publisher), price] } if (ev.type === 'CLICK' && ev.publisher) { - return [ - toBalancesKey(ev.publisher), - new BN((channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.min) || 0) - ] + const minPrice = new BN( + (channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.min) || 0 + ) + const maxPrice = new BN( + (channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.max) || 0 + ) + const price = channel.spec.priceMultiplicationRules + ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) + : new BN((channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.min) || 0) + return [toBalancesKey(ev.publisher), price] } return null } +function payout(rules, ev, maxPrice, startPrice) { + const match = isRuleMatching.bind(null, ev) + const matchingRules = rules.filter(match) + + let finalPrice = startPrice + + if (matchingRules.length > 0) { + const firstFixed = matchingRules.find(x => x.amount) + const priceByRules = firstFixed + ? new BN(firstFixed.amount) + : startPrice.mul( + matchingRules + .filter(x => x.multiplier) + .map(x => x.multiplier) + .reduce((a, b) => a.mul(b), 1) + ) + finalPrice = BN.min(maxPrice, priceByRules) + } + + return finalPrice +} + +function isRuleMatching(ev, rule) { + return rule.eventType + ? rule.eventType.includes(ev.type) + : true && rule.publisher + ? rule.publisher.includes(ev.publisher) + : true && rule.osType + ? rule.osType.includes(ev.os) + : true && rule.country + ? rule.country.includes(ev.country) + : true +} + module.exports = getPayout diff --git a/test/index.js b/test/index.js index 2dbec6b2..2c3a6917 100755 --- a/test/index.js +++ b/test/index.js @@ -357,11 +357,104 @@ tape('eventReducer: newAggr', function(t) { }) tape('getPayout: get event payouts', function(t) { - const pricingBounds = { CLICK: { min: new BN(23) } } - const channel = { depositAmount: '100', spec: { minPerImpression: '8', pricingBounds } } - t.deepEqual(getPayout(channel, { publisher: 'test1', type: 'IMPRESSION' }), ['test1', new BN(8)]) - t.deepEqual(getPayout(channel, { publisher: 'test2', type: 'CLICK' }), ['test2', new BN(23)]) - t.deepEqual(getPayout(channel, { type: 'CLOSE' }), null) + let i = 0 + const pricingBounds = { CLICK: { min: new BN(23), max: new BN(100) } } + const channel = { + depositAmount: '100', + spec: { minPerImpression: '8', maxPerImpression: '64', pricingBounds } + } + const priceMultiplicationRules = [{ amount: '10', country: ['US'], eventType: ['CLICK'] }] + + t.deepEqual( + getPayout(channel, { publisher: 'test1', type: 'IMPRESSION' }), + ['test1', new BN(8)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout(channel, { publisher: 'test2', type: 'CLICK' }), + ['test2', new BN(23)], + `valid payout ${(i += 1)}` + ) + t.deepEqual(getPayout(channel, { type: 'CLOSE' }), null, `valid payout ${(i += 1)}`) + t.deepEqual( + getPayout( + { ...channel, spec: { ...channel.spec, priceMultiplicationRules } }, + { publisher: 'test1', type: 'IMPRESSION' } + ), + ['test1', new BN(8)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout( + { ...channel, spec: { ...channel.spec, priceMultiplicationRules } }, + { publisher: 'test1', type: 'CLICK', country: 'US' } + ), + ['test1', new BN(10)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout( + { ...channel, spec: { ...channel.spec, priceMultiplicationRules: [{ amount: '10' }] } }, + { publisher: 'test1', type: 'CLICK', country: 'US' } + ), + ['test1', new BN(10)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout( + { ...channel, spec: { ...channel.spec, priceMultiplicationRules: [{ amount: '10000' }] } }, + { publisher: 'test1', type: 'IMPRESSION' } + ), + ['test1', new BN(64)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout( + { ...channel, spec: { ...channel.spec, priceMultiplicationRules: [{ amount: '10000' }] } }, + { publisher: 'test1', type: 'CLICK', country: 'US' } + ), + ['test1', new BN(100)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout( + { + ...channel, + spec: { + ...channel.spec, + priceMultiplicationRules: [ + ...priceMultiplicationRules, + { amount: '12', country: ['US'], eventType: ['CLICK'], publisher: ['test1'] } + ] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US' } + ), + ['test1', new BN(10)], + `valid payout ${(i += 1)}` + ) + t.deepEqual( + getPayout( + { + ...channel, + spec: { + ...channel.spec, + priceMultiplicationRules: [ + { + amount: '12', + country: ['US'], + eventType: ['CLICK'], + publisher: ['test1'], + osType: ['android'] + } + ] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' } + ), + ['test1', new BN(12)], + `valid payout ${(i += 1)}` + ) t.end() }) @@ -412,4 +505,6 @@ tape('eventReducer: reduce', function(t) { t.end() }) +// payout test + // @TODO: producer, possibly leader/follower; mergePayableIntoBalances From 8919cde5046d3bebb1e80f61b14f0ecdf62bdc09 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 25 Mar 2020 17:45:35 +0100 Subject: [PATCH 2/9] fix: remove unused files --- .DS_Store | Bin 6148 -> 0 bytes dump.rdb | Bin 769 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .DS_Store delete mode 100644 dump.rdb diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a2eb533abf551d042629249e6fe5fb378b3b5ee0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~u?oUK42Bc!Ah>jNyu}Cb4Gz&K=nFU~E>c0O^F6wMazU^xC+1b{XuyJ79K1T}(S!u1*@b}wNMJ-@TJzTK|1JE}{6A`8N&+PC zX9Tp_belC^D(=>|*R%RAsLdlOoPH4`nu|ZNZi? zUHTmaagm=jr*mNuC-VI5rx(wrm0>Q$W+IdGlgBH{^cqj2%|Rx467=Mm@oIICBce`p@LS>E~FfVk8t4EHb>Z%sC zG+hmR-{K1@Rdt}6H7RYElXTc`?v%-_O0<6SkoRKos9(fAY%>BXW6Xq^-h3cKY>!T= z&G%PxY(E`+D6NGO3@>mE2^!M~^EC@NpqDG2MZo~yq-M}|9aW<4m0^aStm1>B7}$+Q zFAaI`Ak7OK2}MI2rASJ&fnJyJxIR%jRnH=ZPJN?I7!S2j6Mp1+J`h!cuR1zWcAia_7z zN7%c!>+Llw=nmR`V|N#q$KyIaii-o0@guZ1AE#o!;8`tNU|>*27_0Tl0ORq`S6|;% J8mFI6e*q8(-a-HX From 61b74429a40be0c632700475391e22d15a69c6d6 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 26 Mar 2020 10:41:36 +0100 Subject: [PATCH 3/9] add: test fixtures, description --- package-lock.json | 37 ++--- package.json | 1 + routes/schemas.js | 13 +- services/sentry/eventAggregator.js | 2 +- services/sentry/lib/eventReducer.js | 4 +- services/sentry/lib/getPayout.js | 33 +++-- test/fixtures.js | 204 +++++++++++++++++++++++++++- test/index.js | 109 +-------------- 8 files changed, 266 insertions(+), 137 deletions(-) diff --git a/package-lock.json b/package-lock.json index 672d55fa..7ef80fde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -441,6 +441,11 @@ "tweetnacl": "^0.14.3" } }, + "bignumber.js": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, "bind-obj-methods": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-2.0.0.tgz", @@ -2237,7 +2242,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, "invert-kv": { @@ -2429,7 +2434,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "which": { @@ -2447,7 +2452,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", @@ -2479,7 +2484,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -2499,7 +2504,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "requires": { "cliui": "^4.0.0", @@ -2545,7 +2550,7 @@ }, "get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" }, "get-value": { @@ -3876,7 +3881,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { @@ -3920,7 +3925,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -5471,7 +5476,7 @@ }, "p-is-promise": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=" }, "p-limit": { @@ -5537,7 +5542,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { @@ -5960,7 +5965,7 @@ }, "resolve": { "version": "1.7.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "resolved": "http://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", "dev": true, "requires": { @@ -6629,7 +6634,7 @@ }, "strip-eof": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" }, "strip-hex-prefix": { @@ -6860,7 +6865,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -7223,7 +7228,7 @@ }, "uuid": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", + "resolved": "http://registry.npmjs.org/uuid/-/uuid-2.0.1.tgz", "integrity": "sha1-wqMN7bPlNdcsz4LjQ5QaULqFM6w=" }, "validate-npm-package-license": { @@ -7322,7 +7327,7 @@ }, "wrap-ansi": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", "requires": { "string-width": "^1.0.1", @@ -7354,7 +7359,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" diff --git a/package.json b/package.json index fed7e5a0..6a9c18a6 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dependencies": { "adex-protocol-eth": "^4.1.3", "base64url": "^3.0.1", + "bignumber.js": "^9.0.0", "bn.js": "^4.11.8", "body-parser": "^1.18.3", "celebrate": "^9.0.2", diff --git a/routes/schemas.js b/routes/schemas.js index dfc527ff..0d9f551f 100644 --- a/routes/schemas.js +++ b/routes/schemas.js @@ -119,7 +119,18 @@ module.exports = { eventSubmission: Joi.object({ allow: Joi.array().items(Joi.object()) }), nonce: Joi.string(), created: Joi.number(), - activeFrom: Joi.number() + activeFrom: Joi.number(), + priceMultiplicationRules: Joi.array().items( + Joi.object({ + multiplier: Joi.number().precision(10), // max 10 decimal places + amount: numericString, + evType: Joi.array().items(Joi.string().lowercase()), + country: Joi.array().items(Joi.string().lowercase()), + publisher: Joi.array().items(Joi.string().lowercase()), + osType: Joi.array().items(Joi.string().lowercase()) + }) + ), + priceDynamicAdjustment: Joi.bool() }).required() }, validatorMessage: { diff --git a/services/sentry/eventAggregator.js b/services/sentry/eventAggregator.js index b8ac79ab..be975b2e 100644 --- a/services/sentry/eventAggregator.js +++ b/services/sentry/eventAggregator.js @@ -73,7 +73,7 @@ function makeRecorder(channelId) { // this will be saved in the channel object, which is passed into the eventReducer // Record the events - aggr = events.reduce(eventReducer.reduce.bind(null, channel), aggr) + aggr = events.reduce(eventReducer.reduce.bind(null, channel, session), aggr) if (cfg.AGGR_THROTTLE) { throttledPersistAndReset() return { success: true } diff --git a/services/sentry/lib/eventReducer.js b/services/sentry/lib/eventReducer.js index 8b627b78..6524a1d8 100644 --- a/services/sentry/lib/eventReducer.js +++ b/services/sentry/lib/eventReducer.js @@ -6,9 +6,9 @@ function newAggr(channelId) { return { channelId, created: new Date(), events: {}, totals: {}, earners: [] } } -function reduce(channel, initialAggr, ev) { +function reduce(channel, session, initialAggr, event) { let aggr = { ...initialAggr } - + const ev = { ...event, ...session } // add session details (i.e country, device type etc) to event const payout = getPayout(channel, ev) if (payout) { aggr.events[ev.type] = mergeEv(initialAggr.events[ev.type], payout) diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 9a39b4d3..07bff391 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -1,4 +1,5 @@ /* eslint-disable no-nested-ternary */ +// const BN = require('bignumber.js') // allows const BN = require('bn.js') const toBalancesKey = require('../toBalancesKey') @@ -10,7 +11,7 @@ function getPayout(channel, ev) { const price = channel.spec.priceMultiplicationRules ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) : new BN(channel.spec.minPerImpression || 1) - return [toBalancesKey(ev.publisher), price] + return [toBalancesKey(ev.publisher), new BN(price.toString())] } if (ev.type === 'CLICK' && ev.publisher) { const minPrice = new BN( @@ -22,7 +23,7 @@ function getPayout(channel, ev) { const price = channel.spec.priceMultiplicationRules ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) : new BN((channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.min) || 0) - return [toBalancesKey(ev.publisher), price] + return [toBalancesKey(ev.publisher), new BN(price.toString())] } return null } @@ -30,19 +31,25 @@ function getPayout(channel, ev) { function payout(rules, ev, maxPrice, startPrice) { const match = isRuleMatching.bind(null, ev) const matchingRules = rules.filter(match) - let finalPrice = startPrice if (matchingRules.length > 0) { + const divisionExponent = new BN(10).pow(new BN(18, 10)) const firstFixed = matchingRules.find(x => x.amount) const priceByRules = firstFixed ? new BN(firstFixed.amount) - : startPrice.mul( - matchingRules - .filter(x => x.multiplier) - .map(x => x.multiplier) - .reduce((a, b) => a.mul(b), 1) - ) + : startPrice + .mul( + new BN( + ( + matchingRules + .filter(x => x.multiplier) + .map(x => x.multiplier) + .reduce((a, b) => a * b, 1) * 1e18 + ).toString() + ) + ) + .div(divisionExponent) finalPrice = BN.min(maxPrice, priceByRules) } @@ -51,13 +58,13 @@ function payout(rules, ev, maxPrice, startPrice) { function isRuleMatching(ev, rule) { return rule.eventType - ? rule.eventType.includes(ev.type) + ? rule.eventType.includes(ev.type.toLowerCase()) : true && rule.publisher - ? rule.publisher.includes(ev.publisher) + ? rule.publisher.includes(ev.publisher.toLowerCase()) : true && rule.osType - ? rule.osType.includes(ev.os) + ? rule.osType.includes(ev.os.toLowerCase()) : true && rule.country - ? rule.country.includes(ev.country) + ? rule.country.includes(ev.country.toLowerCase()) : true } diff --git a/test/fixtures.js b/test/fixtures.js index b06f69be..978e1308 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -1,3 +1,4 @@ +const BN = require('bn.js') const dummyVals = require('./prep-db/mongo') const validatorMessage = { @@ -8,6 +9,15 @@ const validatorMessage = { balances: { myAwesomePublisher: '214000000000000000000000', anotherPublisher: '2' } } +const payoutChannel = { + depositAmount: '100', + spec: { + minPerImpression: '8', + maxPerImpression: '64', + pricingBounds: { CLICK: { min: new BN(23), max: new BN(100) } } + } +} + module.exports = { createChannel: [ [ @@ -413,5 +423,197 @@ module.exports = { `ValidationError: "value" at position 0 fails because [child "channelId" fails because ["channelId" is required]]` ] ] - } + }, + payoutRules: [ + [ + { + depositAmount: '100', + spec: { + minPerImpression: '8', + maxPerImpression: '64', + pricingBounds: { CLICK: { min: new BN(23), max: new BN(100) } } + } + }, + { publisher: 'test1', type: 'IMPRESSION' }, + ['test1', new BN(8)], + `pricingBounds: impression event` + ], + [ + { + depositAmount: '100', + spec: { + minPerImpression: '8', + maxPerImpression: '64', + pricingBounds: { CLICK: { min: new BN(23), max: new BN(100) } } + } + }, + { publisher: 'test2', type: 'CLICK' }, + ['test2', new BN(23)], + `pricingBounds: click event` + ], + [ + { + depositAmount: '100', + spec: { + minPerImpression: '8', + maxPerImpression: '64', + pricingBounds: { CLICK: { min: new BN(23), max: new BN(100) } } + } + }, + { type: 'CLOSE' }, + null, + `pricingBounds: close event ` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [{ amount: '10', country: ['us'], eventType: ['click'] }] + } + }, + { publisher: 'test1', type: 'IMPRESSION' }, + ['test1', new BN(8)], + `fixedAmount: impression` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [{ amount: '10', country: ['us'], eventType: ['click'] }] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US' }, + ['test1', new BN(10)], + `fixedAmount (country, publisher): click` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [{ amount: '10' }] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US' }, + ['test1', new BN(10)], + `fixedAmount (all): click` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [{ amount: '10000' }] + } + }, + { publisher: 'test1', type: 'IMPRESSION' }, + ['test1', new BN(64)], + `fixedAmount (all): price should not exceed maxPerImpressionPrice` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [{ amount: '10000' }] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US' }, + ['test1', new BN(100)], + `fixedAmount (all): price should not exceed event pricingBound max` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [ + { amount: '10', country: ['us'], eventType: ['click'] }, + { amount: '12', country: ['us'], eventType: ['click'], publisher: ['test1'] } + ] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US' }, + ['test1', new BN(10)], + `fixedAmount (country, pulisher): should choose first fixedAmount rule` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [ + { + amount: '15', + country: ['us'], + eventType: ['click'], + publisher: ['test1'], + osType: ['android'] + } + ] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' }, + ['test1', new BN(15)], + `fixedAmount (country, pulisher, osType): click` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + priceMultiplicationRules: [ + { + multiplier: 1.2, + country: ['us'], + eventType: ['click'], + publisher: ['test1'], + osType: ['android'] + }, + { + amount: '12', + country: ['us'], + eventType: ['click'], + publisher: ['test1'], + osType: ['android'] + } + ] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' }, + ['test1', new BN(12)], + `fixedAmount (country, osType, publisher): choose fixedAmount rule over multiplier if present` + ], + [ + { + ...payoutChannel, + spec: { + ...payoutChannel.spec, + pricingBounds: { + CLICK: { + min: new BN((1e18).toString()).toString(), + max: new BN((100e18).toString()).toString() + } + }, + priceMultiplicationRules: [ + { + multiplier: 1.2, + country: ['us'], + eventType: ['click'], + publisher: ['test1'], + osType: ['android'] + }, + { + multiplier: 1.2 + } + ] + } + }, + { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' }, + ['test1', new BN('1440000000000000000')], + `multiplier (country, osType, publisher | all) - apply all multiplier rules` + ] + ] } diff --git a/test/index.js b/test/index.js index 2c3a6917..761a68ad 100755 --- a/test/index.js +++ b/test/index.js @@ -357,104 +357,9 @@ tape('eventReducer: newAggr', function(t) { }) tape('getPayout: get event payouts', function(t) { - let i = 0 - const pricingBounds = { CLICK: { min: new BN(23), max: new BN(100) } } - const channel = { - depositAmount: '100', - spec: { minPerImpression: '8', maxPerImpression: '64', pricingBounds } - } - const priceMultiplicationRules = [{ amount: '10', country: ['US'], eventType: ['CLICK'] }] - - t.deepEqual( - getPayout(channel, { publisher: 'test1', type: 'IMPRESSION' }), - ['test1', new BN(8)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout(channel, { publisher: 'test2', type: 'CLICK' }), - ['test2', new BN(23)], - `valid payout ${(i += 1)}` - ) - t.deepEqual(getPayout(channel, { type: 'CLOSE' }), null, `valid payout ${(i += 1)}`) - t.deepEqual( - getPayout( - { ...channel, spec: { ...channel.spec, priceMultiplicationRules } }, - { publisher: 'test1', type: 'IMPRESSION' } - ), - ['test1', new BN(8)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout( - { ...channel, spec: { ...channel.spec, priceMultiplicationRules } }, - { publisher: 'test1', type: 'CLICK', country: 'US' } - ), - ['test1', new BN(10)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout( - { ...channel, spec: { ...channel.spec, priceMultiplicationRules: [{ amount: '10' }] } }, - { publisher: 'test1', type: 'CLICK', country: 'US' } - ), - ['test1', new BN(10)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout( - { ...channel, spec: { ...channel.spec, priceMultiplicationRules: [{ amount: '10000' }] } }, - { publisher: 'test1', type: 'IMPRESSION' } - ), - ['test1', new BN(64)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout( - { ...channel, spec: { ...channel.spec, priceMultiplicationRules: [{ amount: '10000' }] } }, - { publisher: 'test1', type: 'CLICK', country: 'US' } - ), - ['test1', new BN(100)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout( - { - ...channel, - spec: { - ...channel.spec, - priceMultiplicationRules: [ - ...priceMultiplicationRules, - { amount: '12', country: ['US'], eventType: ['CLICK'], publisher: ['test1'] } - ] - } - }, - { publisher: 'test1', type: 'CLICK', country: 'US' } - ), - ['test1', new BN(10)], - `valid payout ${(i += 1)}` - ) - t.deepEqual( - getPayout( - { - ...channel, - spec: { - ...channel.spec, - priceMultiplicationRules: [ - { - amount: '12', - country: ['US'], - eventType: ['CLICK'], - publisher: ['test1'], - osType: ['android'] - } - ] - } - }, - { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' } - ), - ['test1', new BN(12)], - `valid payout ${(i += 1)}` - ) + fixtures.payoutRules.forEach(([channel, event, expectedResult, message]) => { + t.deepEqual(getPayout(channel, event), expectedResult, message) + }) t.end() }) @@ -474,10 +379,10 @@ tape('eventReducer: reduce', function(t) { // reduce 100 events for (let i = 0; i < 100; i += 1) { - eventReducer.reduce(channel, aggr, event) + eventReducer.reduce(channel, {}, aggr, event) } - const result = eventReducer.reduce(channel, aggr, event) + const result = eventReducer.reduce(channel, {}, aggr, event) t.equal(result.channelId, channel.id, 'should have same channel id') t.equal( @@ -491,7 +396,7 @@ tape('eventReducer: reduce', function(t) { 'should have the correct number of eventsPayouts' ) - const closeReduce = eventReducer.reduce(channel, aggr, { + const closeReduce = eventReducer.reduce(channel, {}, aggr, { type: 'CLOSE' }) @@ -505,6 +410,4 @@ tape('eventReducer: reduce', function(t) { t.end() }) -// payout test - // @TODO: producer, possibly leader/follower; mergePayableIntoBalances From 55a481c75eb6b05aa3b835188061c929c174869e Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Thu, 26 Mar 2020 10:56:45 +0100 Subject: [PATCH 4/9] fix: remove unused package --- package-lock.json | 5 ----- package.json | 1 - 2 files changed, 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ef80fde..9734df61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -441,11 +441,6 @@ "tweetnacl": "^0.14.3" } }, - "bignumber.js": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", - "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" - }, "bind-obj-methods": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/bind-obj-methods/-/bind-obj-methods-2.0.0.tgz", diff --git a/package.json b/package.json index 6a9c18a6..fed7e5a0 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "dependencies": { "adex-protocol-eth": "^4.1.3", "base64url": "^3.0.1", - "bignumber.js": "^9.0.0", "bn.js": "^4.11.8", "body-parser": "^1.18.3", "celebrate": "^9.0.2", From 8d1a6db77f48cba97b19f272c5b4525e0130c930 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Fri, 27 Mar 2020 13:10:51 +0100 Subject: [PATCH 5/9] fix: refactor price bounds --- routes/schemas.js | 2 +- services/sentry/lib/getPayout.js | 33 ++++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/routes/schemas.js b/routes/schemas.js index 0d9f551f..e6e3cfd9 100644 --- a/routes/schemas.js +++ b/routes/schemas.js @@ -126,7 +126,7 @@ module.exports = { amount: numericString, evType: Joi.array().items(Joi.string().lowercase()), country: Joi.array().items(Joi.string().lowercase()), - publisher: Joi.array().items(Joi.string().lowercase()), + publisher: Joi.array().items(Joi.string()), osType: Joi.array().items(Joi.string().lowercase()) }) ), diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 07bff391..93a5d372 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -6,23 +6,17 @@ const toBalancesKey = require('../toBalancesKey') function getPayout(channel, ev) { if (ev.type === 'IMPRESSION' && ev.publisher) { // add the minimum price for the event to the current amount - const minPrice = new BN(channel.spec.minPerImpression || 1) - const maxPrice = new BN(channel.spec.maxPerImpression || 1) + const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) const price = channel.spec.priceMultiplicationRules ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) - : new BN(channel.spec.minPerImpression || 1) + : minPrice return [toBalancesKey(ev.publisher), new BN(price.toString())] } if (ev.type === 'CLICK' && ev.publisher) { - const minPrice = new BN( - (channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.min) || 0 - ) - const maxPrice = new BN( - (channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.max) || 0 - ) + const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) const price = channel.spec.priceMultiplicationRules ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) - : new BN((channel.spec.pricingBounds && channel.spec.pricingBounds.CLICK.min) || 0) + : minPrice return [toBalancesKey(ev.publisher), new BN(price.toString())] } return null @@ -68,4 +62,23 @@ function isRuleMatching(ev, rule) { : true } +function getPriceBounds(spec, evType) { + const { pricingBounds, minPerImpression, maxPerImpression } = spec + if (evType === 'IMPRESSION') { + return ( + (pricingBounds && + pricingBounds[evType] && [ + new BN(pricingBounds[evType].min), + new BN(pricingBounds[evType].max) + ]) || [new BN(minPerImpression || 1), new BN(maxPerImpression || 1)] + ) + } + return ( + (pricingBounds && + pricingBounds[evType] && [ + new BN(pricingBounds[evType].min), + new BN(pricingBounds[evType].max) + ]) || [new BN(0), new BN(0)] + ) +} module.exports = getPayout From 7c5a2ab6460b8e55fceef52d194a871b334a79e7 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Mon, 30 Mar 2020 11:32:14 +0100 Subject: [PATCH 6/9] fix: pass session param eventReducer --- routes/schemas.js | 2 +- services/sentry/lib/getPayout.js | 18 +++---- test/fixtures.js | 80 +++++++++++++++++++++----------- test/index.js | 6 +-- 4 files changed, 66 insertions(+), 40 deletions(-) diff --git a/routes/schemas.js b/routes/schemas.js index e6e3cfd9..1c9ed919 100644 --- a/routes/schemas.js +++ b/routes/schemas.js @@ -114,7 +114,7 @@ module.exports = { .keys() .pattern( /^(IMPRESSION|CLICK)$/, - Joi.object({ min: numericString.default('0'), max: numericString.default('0') }) + Joi.object({ min: numericString.required(), max: numericString.required() }) ), eventSubmission: Joi.object({ allow: Joi.array().items(Joi.object()) }), nonce: Joi.string(), diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 93a5d372..70273068 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -3,27 +3,27 @@ const BN = require('bn.js') const toBalancesKey = require('../toBalancesKey') -function getPayout(channel, ev) { +function getPayout(channel, ev, session) { if (ev.type === 'IMPRESSION' && ev.publisher) { // add the minimum price for the event to the current amount const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) const price = channel.spec.priceMultiplicationRules - ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) + ? payout(channel.spec.priceMultiplicationRules, ev, session, maxPrice, minPrice) : minPrice return [toBalancesKey(ev.publisher), new BN(price.toString())] } if (ev.type === 'CLICK' && ev.publisher) { const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) const price = channel.spec.priceMultiplicationRules - ? payout(channel.spec.priceMultiplicationRules, ev, maxPrice, minPrice) + ? payout(channel.spec.priceMultiplicationRules, ev, session, maxPrice, minPrice) : minPrice return [toBalancesKey(ev.publisher), new BN(price.toString())] } return null } -function payout(rules, ev, maxPrice, startPrice) { - const match = isRuleMatching.bind(null, ev) +function payout(rules, ev, session, maxPrice, startPrice) { + const match = isRuleMatching.bind(null, ev, session) const matchingRules = rules.filter(match) let finalPrice = startPrice @@ -50,15 +50,15 @@ function payout(rules, ev, maxPrice, startPrice) { return finalPrice } -function isRuleMatching(ev, rule) { +function isRuleMatching(ev, session, rule) { return rule.eventType ? rule.eventType.includes(ev.type.toLowerCase()) : true && rule.publisher - ? rule.publisher.includes(ev.publisher.toLowerCase()) + ? rule.publisher.includes(ev.publisher) : true && rule.osType - ? rule.osType.includes(ev.os.toLowerCase()) + ? rule.osType.includes(session.os && session.os.toLowerCase()) : true && rule.country - ? rule.country.includes(ev.country.toLowerCase()) + ? rule.country.includes(session.country && session.country.toLowerCase()) : true } diff --git a/test/fixtures.js b/test/fixtures.js index 978e1308..656c4a4d 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -434,8 +434,9 @@ module.exports = { pricingBounds: { CLICK: { min: new BN(23), max: new BN(100) } } } }, - { publisher: 'test1', type: 'IMPRESSION' }, - ['test1', new BN(8)], + { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'IMPRESSION' }, + {}, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(8)], `pricingBounds: impression event` ], [ @@ -447,8 +448,9 @@ module.exports = { pricingBounds: { CLICK: { min: new BN(23), max: new BN(100) } } } }, - { publisher: 'test2', type: 'CLICK' }, - ['test2', new BN(23)], + { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'CLICK' }, + {}, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(23)], `pricingBounds: click event` ], [ @@ -461,6 +463,7 @@ module.exports = { } }, { type: 'CLOSE' }, + {}, null, `pricingBounds: close event ` ], @@ -472,8 +475,9 @@ module.exports = { priceMultiplicationRules: [{ amount: '10', country: ['us'], eventType: ['click'] }] } }, - { publisher: 'test1', type: 'IMPRESSION' }, - ['test1', new BN(8)], + { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'IMPRESSION' }, + {}, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(8)], `fixedAmount: impression` ], [ @@ -484,8 +488,9 @@ module.exports = { priceMultiplicationRules: [{ amount: '10', country: ['us'], eventType: ['click'] }] } }, - { publisher: 'test1', type: 'CLICK', country: 'US' }, - ['test1', new BN(10)], + { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'CLICK' }, + { country: 'US' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(10)], `fixedAmount (country, publisher): click` ], [ @@ -496,8 +501,9 @@ module.exports = { priceMultiplicationRules: [{ amount: '10' }] } }, - { publisher: 'test1', type: 'CLICK', country: 'US' }, - ['test1', new BN(10)], + { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'CLICK' }, + { country: 'US' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(10)], `fixedAmount (all): click` ], [ @@ -508,8 +514,9 @@ module.exports = { priceMultiplicationRules: [{ amount: '10000' }] } }, - { publisher: 'test1', type: 'IMPRESSION' }, - ['test1', new BN(64)], + { publisher: '0xce07cbb7e054514d590a0262c93070d838bfba2e', type: 'IMPRESSION' }, + {}, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(64)], `fixedAmount (all): price should not exceed maxPerImpressionPrice` ], [ @@ -520,8 +527,9 @@ module.exports = { priceMultiplicationRules: [{ amount: '10000' }] } }, - { publisher: 'test1', type: 'CLICK', country: 'US' }, - ['test1', new BN(100)], + { publisher: '0xce07cbb7e054514d590a0262c93070d838bfba2e', type: 'CLICK' }, + { country: 'US' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(100)], `fixedAmount (all): price should not exceed event pricingBound max` ], [ @@ -531,12 +539,18 @@ module.exports = { ...payoutChannel.spec, priceMultiplicationRules: [ { amount: '10', country: ['us'], eventType: ['click'] }, - { amount: '12', country: ['us'], eventType: ['click'], publisher: ['test1'] } + { + amount: '12', + country: ['us'], + eventType: ['click'], + publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'] + } ] } }, - { publisher: 'test1', type: 'CLICK', country: 'US' }, - ['test1', new BN(10)], + { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'CLICK' }, + { country: 'US' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(10)], `fixedAmount (country, pulisher): should choose first fixedAmount rule` ], [ @@ -549,14 +563,18 @@ module.exports = { amount: '15', country: ['us'], eventType: ['click'], - publisher: ['test1'], + publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] } ] } }, - { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' }, - ['test1', new BN(15)], + { + publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', + type: 'CLICK' + }, + { country: 'US', osType: 'android' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(15)], `fixedAmount (country, pulisher, osType): click` ], [ @@ -569,21 +587,25 @@ module.exports = { multiplier: 1.2, country: ['us'], eventType: ['click'], - publisher: ['test1'], + publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] }, { amount: '12', country: ['us'], eventType: ['click'], - publisher: ['test1'], + publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] } ] } }, - { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' }, - ['test1', new BN(12)], + { + publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', + type: 'CLICK' + }, + { country: 'US', osType: 'android' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN(12)], `fixedAmount (country, osType, publisher): choose fixedAmount rule over multiplier if present` ], [ @@ -602,7 +624,7 @@ module.exports = { multiplier: 1.2, country: ['us'], eventType: ['click'], - publisher: ['test1'], + publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] }, { @@ -611,8 +633,12 @@ module.exports = { ] } }, - { publisher: 'test1', type: 'CLICK', country: 'US', osType: 'android' }, - ['test1', new BN('1440000000000000000')], + { + publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', + type: 'CLICK' + }, + { country: 'US', osType: 'android' }, + ['0xce07cbb7e054514d590a0262c93070d838bfba2e', new BN('1440000000000000000')], `multiplier (country, osType, publisher | all) - apply all multiplier rules` ] ] diff --git a/test/index.js b/test/index.js index 761a68ad..eaba0c3a 100755 --- a/test/index.js +++ b/test/index.js @@ -356,9 +356,9 @@ tape('eventReducer: newAggr', function(t) { t.end() }) -tape('getPayout: get event payouts', function(t) { - fixtures.payoutRules.forEach(([channel, event, expectedResult, message]) => { - t.deepEqual(getPayout(channel, event), expectedResult, message) +tape.only('getPayout: get event payouts', function(t) { + fixtures.payoutRules.forEach(([channel, event, session, expectedResult, message]) => { + t.deepEqual(getPayout(channel, event, session), expectedResult, message) }) t.end() }) From 90b984a99f9113099567d61c0359f50d1e592a6a Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Tue, 31 Mar 2020 11:13:04 +0100 Subject: [PATCH 7/9] fix: remoev redundant impl --- services/sentry/lib/eventReducer.js | 5 ++--- services/sentry/lib/getPayout.js | 30 ++++++----------------------- test/index.js | 2 +- 3 files changed, 9 insertions(+), 28 deletions(-) diff --git a/services/sentry/lib/eventReducer.js b/services/sentry/lib/eventReducer.js index 6524a1d8..6b3c1efb 100644 --- a/services/sentry/lib/eventReducer.js +++ b/services/sentry/lib/eventReducer.js @@ -6,10 +6,9 @@ function newAggr(channelId) { return { channelId, created: new Date(), events: {}, totals: {}, earners: [] } } -function reduce(channel, session, initialAggr, event) { +function reduce(channel, session, initialAggr, ev) { let aggr = { ...initialAggr } - const ev = { ...event, ...session } // add session details (i.e country, device type etc) to event - const payout = getPayout(channel, ev) + const payout = getPayout(channel, ev, session) if (payout) { aggr.events[ev.type] = mergeEv(initialAggr.events[ev.type], payout) aggr = { ...aggr, ...mergeToGlobalAcc(aggr, ev.type, payout) } diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 70273068..9059624b 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -1,18 +1,9 @@ /* eslint-disable no-nested-ternary */ -// const BN = require('bignumber.js') // allows const BN = require('bn.js') const toBalancesKey = require('../toBalancesKey') function getPayout(channel, ev, session) { - if (ev.type === 'IMPRESSION' && ev.publisher) { - // add the minimum price for the event to the current amount - const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) - const price = channel.spec.priceMultiplicationRules - ? payout(channel.spec.priceMultiplicationRules, ev, session, maxPrice, minPrice) - : minPrice - return [toBalancesKey(ev.publisher), new BN(price.toString())] - } - if (ev.type === 'CLICK' && ev.publisher) { + if (ev.type && ev.publisher && ['IMPRESSION', 'CLICK'].includes(ev.type)) { const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) const price = channel.spec.priceMultiplicationRules ? payout(channel.spec.priceMultiplicationRules, ev, session, maxPrice, minPrice) @@ -64,21 +55,12 @@ function isRuleMatching(ev, session, rule) { function getPriceBounds(spec, evType) { const { pricingBounds, minPerImpression, maxPerImpression } = spec + const fromPricingBounds = pricingBounds && + pricingBounds[evType] && [new BN(pricingBounds[evType].min), new BN(pricingBounds[evType].max)] if (evType === 'IMPRESSION') { - return ( - (pricingBounds && - pricingBounds[evType] && [ - new BN(pricingBounds[evType].min), - new BN(pricingBounds[evType].max) - ]) || [new BN(minPerImpression || 1), new BN(maxPerImpression || 1)] - ) + return [new BN(minPerImpression || 1), new BN(maxPerImpression || 1)] } - return ( - (pricingBounds && - pricingBounds[evType] && [ - new BN(pricingBounds[evType].min), - new BN(pricingBounds[evType].max) - ]) || [new BN(0), new BN(0)] - ) + return fromPricingBounds || [new BN(0), new BN(0)] } + module.exports = getPayout diff --git a/test/index.js b/test/index.js index eaba0c3a..893a2049 100755 --- a/test/index.js +++ b/test/index.js @@ -356,7 +356,7 @@ tape('eventReducer: newAggr', function(t) { t.end() }) -tape.only('getPayout: get event payouts', function(t) { +tape('getPayout: get event payouts', function(t) { fixtures.payoutRules.forEach(([channel, event, session, expectedResult, message]) => { t.deepEqual(getPayout(channel, event, session), expectedResult, message) }) From b261949899baa3dada43d47190da77d070d7dddb Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 1 Apr 2020 08:04:13 +0100 Subject: [PATCH 8/9] add: test impl --- services/sentry/lib/getPayout.js | 6 +++--- test/fixtures.js | 16 ++++++++-------- test/integration.js | 32 +++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 9059624b..917fd864 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -3,7 +3,7 @@ const BN = require('bn.js') const toBalancesKey = require('../toBalancesKey') function getPayout(channel, ev, session) { - if (ev.type && ev.publisher && ['IMPRESSION', 'CLICK'].includes(ev.type)) { + if (ev.type && ev.publisher && ['IMPRESSION', 'CLICK'].includes(ev.type.toUpperCase())) { const [minPrice, maxPrice] = getPriceBounds(channel.spec, ev.type) const price = channel.spec.priceMultiplicationRules ? payout(channel.spec.priceMultiplicationRules, ev, session, maxPrice, minPrice) @@ -42,8 +42,8 @@ function payout(rules, ev, session, maxPrice, startPrice) { } function isRuleMatching(ev, session, rule) { - return rule.eventType - ? rule.eventType.includes(ev.type.toLowerCase()) + return rule.evType + ? rule.evType.includes(ev.type.toLowerCase()) : true && rule.publisher ? rule.publisher.includes(ev.publisher) : true && rule.osType diff --git a/test/fixtures.js b/test/fixtures.js index 656c4a4d..fe5b3a58 100644 --- a/test/fixtures.js +++ b/test/fixtures.js @@ -472,7 +472,7 @@ module.exports = { ...payoutChannel, spec: { ...payoutChannel.spec, - priceMultiplicationRules: [{ amount: '10', country: ['us'], eventType: ['click'] }] + priceMultiplicationRules: [{ amount: '10', country: ['us'], evType: ['click'] }] } }, { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'IMPRESSION' }, @@ -485,7 +485,7 @@ module.exports = { ...payoutChannel, spec: { ...payoutChannel.spec, - priceMultiplicationRules: [{ amount: '10', country: ['us'], eventType: ['click'] }] + priceMultiplicationRules: [{ amount: '10', country: ['us'], evType: ['click'] }] } }, { publisher: '0xce07CbB7e054514D590a0262C93070D838bFBA2e', type: 'CLICK' }, @@ -538,11 +538,11 @@ module.exports = { spec: { ...payoutChannel.spec, priceMultiplicationRules: [ - { amount: '10', country: ['us'], eventType: ['click'] }, + { amount: '10', country: ['us'], evType: ['click'] }, { amount: '12', country: ['us'], - eventType: ['click'], + evType: ['click'], publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'] } ] @@ -562,7 +562,7 @@ module.exports = { { amount: '15', country: ['us'], - eventType: ['click'], + evType: ['click'], publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] } @@ -586,14 +586,14 @@ module.exports = { { multiplier: 1.2, country: ['us'], - eventType: ['click'], + evType: ['click'], publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] }, { amount: '12', country: ['us'], - eventType: ['click'], + evType: ['click'], publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] } @@ -623,7 +623,7 @@ module.exports = { { multiplier: 1.2, country: ['us'], - eventType: ['click'], + evType: ['click'], publisher: ['0xce07CbB7e054514D590a0262C93070D838bFBA2e'], osType: ['android'] }, diff --git a/test/integration.js b/test/integration.js index 75ebea6c..a571d812 100755 --- a/test/integration.js +++ b/test/integration.js @@ -465,9 +465,39 @@ tape('should record clicks', async function(t) { t.end() }) -tape('analytics routes return correct values', async function(t) { +tape('should record: correct payout clicks', async function(t) { const channel = getValidEthChannel() + channel.spec = { + ...channel.spec, + pricingBounds: { + CLICK: { + min: '1', + max: '2' + } + }, + priceMultiplicationRules: [{ amount: '2', country: ['US'], evType: ['CLICK'] }] + } + const num = 66 + const evs = genEvents(num, randomAddress(), 'CLICK') + // Submit a new channel; we submit it to both sentries to avoid 404 when propagating messages + await Promise.all([ + fetchPost(`${leaderUrl}/channel`, dummyVals.auth.leader, channel), + fetchPost(`${followerUrl}/channel`, dummyVals.auth.follower, channel) + ]) + await postEvsAsCreator(leaderUrl, channel.id, evs, { 'cf-ipcountry': 'US' }) + // Technically we don't need to tick, since the events should be reflected immediately + const analytics = await fetch( + `${leaderUrl}/analytics/${channel.id}?eventType=CLICK&metric=eventPayouts` + ).then(r => r.json()) + + t.equal(analytics.aggr[0].value, (num * 2).toString(), 'proper payout amount') + + t.end() +}) + +tape('analytics routes return correct values', async function(t) { + const channel = getValidEthChannel() // Submit a new channel; we submit it to both sentries to avoid 404 when propagating messages await Promise.all([ fetchPost(`${leaderUrl}/channel`, dummyVals.auth.leader, channel), From 7e89bb0957b10a457a80f6842075669542ce9ed8 Mon Sep 17 00:00:00 2001 From: samparsky <8148384+samparsky@users.noreply.github.com> Date: Wed, 1 Apr 2020 08:42:43 +0100 Subject: [PATCH 9/9] fix: impression price bound --- services/sentry/lib/getPayout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/sentry/lib/getPayout.js b/services/sentry/lib/getPayout.js index 917fd864..1785b4f8 100644 --- a/services/sentry/lib/getPayout.js +++ b/services/sentry/lib/getPayout.js @@ -58,7 +58,7 @@ function getPriceBounds(spec, evType) { const fromPricingBounds = pricingBounds && pricingBounds[evType] && [new BN(pricingBounds[evType].min), new BN(pricingBounds[evType].max)] if (evType === 'IMPRESSION') { - return [new BN(minPerImpression || 1), new BN(maxPerImpression || 1)] + return fromPricingBounds || [new BN(minPerImpression || 1), new BN(maxPerImpression || 1)] } return fromPricingBounds || [new BN(0), new BN(0)] }