From d256dc7034b54acce1678137a02f0d73d99534a5 Mon Sep 17 00:00:00 2001 From: bsears90 Date: Wed, 9 May 2018 14:51:46 -0400 Subject: [PATCH 001/865] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ca7620fe..cbe32cae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "servicebot", - "version": "0.9.11", + "version": "1.0.0", "license": "GPL-3.0", "scripts": { "start": "node ./bin/start", From 16353bfd548564adfb354ba6e1842594efff1b06 Mon Sep 17 00:00:00 2001 From: bsears90 Date: Mon, 14 May 2018 13:59:19 -0400 Subject: [PATCH 002/865] Update References can now update nested references Added Tiers Added Payment Templates Added apply payment template API --- api/entity.js | 74 +- api/service-instances.js | 19 + api/service-templates.js | 25 +- api/tiers.js | 25 + lib/handleInputs.js | 19 +- models/base/entity.js | 112 ++- models/payment-structure-template.js | 7 + models/service-instance.js | 115 +++- models/service-template.js | 19 +- models/tier.js | 9 + package.json | 2 +- plugins/api-gateway/app.js | 1 + plugins/database/initialize.js | 649 +++++++++--------- .../database/migrations/versions/0.10.0.js | 98 +++ plugins/database/populate.js | 4 +- plugins/database/tests/refresh_version.js | 8 +- plugins/service-lifecycle/main.js | 25 +- plugins/updates/updates.js | 1 - 18 files changed, 783 insertions(+), 429 deletions(-) create mode 100644 api/tiers.js create mode 100644 models/payment-structure-template.js create mode 100644 models/tier.js create mode 100644 plugins/database/migrations/versions/0.10.0.js diff --git a/api/entity.js b/api/entity.js index 47a7a8f9..85f5db8d 100644 --- a/api/entity.js +++ b/api/entity.js @@ -115,23 +115,38 @@ module.exports = function (router, model, resourceName, userCorrelator) { router.put(`/${resourceName}/:id(\\d+)`, validate(model), auth(null, model, userCorrelator),async function (req, res, next) { try { let entity = res.locals.valid_object; - req.body.id = entity.get("id"); - Object.assign(entity.data, req.body); - let updatedEntity = await entity.update(); - let requestReferences = req.body.references || {}; - - //todo: combine updateReferences into a single transaction - for (let reference of references) { - let referenceData = requestReferences[reference.model.table] - if (referenceData) { - let upsertedReferences = await updatedEntity.updateReferences(referenceData, reference); + await model.database.transaction(async function (trx) { + let CreateEntity = require("../models/base/entity"); + let trxEntity = CreateEntity(model.table, model.references, model.primaryKey, trx); + + entity = new trxEntity(entity.data); + + + req.body.id = entity.get("id"); + Object.assign(entity.data, req.body); + let updatedEntity = await entity.update(); + let requestReferences = req.body.references || {}; + + //todo: combine updateReferences into a single transaction + updatedEntity.data.references = {}; + for (let reference of references) { + + let referenceData = requestReferences[reference.model.table] + if (referenceData) { + updatedEntity.data.references[reference.model.table] = await updatedEntity.updateReferences(referenceData, reference, true); + } } - } - res.locals.json = updatedEntity.data; - store.dispatchEvent(`${model.table}_updated`, updatedEntity); + res.locals.json = updatedEntity.data; + store.dispatchEvent(`${model.table}_updated`, updatedEntity); + }); next(); + }catch(error){ console.error("Server error updating entity: ", error); + if(error.code == 23503){ + res.status(400).send({error : error.constraint}); + + } res.status(500).send({error : "error updating"}); } }); @@ -157,13 +172,13 @@ module.exports = function (router, model, resourceName, userCorrelator) { router.post(`/${resourceName}`, auth(), function (req, res, next) { let entity = new model(req.body); - entity.create(function (err, newEntity) { + entity.create(async function (err, newEntity) { if(err){ console.error("Server error creating entity: " + err); res.status(500).send({ error: "Error creating new " + resourceName }) } else { - if (references.length === 0 || req.body.references === undefined || req.body.references.length == 0) { + if (references.length === 0 || req.body.references === undefined || req.body.references.length === 0) { res.locals.json = newEntity.data; store.dispatchEvent(`${model.table}_created`, newEntity) EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); @@ -172,30 +187,15 @@ module.exports = function (router, model, resourceName, userCorrelator) { else { let requestReferenceData = req.body.references; newEntity.data.references = {}; - let counter = 0; for (let reference of references) { - if (!requestReferenceData[reference.model.table] || requestReferenceData[reference.model.table].length == 0) { - counter++; - if (counter == references.length) { - res.locals.json = newEntity.data; - store.dispatchEvent(`${model.table}_created`, newEntity) - - EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); - next(); - } - } - else { - let referenceData = requestReferenceData[reference.model.table]; - newEntity.createReferences(referenceData, reference, function (modifiedEntity) { - counter++; - if (counter == references.length) { - res.locals.json = modifiedEntity.data; - store.dispatchEvent(`${model.table}_created`, modifiedEntity) - EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); - next(); - } - }); + let referenceData = requestReferenceData[reference.model.table]; + if(referenceData) { + newEntity.data.references[reference.model.table] = await newEntity.updateReferences(referenceData, reference); } + res.locals.json= newEntity.data; + store.dispatchEvent(`${model.table}_created`, newEntity) + EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); + next(); } } } diff --git a/api/service-instances.js b/api/service-instances.js index 84c27407..3f14c8f6 100644 --- a/api/service-instances.js +++ b/api/service-instances.js @@ -9,6 +9,7 @@ let mkdirp = require("mkdirp"); let path = require("path"); let multer= require("multer"); let _ = require('lodash'); +let PaymentStructureTemplate = require("../models/payment-structure-template"); let dispatchEvent = require("../config/redux/store").dispatchEvent; let store = require("../config/redux/store"); //todo - entity posting should have correct error handling, response should tell user what is wrong like if missing column @@ -36,6 +37,7 @@ module.exports = function(router) { delete req.body.user_id; delete req.body.payment_plan; delete req.body.subscription_id; + delete req.body.payment_structure_template_id; next(); }); @@ -95,6 +97,23 @@ module.exports = function(router) { res.json({error}); }); }); + router.post('/service-instances/:id/apply-payment-structure/:payment_structure_id(\\d+)', validate(ServiceInstance), auth(), async function(req, res, next) { + let instance_object = await res.locals.valid_object.attachReferences(); + let newPaymentStructure = (await PaymentStructureTemplate.find({id: req.params.payment_structure_id}))[0]; + let permission_array = res.locals.permissions; + let hasPermission = (permission_array.some(p => p.get("permission_name") === "can_administrate" || p.get("permission_name") === "can_manage")); + if((instance_object.data.references.payment_structure_templates[0].type === "custom" || newPaymentStructure.data.type === "custom" ) && !hasPermission){ + return res.status(403).json({error: "Unauthorized"}); + } + instance_object.applyPaymentStructure(req.params.payment_structure_id, true).then(function (updatedInstance) { + res.json(updatedInstance); + store.dispatchEvent("service_instance_updated", updatedInstance); + }).catch(function (error) { + console.error(error); + res.json({error}); + }); + }); + router.post('/service-instances/:id/change-properties', validate(ServiceInstance),auth(), async function(req, res, next) { let instance_object = res.locals.valid_object; diff --git a/api/service-templates.js b/api/service-templates.js index b7f562bd..a2688b74 100644 --- a/api/service-templates.js +++ b/api/service-templates.js @@ -22,6 +22,8 @@ let iconFilePath = ServiceTemplate.iconFilePath; let imageFilePath = ServiceTemplate.imageFilePath; let slug = require("slug"); let validateProperties = require("../lib/handleInputs").validateProperties; +let Tier = require("../models/tier"); +let PaymentStructureTemplate = require("../models/payment-structure-template"); let fileManager = store.getState(true).pluginbot.services.fileManager[0]; let jwt = require('jsonwebtoken'); @@ -202,6 +204,18 @@ module.exports = function (router) { let props = (await serviceTemplate.getRelated(ServiceTemplateProperty)) || null; let req_body = req.body; let reqProps = req_body.references && req_body.references.service_template_properties || []; + let paymentStructureTemplate = (await PaymentStructureTemplate.find({id: req_body.payment_structure_template_id}))[0]; + + //check if the payment structure exists + if(!paymentStructureTemplate){ + return res.status(400).json({error: 'Payment structure template not found'}); + } + let tier = (await Tier.find({id: paymentStructureTemplate.data.tier_id, service_template_id: serviceTemplate.data.id}))[0]; + + //check if the structure's tier belongs to the template + if(!tier){ + return res.status(400).json({error: 'Payment Structure does not belong to this service template'}); + } await authPromise(req, res); let permission_array = res.locals.permissions || []; let handlers = (store.getState(true).pluginbot.services.inputHandler || []).reduce((acc, handler) => { @@ -210,9 +224,9 @@ module.exports = function (router) { }, {}); //this is true when user can override things let hasPermission = (permission_array.some(p => p.get("permission_name") === "can_administrate" || p.get("permission_name") === "can_manage")); - let templatePrice = serviceTemplate.get("amount"); + let templatePrice = paymentStructureTemplate.get("amount"); let price = hasPermission ? (req_body.amount || templatePrice) : templatePrice; - let trialPeriod = serviceTemplate.get("trial_period_days"); + let trialPeriod = paymentStructureTemplate.get("trial_period_days"); //todo: this doesn't do anthing yet, needs to check the "passed" props not the ones on the original... // let validationResult = props ? validateProperties(props, handlers) : []; @@ -235,6 +249,7 @@ module.exports = function (router) { res.locals.adjusted_price = price; res.locals.merged_props = mergedProps; + res.locals.payment_structure_template = paymentStructureTemplate; if (!req.isAuthenticated()) { if (req_body.hasOwnProperty("email")) { @@ -283,6 +298,7 @@ module.exports = function (router) { try { let serviceTemplate = res.locals.valid_object; let references = serviceTemplate.references; + let paymentStructureTemplate = res.locals.payment_structure_template; let props = references ? references.service_template_properties : null; let req_body = req.body; req_body.references.service_template_properties = res.locals.merged_props; @@ -313,6 +329,7 @@ module.exports = function (router) { } //if it's a new user request we need to create an account, invitation + //todo: move this part into a function if (isNew && serviceTemplate.get('published')) { let globalProps = store.getState().options; @@ -396,7 +413,7 @@ module.exports = function (router) { requested_by : req.user.get("id"), description : req_body.description || serviceTemplate.get("description"), name : req_body.name || serviceTemplate.get("name"), - trial_period_days : req_body.trial_period_days || serviceTemplate.get("trial_period_days") + trial_period_days : req_body.trial_period_days || paymentStructureTemplate.get("trial_period_days") }; @@ -404,7 +421,7 @@ module.exports = function (router) { res.locals.overrides = { user_id : req.user.get("id"), requested_by : req.user.get("id"), - trial_period_days : serviceTemplate.get("trial_period_days") || 0 + trial_period_days : paymentStructureTemplate.get("trial_period_days") || 0 } } diff --git a/api/tiers.js b/api/tiers.js new file mode 100644 index 00000000..19eaf816 --- /dev/null +++ b/api/tiers.js @@ -0,0 +1,25 @@ + + +module.exports = function(router) { + let Tier = require('../models/tier'); + // router.get("/service-categories", function(req, res, next){ + // if (!req.isAuthenticated()) { + // let key = req.query.key; + // let value = req.query.value; + // if (!key || !value) { + // key = undefined; + // value = undefined; + // } + // ServiceCategories.findAll(key, value, function (templates) { + // res.json(templates.map(entity => entity.data)) + // }); + // } + // else { + // console.log("authorized person, go on"); + // next(); + // } + // }); + require("./entity")(router, Tier, "tiers"); + + return router; +}; \ No newline at end of file diff --git a/lib/handleInputs.js b/lib/handleInputs.js index 28f2f9d3..c17c5d87 100644 --- a/lib/handleInputs.js +++ b/lib/handleInputs.js @@ -78,7 +78,16 @@ module.exports = { } } - return (currentPrice - additions)/multiplication; + //make sure not dividing by 0 + if(multiplication === 0){ + multiplication = 1; + } + + let basePrice = (currentPrice - additions)/multiplication; + if(basePrice < 0){ + basePrice = 0; + } + return basePrice; }, getPrice : function(properties, handlers, basePrice, cents=false){ let adjustments = []; @@ -87,7 +96,7 @@ module.exports = { }catch(e){ console.error("price error", e); } - return (basePrice + adjustments.reduce((acc, adjustment) => { + let price = (basePrice + adjustments.reduce((acc, adjustment) => { let operation = adjustment.operation; if(adjustment.value === null || adjustment.value === undefined){ return acc; @@ -111,8 +120,10 @@ module.exports = { } return acc; }, 0)); - - + if(price < 0){ + price = 0; + } + return price; }, validateProperties: function (properties, handlers) { // let handlers = getHandlers(handlerArray); diff --git a/models/base/entity.js b/models/base/entity.js index f5a74cf2..4e479dc5 100644 --- a/models/base/entity.js +++ b/models/base/entity.js @@ -16,7 +16,7 @@ var whereFilter = require('knex-filter-loopback').whereFilter; * @returns {Entity} */ -module.exports = function (tableName, references = [], primaryKey = 'id', database = knex) { +var CreateEntity = function (tableName, references = [], primaryKey = 'id', database = knex) { var Entity = function (data) { let self = this; this.data = data; @@ -199,23 +199,29 @@ module.exports = function (tableName, references = [], primaryKey = 'id', databa //todo - combine stuff into a single query //todo - possibly dispatch events - Entity.prototype.updateReferences = async function (referenceData, reference) { + Entity.prototype.updateReferences = async function (referenceData, reference, isTransaction=false) { let self = this; if (reference.readOnly) { console.log("Reference is readonly"); this; } else { - - // let ids = referenceData.reduce((acc, refInstance) => acc.concat(refInstance.id || []), []); referenceData.forEach(newChild => (newChild[reference.referenceField] = this.get(primaryKey))); + let references = await this.getRelated(reference.model.table); let removedReferences = await reference.model.batchDelete({ not: {id: {"in": ids}}, [reference.referenceField]: self.get(primaryKey) }); - let upsertedReferences = await reference.model.batchUpdate(referenceData); + console.log(ids, removedReferences, "DELTED\n"); + let upsertedReferences = await reference.model.batchUpdate(referenceData, true, isTransaction); + + //change "to" reference if it's different + if(reference.direction === "to" && upsertedReferences[0][reference.model.primaryKey] !== self.get(reference.referenceField)){ + self.set(reference.referenceField, upsertedReferences[0][reference.model.primaryKey]); + await self.update(); + } return upsertedReferences; @@ -243,8 +249,12 @@ module.exports = function (tableName, references = [], primaryKey = 'id', databa //best one! (todo... clean up ORM cuz it sucks) Entity.find = async function (filter = {}, attatchReferences = false) { try { - let entities = await Entity.database(Entity.table).where(whereFilter(filter)); - return entities ? entities.map(e => new Entity(e)) : []; + let result = await Entity.database(Entity.table).where(whereFilter(filter)); + let entities = result ? result.map(e => new Entity(e)) : []; + if(attatchReferences){ + entities = await Entity.batchAttatchReference(entities); + } + return entities; } catch (err) { console.error(err); return []; @@ -436,7 +446,7 @@ module.exports = function (tableName, references = [], primaryKey = 'id', databa }; Entity.batchDelete = async function (filter) { - return knex(Entity.table).where(whereFilter(filter)).del(); + return knex(Entity.table).where(whereFilter(filter)).del().returning("*"); }; /** * @@ -464,33 +474,70 @@ module.exports = function (tableName, references = [], primaryKey = 'id', databa }) }; - //TODO this batch update work - let batchUpdate = function (dataArray, callback) { + //TODO: figure out if updateReferences is even needed, if it is figure out how to remove it. + Entity.batchUpdate = async function (dataArray, updateReferences, isTransaction=false) { + let columns = await Entity.database(Entity.table).columnInfo() + let data = dataArray.map(function (entity) { + return _.pick(entity, _.keys(columns)); + }) + let transaction = Entity.database.transaction; + if(isTransaction){ + transaction = async (callback) => { + return callback(Entity.database); + } + } + return await transaction(async function (trx) { + return Promise.map(data, async function (entityData, index) { + let record; + if (entityData[Entity.primaryKey]) { + record = (await trx.from(Entity.table).where(Entity.primaryKey, entityData[Entity.primaryKey]).update(entityData).returning("*"))[0]; + } else { + record = (await trx.from(Entity.table).insert(entityData).returning("*"))[0]; + } - Entity.database(Entity.table).columnInfo() - .then(function (info) { - return dataArray.map(function (entity) { - return _.pick(entity, _.keys(info)); - }) - }) - .then(function (data) { - Entity.database.transaction(function (trx) { - return Promise.map(data, function (entityData) { - if (entityData[Entity.primaryKey]) { - return trx.from(Entity.table).where(Entity.primaryKey, entityData[Entity.primaryKey]).update(entityData).returning("*"); - } else { - return trx.from(Entity.table).insert(entityData).returning("*"); - } - }); + if(updateReferences && dataArray[index].references){ + record.references = dataArray[index].references; + for(let [refName, refValues] of Object.entries(dataArray[index].references)){ + let reference = references.find(ref => ref.model.table === refName); + if(reference && !reference.readOnly){ + let trxReference = CreateEntity(reference.model.table, reference.model.references, reference.model.primaryKey, trx); + let ids = refValues.reduce((acc, refInstance) => acc.concat(refInstance.id || []), []); + let removedReferences = await trxReference.batchDelete({ + not: {id: {"in": ids}}, + [reference.referenceField]: record[Entity.primaryKey] + }); + let refsToUpdate = refValues.reduce((acc, val) => { + if(val[reference.model.primaryKey]){ + + //make sure that the referenceField is pointing properly + if(reference.direction === "from" && val[reference.referenceField] === record[Entity.primaryKey]){ + acc.push(val); + + }else if(reference.direction === "to" && record[reference.referenceField] === val[reference.model.primaryKey]){ + acc.push(val); + } + }else{ + if(reference.direction === "from"){ + console.log(record, Entity.primaryKey, record[Entity.primaryKey]); + val[reference.referenceField] = record[Entity.primaryKey]; + acc.push(val); + + } + } + return acc; + }, []); + record.references[reference.model.table] = (await trxReference.batchUpdate(refsToUpdate, true, true)) || []; + console.log("LOL\n", record, "\nLOL"); - }).then(function (result) { - callback(result); - }).catch(function (err) { - console.log(err); - }) + } + } + } + console.log("END BATCH UPDATE") + return record; }); + }); + }; - Entity.batchUpdate = promiseProxy(batchUpdate) Entity.prototype.update = promiseProxy(update, false); Entity.findOne = promiseProxy(findOne); Entity.prototype.attachReferences = promiseProxy(attachReferences); @@ -500,4 +547,5 @@ module.exports = function (tableName, references = [], primaryKey = 'id', databa return Entity; -} \ No newline at end of file +} +module.exports = CreateEntity; \ No newline at end of file diff --git a/models/payment-structure-template.js b/models/payment-structure-template.js new file mode 100644 index 00000000..65db5f30 --- /dev/null +++ b/models/payment-structure-template.js @@ -0,0 +1,7 @@ + +let PaymentStructureTemplate = require("./base/entity")("payment_structure_templates"); + + +//Notification.createFromTemplate() + +module.exports = PaymentStructureTemplate; \ No newline at end of file diff --git a/models/service-instance.js b/models/service-instance.js index a65d3615..de23f62f 100644 --- a/models/service-instance.js +++ b/models/service-instance.js @@ -2,6 +2,8 @@ let File = require("./file"); let async = require('async'); let ServiceTemplates = require("./service-template"); +let Tier = require("./tier"); +let PaymentStructureTemplate = require("./payment-structure-template"); let ServiceTemplateProperties = require("../models/service-template-property"); let ServiceInstanceProperties = require("./service-instance-property"); let ServiceInstanceMessages = require("./service-instance-message"); @@ -23,7 +25,14 @@ let references = [ "direction": "from", "readOnly": true }, - {"model": User, "referenceField": "user_id", "direction": "to", "readOnly": true} + {"model": User, "referenceField": "user_id", "direction": "to", "readOnly": true}, + { + "model": PaymentStructureTemplate, + "referenceField": "payment_structure_template_id", + "direction": "to", + "readOnly": true + } + ]; let ServiceInstance = require("./base/entity")("service_instances", references); let Stripe = require('../config/stripe'); @@ -34,6 +43,7 @@ let buildPayStructure = function (payment_object, callback) { let self = this; let plan_arr = ['name', 'amount', 'currency', 'interval', 'interval_count', 'statement_descriptor', 'trial_period_days']; let random_code = Math.random().toString(36).substring(10, 12) + Math.random().toString(36).substring(10, 12); + console.log(payment_object); let default_plan = { 'id': `${payment_object.name.replace(/ +/g, '-')}-ID${self.get("id")}-${random_code}`, 'currency': 'usd', @@ -56,8 +66,10 @@ let buildPayStructure = function (payment_object, callback) { ServiceInstance.prototype.createPayPlan = async function (plan = null) { if (plan === null) { - let template = (await ServiceTemplates.find({"id": this.data.service_id}))[0]; - plan = await this.buildPayStructure(template.data); + let paymentStructure = (await PaymentStructureTemplate.find({id: this.data.payment_structure_template_id}))[0]; + + // let template = (await ServiceTemplates.find({"id": this.data.service_id}))[0]; + plan = await this.buildPayStructure(paymentStructure.data); } if (plan.trial_period_days === null) { plan.trial_period_days = 0; @@ -69,7 +81,7 @@ ServiceInstance.prototype.createPayPlan = async function (plan = null) { } catch (error) { try { this.data.payment_plan = await Stripe().connection.plans.create(plan); - }catch(error){ + } catch (error) { this.data.status = "missing_payment"; await this.update() throw error; @@ -83,7 +95,7 @@ ServiceInstance.prototype.deletePayPlan = async function () { let self = this; if (self.data.payment_plan.id) { //Remove the plan from Stripe - await Stripe().connection.plans.del(self.data.payment_plan.id); + await Stripe().connection.plans.del(self.data.payment_plan.id); self.data.payment_plan = null; return await self.update() } else { @@ -92,18 +104,19 @@ ServiceInstance.prototype.deletePayPlan = async function () { }; -ServiceInstance.prototype.subscribe = async function (paymentPlan=null) { +ServiceInstance.prototype.subscribe = async function (paymentPlan = null) { let self = this; if (self.data.subscription_id) { throw "Instance is already subscribed" } - if(paymentPlan){ - self = await this.changePaymentPlan(paymentPlan); - } let user = (await User.find({"id": self.data.user_id}))[0]; if (user && user.data.status === "suspended") { throw "User is suspended, unable to subscribe" } + + if (paymentPlan) { + self = await this.changePaymentPlan(paymentPlan); + } let sub_obj = { "customer": user.data.customer_id, "plan": self.data.payment_plan.id @@ -134,16 +147,16 @@ let requestCancellation = function (callback) { "service_instance_id": self.data.id, "user_id": self.data.user_id }; - if(approve_cancellation){ + if (approve_cancellation) { cancellationData.status = "approved" } let newServiceCancellation = new ServiceInstanceCancellations(cancellationData); newServiceCancellation.create(async function (err, result) { //Update the service instance status - if(approve_cancellation){ + if (approve_cancellation) { let unsub = await self.unsubscribe() callback(result); - }else { + } else { self.data.status = "waiting_cancellation"; self.update(function (err, updated_instance) { callback(result); @@ -229,7 +242,7 @@ let deleteFiles = function (callback) { ServiceInstance.prototype.changeProperties = async function (properties) { let updatedInstance = await this.attachReferences(); - let oldInstance = {data : {...updatedInstance.data}} + let oldInstance = {data: {...updatedInstance.data}}; //todo: support creating new properties, shouldn't be bad... just need to validate the config if (properties.some(prop => prop.id === null || prop.parent_id !== updatedInstance.get("id"))) { throw "prop id bad or parent id does not match" @@ -240,7 +253,7 @@ ServiceInstance.prototype.changeProperties = async function (properties) { lifecycleManager = lifecycleManager[0]; await lifecycleManager.prePropertyChange({ instance: updatedInstance, - property_updates : properties + property_updates: properties }); } @@ -266,9 +279,9 @@ ServiceInstance.prototype.changeProperties = async function (properties) { } let updatedProps = await ServiceInstanceProperties.batchUpdate(mergedProps); if (lifecycleManager) { - await updatedInstance.attachReferences() + await updatedInstance.attachReferences(); await lifecycleManager.postPropertyChange({ - old_instance : oldInstance, + old_instance: oldInstance, instance: updatedInstance, }); } @@ -276,19 +289,77 @@ ServiceInstance.prototype.changeProperties = async function (properties) { return updatedInstance; }; +ServiceInstance.prototype.applyPaymentStructure = async function (paymentStructureId, ignoreTrial) { + let instance_object = await this.attachReferences(); + let currentStructure = instance_object.data.references.payment_structure_templates[0]; + + if (!currentStructure) { + throw "Instance has no payment structure"; + } + + let paymentStructureTemplate = (await PaymentStructureTemplate.find({id: paymentStructureId}))[0]; + if (!paymentStructureTemplate) { + throw "Payment Structure Not Found"; + + } + + let tier = (await Tier.find({ + service_template_id: instance_object.data.service_id, + id: paymentStructureTemplate.data.tier_id + }))[0]; + if (!tier) { + throw "Incompatible Payment Structure"; + } + + let lifecycleManager = store.getState(true).pluginbot.services.lifecycleManager; + if (lifecycleManager) { + lifecycleManager = lifecycleManager[0]; + await lifecycleManager.prePaymentStructureChange({ + instance: instance_object, + payment_structure_template: paymentStructureTemplate, + }); + } + let paymentPlan = { + ...paymentStructureTemplate.data, + name : tier.data.name + "-" + paymentStructureTemplate.data.id + } + let newInstance = await (await instance_object.changePaymentPlan(paymentPlan, ignoreTrial)).attachReferences(); + console.log("before", newInstance); + + if (lifecycleManager) { + let response = await lifecycleManager.postPaymentStructureChange({ + old_instance: instance_object, + instance: newInstance + }); + if (response) { + newInstance.data = { + ...newInstance.data, + ...response + } + } + } + console.log("after", newInstance); + return newInstance; + + +}; + ServiceInstance.prototype.changePaymentPlan = async function (newPlan, ignorePlanTrial) { await this.deletePayPlan(); let planStructure = await this.buildPayStructure(newPlan); let updatedInstance = await this.createPayPlan(planStructure); - if(this.data.subscription_id !== null) { - let payload = {plan: updatedInstance.data.payment_plan.id} - if(ignorePlanTrial){ + if (this.data.subscription_id !== null) { + let payload = {plan: updatedInstance.data.payment_plan.id}; + if (ignorePlanTrial) { payload.trial_from_plan = false; } let stripeSubscription = await Stripe().connection.subscriptions.update(this.data.subscription_id, payload); let oldTrial = updatedInstance.data.trial_end; - updatedInstance.data.trial_end = stripeSubscription.trial_end - if(oldTrial !== stripeSubscription.trial_end){ + updatedInstance.data.trial_end = stripeSubscription.trial_end; + if (newPlan.id) { + updatedInstance.data.payment_structure_template_id = newPlan.id + } + if (oldTrial !== stripeSubscription.trial_end) { //todo: handle this better, don't like dispatching here. store.dispatchEvent("service_instance_trial_change", updatedInstance); @@ -327,7 +398,7 @@ ServiceInstance.prototype.unsubscribe = async function () { console.error(error); throw error; } -} +}; //todo: clean this up so they really support promises. diff --git a/models/service-template.js b/models/service-template.js index 9bb10f53..9acaea6f 100644 --- a/models/service-template.js +++ b/models/service-template.js @@ -3,6 +3,8 @@ let ServiceCategory = require('./service-category'); let ServiceInstance = require('./service-instance'); let User = require('./user'); let File = require("./file"); +let Tier = require("./tier"); +let PaymentStructureTemplate = require("./payment-structure-template"); let Charges = require("./charge"); @@ -10,7 +12,8 @@ let Charges = require("./charge"); let references = [ {"model":ServiceTemplateProperty, "referenceField": "parent_id", "direction":"from", "readOnly":false}, {"model":ServiceCategory, "referenceField": "category_id", "direction":"to", "readOnly":true}, - {"model":User, "referenceField": "created_by", "direction":"to", "readOnly": true} + {"model":User, "referenceField": "created_by", "direction":"to", "readOnly": true}, + {"model" :Tier, "referenceField": "service_template_id", "direction" : "from", "readOnly" : false} ]; var ServiceTemplate = require("./base/entity")("service_templates", references); ServiceTemplate.iconFilePath = "uploads/templates/icons"; @@ -22,6 +25,8 @@ ServiceTemplate.prototype.requestPromise = async function (instanceRequest) { let service_user_id = ""; let service_description = self.data.description; let service_name = self.data.name; + let paymentStructure = (await PaymentStructureTemplate.find({tier_id: instanceRequest.tier_id, interval: instanceRequest.interval}))[0]; + if (self.data.detail) { //todo : strip out XSS @@ -34,8 +39,8 @@ ServiceTemplate.prototype.requestPromise = async function (instanceRequest) { requested_by: instanceRequest.requested_by, user_id: instanceRequest.user_id, service_id: self.get("id"), - type: self.get("type"), - split_configuration : self.get("split_configuration"), + type: paymentStructure.get("type"), + split_configuration : paymentStructure.get("split_configuration"), status : "requested" }; @@ -43,17 +48,17 @@ ServiceTemplate.prototype.requestPromise = async function (instanceRequest) { let ServiceInstance = require('../models/service-instance'); let service = new ServiceInstance(await ServiceInstance.createPromise(instanceAttributes)); let props = await service.generateProps(submittedProperties); - if (self.data.type === 'one_time') { + if (paymentStructure.data.type === 'one_time') { let charge_obj = { 'user_id': service.get('user_id'), 'service_instance_id': service.get('id'), - 'currency': self.get('currency'), - 'amount': instanceRequest.amount || self.get('amount') || 0, + 'currency': paymentStructure.get('currency'), + 'amount': instanceRequest.amount || paymentStructure.get('amount') || 0, 'description': service.get('name') }; let charge = await Charges.createPromise(charge_obj); } - let plan = (self.data.type === 'one_time' || self.data.type === 'custom' || self.data.type === "split") ? {...self.data, amount : 0, interval : "day"} : instanceRequest; + let plan = (paymentStructure.data.type === 'one_time' || paymentStructure.data.type === 'custom' || paymentStructure.data.type === "split") ? {...paymentStructure.data, amount : 0, interval : "day"} : {...paymentStructure.data, ...instanceRequest}; let payStructure = (instanceRequest.amount === 0 || instanceRequest.amount === undefined) ? null : (await service.buildPayStructure(plan)); let payPlan = await service.createPayPlan(payStructure); diff --git a/models/tier.js b/models/tier.js new file mode 100644 index 00000000..dd50abb7 --- /dev/null +++ b/models/tier.js @@ -0,0 +1,9 @@ +let PaymentStructureTemplate = require("./payment-structure-template"); +let references = [ + {"model" :PaymentStructureTemplate, "referenceField": "tier_id", "direction" : "from", "readOnly" : false} +]; + +let Tier = require("./base/entity")("tiers", references); + + +module.exports = Tier; \ No newline at end of file diff --git a/package.json b/package.json index cbe32cae..1fa8e45a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "servicebot", - "version": "1.0.0", + "version": "0.10.0", "license": "GPL-3.0", "scripts": { "start": "node ./bin/start", diff --git a/plugins/api-gateway/app.js b/plugins/api-gateway/app.js index 70fbc985..d86c0024 100644 --- a/plugins/api-gateway/app.js +++ b/plugins/api-gateway/app.js @@ -158,6 +158,7 @@ module.exports = { require('../../api/notifications')(api); require('../../api/permissions')(api); require('../../api/roles')(api); + require('../../api/tiers')(api); require('../../api/webhooks')(api); let routeConsumer = require("./router"); let authService = yield consume(services.authService); diff --git a/plugins/database/initialize.js b/plugins/database/initialize.js index e0199466..b5fefe2f 100644 --- a/plugins/database/initialize.js +++ b/plugins/database/initialize.js @@ -1,7 +1,7 @@ let consume = require("pluginbot/effects/consume") let {call, put} = require("redux-saga/effects") -let createTable = function(knex){ - return async function(tableName, tableFunction){ +let createTable = function (knex) { + return async function (tableName, tableFunction) { let table = await knex.schema.createTable(tableName, tableFunction); console.log("Created table : " + tableName); return table @@ -11,321 +11,344 @@ let createTable = function(knex){ //todo: split this up into multiple plugins. -let buildTables = async function(knex) { - - - console.log("Creating tables..."); - let create = createTable(knex); - - await create('user_roles', function (table) { - table.increments(); - table.string('role_name').unique(); - table.timestamps(true, true); - - }); - - await create('user_permissions', function (table) { - table.increments(); - table.string('permission_name').unique(); - table.timestamps(true, true); - - }); - await create('roles_to_permissions', function (table) { - table.increments(); - table.integer('role_id').references('user_roles.id').onDelete('cascade'); - table.integer('permission_id').references('user_permissions.id').onDelete('cascade'); - table.timestamps(true, true); - - }); - await create('users', function (table) { - table.increments(); - table.integer('role_id').references('user_roles.id'); - table.string('name'); - table.string('email').notNullable().unique(); - table.string('password'); - table.string('provider').defaultTo("local"); - table.enu('status', ['active', 'suspended', 'invited', 'flagged', 'disconnected']).defaultTo('active'); - table.string('customer_id'); - table.string('phone'); - table.timestamp('last_login'); - table.timestamps(true, true); - - }); - await create('funds', function (table) { - table.increments(); - table.integer('user_id').references('users.id').onDelete('cascade'); - table.boolean('flagged').defaultTo(false); - table.jsonb('source'); - table.timestamps(true, true); - - }); - await create('invitations', function (table) { - table.increments(); - table.string('token'); - table.integer('user_id').references('users.id').onDelete('cascade'); - table.timestamps(true, true); - - }); - await create('system_options', function (table) { - table.string('option').primary(); - table.string('value'); - table.boolean("public").defaultTo(false); - table.string('type'); - table.string('data_type'); - table.timestamps(true, true); - - }); - await create('event_logs', function (table) { - table.increments(); - table.integer('user_id'); - table.string('log_level'); - table.string('log_type'); - table.string('log'); - table.timestamps(true, true); - - }); - await create('files', function (table) { - table.increments(); - table.integer('user_id').references('users.id').onDelete('cascade'); - table.string('path'); - table.integer('size'); - table.string('name'); - table.string('mimetype'); - table.timestamps(true, true); - - }); - await create('notification_templates', function (table) { - table.increments(); - table.string('name'); - table.string('event_name'); - table.text('message', 'longtext'); - table.string('subject'); - table.string('description'); - table.string("model"); - table.specificType('additional_recipients', 'text[]'); - table.boolean("send_email").defaultTo(false); - table.boolean("send_to_owner").defaultTo(true); - table.boolean("create_notification").default(true); - table.timestamps(true, true); - - }); - await create("notification_templates_to_roles", function (table) { - table.increments(); - table.integer("notification_template_id").references("notification_templates.id"); - table.integer("role_id").references("user_roles.id"); - table.timestamps(true, true); - - }); - await create('notifications', function (table) { - table.increments(); - table.string("source_id").unique(); - table.text('message', 'longtext'); - table.string("type"); - table.integer("user_id").references("users.id").onDelete('cascade'); - table.string("subject"); - table.string("affected_versions").defaultTo("*"); - table.boolean("read").defaultTo(false); - table.boolean("email_delivered").defaultTo(false); - table.boolean("email_read").defaultTo(false); - table.timestamp('created_at').defaultTo(knex.fn.now()); - - }); - await create('service_categories', function (table) { - table.increments(); - table.string('name'); - table.string('description'); - table.timestamps(true, true); - - }); - await create('service_templates', function (table) { - table.increments(); - table.integer('category_id').references('service_categories.id'); - table.integer('created_by').references('users.id'); - table.string('name').notNullable().unique(); - table.string('description'); - table.text('details', 'longtext'); - table.boolean('published').defaultTo(false); - table.string('statement_descriptor'); - table.integer('trial_period_days'); - table.float('amount'); - table.float('overhead'); - table.string('currency').defaultTo('usd'); - table.string('interval'); - table.integer('interval_count').defaultTo(1); - table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); - table.boolean('subscription_prorate').defaultTo(true); - table.jsonb('split_configuration'); - table.timestamps(true, true); - - }); - await create('service_instances', function (table) { - table.increments(); - table.integer('service_id').references('service_templates.id'); - table.integer('user_id').references('users.id'); - table.integer('requested_by').references('users.id'); - table.jsonb('payment_plan'); - table.string('name'); - table.text('description', 'longtext'); - table.string('subscription_id'); - table.bigInteger('subscribed_at'); - table.bigInteger('trial_end'); - table.enu('status', ['running', 'requested', 'in_progress', 'waiting_cancellation', 'missing_payment', 'cancelled', 'completed']).defaultTo('missing_payment'); - table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); - table.jsonb('split_configuration'); - table.timestamps(true, true); - - }); - await create('service_instance_messages', function (table) { - table.increments(); - table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); - table.integer('user_id').references('users.id'); - table.string('message'); - table.timestamps(true, true); - - }); - await create('service_instance_cancellations', function (table) { - table.increments(); - table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); - table.integer('user_id').references('users.id'); - table.enu('status', ['waiting', 'approved', 'rejected']).defaultTo('waiting'); - table.integer('fulfilled_by').references('users.id'); - table.timestamps(true, true); - - }); - await create('properties', function (table) { - table.increments(); - table.string('name'); - table.string('type'); - table.jsonb('data'); - table.jsonb('config'); - table.string('prop_class'); - table.string('prop_label'); - table.string('prop_description'); - table.timestamps(true, true); - - }); - await create('service_template_properties', function (table) { - //Inherits the properties table. - table.inherits('properties'); - table.increments(); - table.integer('parent_id').references('service_templates.id').onDelete('cascade'); - table.boolean('private').defaultTo(false); - table.boolean('prompt_user').defaultTo(true); - table.boolean('required').defaultTo(false); - table.timestamps(true, true); - - }); - await create('service_instance_properties', function (table) { - //Inherits the properties table. - table.inherits('properties'); - table.increments(); - table.integer('parent_id').references('service_instances.id').onDelete('cascade'); - table.boolean('private').defaultTo(false); - table.boolean('prompt_user').defaultTo(true); - table.boolean('required').defaultTo(false); - table.timestamps(true, true); - - }); - await create('user_invoices', function (table) { - table.increments(); - table.integer('user_id').references('users.id'); - table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); - table.string('invoice_id').unique(); - table.string('subscription'); - table.string('charge'); - table.string('description'); - table.float('amount_due'); - table.float('discount'); - table.integer('attempt_count'); - table.boolean('closed'); - table.string('currency'); - table.boolean('forgiven'); - table.bigInteger('date'); - table.bigInteger('next_attempt'); - table.boolean('paid'); - table.bigInteger('period_end'); - table.bigInteger('period_start'); - table.string('receipt_number'); - table.float('starting_balance'); - table.float('ending_balance'); - table.float('total'); - table.boolean('livemode'); - table.timestamps(true, true); - - }); - await create('charge_items', function (table) { - table.increments(); - table.integer('user_id').references('users.id'); - table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); - table.boolean('approved').defaultTo(false); - table.string('subscription_id'); - table.string('item_id'); - table.float('amount'); - table.string('currency').defaultTo('usd'); - table.string('description'); - table.bigInteger('period_start'); - table.bigInteger('period_end'); - table.timestamps(true, true); - - }) - await create('user_invoice_lines', function (table) { - table.increments(); - table.integer('invoice_id').references('user_invoices.id').onDelete('cascade'); - table.string('line_item_id'); - table.float('amount'); - table.string('currency'); - table.string('description'); - table.boolean('proration'); - table.integer('quantity'); - table.string('type'); - table.boolean('livemode'); - table.timestamps(true, true); - - }); - await create('transactions', function (table) { - table.increments(); - table.integer('invoice_id').references('user_invoices.id').onDelete('cascade'); - table.integer('user_id').references('users.id'); - table.string('charge_id'); - table.string('invoice'); - table.float('amount'); - table.boolean('refunded'); - table.float('amount_refunded'); - table.jsonb('refunds'); - table.boolean('captured'); - table.string('currency'); - table.string('dispute'); - table.boolean('paid'); - table.string('description'); - table.string('failure_code'); - table.string('failure_message'); - table.string('statement_descriptor'); - table.string('status'); - table.boolean('livemode'); - table.timestamps(true, true); - - }); - await create('user_upcoming_invoice', function (table) { - table.increments(); - table.integer('user_id').references('users.id').onDelete('cascade'); - table.bigInteger('next_payment'); - table.jsonb('invoice_json'); - table.timestamps(true, true); - console.log("Created 'user_upcoming_invoice' table."); - - }); - await create('password_reset_request', function (table) { - table.increments(); - table.integer('user_id').references('users.id').onDelete('cascade'); - table.string('hash'); - table.timestamps(true, true); - }); +let buildTables = async function (knex) { + + + console.log("Creating tables..."); + let create = createTable(knex); + + await create('user_roles', function (table) { + table.increments(); + table.string('role_name').unique(); + table.timestamps(true, true); + + }); + + await create('user_permissions', function (table) { + table.increments(); + table.string('permission_name').unique(); + table.timestamps(true, true); + + }); + await create('roles_to_permissions', function (table) { + table.increments(); + table.integer('role_id').references('user_roles.id').onDelete('cascade'); + table.integer('permission_id').references('user_permissions.id').onDelete('cascade'); + table.timestamps(true, true); + + }); + await create('users', function (table) { + table.increments(); + table.integer('role_id').references('user_roles.id'); + table.string('name'); + table.string('email').notNullable().unique(); + table.string('password'); + table.string('provider').defaultTo("local"); + table.enu('status', ['active', 'suspended', 'invited', 'flagged', 'disconnected']).defaultTo('active'); + table.string('customer_id'); + table.string('phone'); + table.timestamp('last_login'); + table.timestamps(true, true); + + }); + await create('funds', function (table) { + table.increments(); + table.integer('user_id').references('users.id').onDelete('cascade'); + table.boolean('flagged').defaultTo(false); + table.jsonb('source'); + table.timestamps(true, true); + + }); + await create('invitations', function (table) { + table.increments(); + table.string('token'); + table.integer('user_id').references('users.id').onDelete('cascade'); + table.timestamps(true, true); + + }); + await create('system_options', function (table) { + table.string('option').primary(); + table.string('value'); + table.boolean("public").defaultTo(false); + table.string('type'); + table.string('data_type'); + table.timestamps(true, true); + + }); + await create('event_logs', function (table) { + table.increments(); + table.integer('user_id'); + table.string('log_level'); + table.string('log_type'); + table.string('log'); + table.timestamps(true, true); + + }); + await create('files', function (table) { + table.increments(); + table.integer('user_id').references('users.id').onDelete('cascade'); + table.string('path'); + table.integer('size'); + table.string('name'); + table.string('mimetype'); + table.timestamps(true, true); + + }); + await create('notification_templates', function (table) { + table.increments(); + table.string('name'); + table.string('event_name'); + table.text('message', 'longtext'); + table.string('subject'); + table.string('description'); + table.string("model"); + table.specificType('additional_recipients', 'text[]'); + table.boolean("send_email").defaultTo(false); + table.boolean("send_to_owner").defaultTo(true); + table.boolean("create_notification").default(true); + table.timestamps(true, true); + + }); + await create("notification_templates_to_roles", function (table) { + table.increments(); + table.integer("notification_template_id").references("notification_templates.id"); + table.integer("role_id").references("user_roles.id"); + table.timestamps(true, true); + + }); + await create('notifications', function (table) { + table.increments(); + table.string("source_id").unique(); + table.text('message', 'longtext'); + table.string("type"); + table.integer("user_id").references("users.id").onDelete('cascade'); + table.string("subject"); + table.string("affected_versions").defaultTo("*"); + table.boolean("read").defaultTo(false); + table.boolean("email_delivered").defaultTo(false); + table.boolean("email_read").defaultTo(false); + table.timestamp('created_at').defaultTo(knex.fn.now()); + + }); + await create('service_categories', function (table) { + table.increments(); + table.string('name'); + table.string('description'); + table.timestamps(true, true); + + }); + await create('service_templates', function (table) { + table.increments(); + table.integer('category_id').references('service_categories.id'); + table.integer('created_by').references('users.id'); + table.string('name').notNullable().unique(); + // table.string('description'); + // table.text('details', 'longtext'); + table.boolean('published').defaultTo(false); + // table.string('statement_descriptor'); + // table.integer('trial_period_days'); + // table.float('amount'); + // table.float('overhead'); + // table.string('currency').defaultTo('usd'); + // table.string('interval'); + // table.integer('interval_count').defaultTo(1); + // table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); + // table.boolean('subscription_prorate').defaultTo(true); + // table.jsonb('split_configuration'); + table.timestamps(true, true); + + }); + await create("tiers", table => { + table.increments(); + table.string("name"); + table.specificType('features', 'text[]'); + table.integer('service_template_id').references('service_templates.id'); + table.timestamps(true, true); + }); + await create("payment_structure_templates", table => { + table.increments(); + table.integer('tier_id').references('tiers.id').onDelete('cascade');; + table.integer('trial_period_days'); + table.float('amount'); + table.string('currency').defaultTo('usd'); + table.string('interval'); + table.integer('interval_count').defaultTo(1); + table.boolean('subscription_prorate').defaultTo(true); + table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); + table.string('statement_descriptor'); + table.jsonb('split_configuration'); + table.timestamps(true, true); + }); + + await create('service_instances', function (table) { + table.increments(); + table.integer('service_id').references('service_templates.id'); + table.integer('user_id').references('users.id'); + table.integer('requested_by').references('users.id'); + table.jsonb('payment_plan'); + table.string('name'); + table.text('description', 'longtext'); + table.string('subscription_id'); + table.bigInteger('subscribed_at'); + table.bigInteger('trial_end'); + table.integer('payment_structure_template_id').references('payment_structure_templates.id'); + table.enu('status', ['running', 'requested', 'in_progress', 'waiting_cancellation', 'missing_payment', 'cancelled', 'completed']).defaultTo('missing_payment'); + table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); + table.jsonb('split_configuration'); + table.timestamps(true, true); + + }); + await create('service_instance_messages', function (table) { + table.increments(); + table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); + table.integer('user_id').references('users.id'); + table.string('message'); + table.timestamps(true, true); + + }); + await create('service_instance_cancellations', function (table) { + table.increments(); + table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); + table.integer('user_id').references('users.id'); + table.enu('status', ['waiting', 'approved', 'rejected']).defaultTo('waiting'); + table.integer('fulfilled_by').references('users.id'); + table.timestamps(true, true); + + }); + await create('properties', function (table) { + table.increments(); + table.string('name'); + table.string('type'); + table.jsonb('data'); + table.jsonb('config'); + table.string('prop_class'); + table.string('prop_label'); + table.string('prop_description'); + table.timestamps(true, true); + + }); + await create('service_template_properties', function (table) { + //Inherits the properties table. + table.inherits('properties'); + table.increments(); + table.integer('parent_id').references('service_templates.id').onDelete('cascade'); + table.boolean('private').defaultTo(false); + table.boolean('prompt_user').defaultTo(true); + table.boolean('required').defaultTo(false); + table.timestamps(true, true); + + }); + await create('service_instance_properties', function (table) { + //Inherits the properties table. + table.inherits('properties'); + table.increments(); + table.integer('parent_id').references('service_instances.id').onDelete('cascade'); + table.boolean('private').defaultTo(false); + table.boolean('prompt_user').defaultTo(true); + table.boolean('required').defaultTo(false); + table.timestamps(true, true); + + }); + await create('user_invoices', function (table) { + table.increments(); + table.integer('user_id').references('users.id'); + table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); + table.string('invoice_id').unique(); + table.string('subscription'); + table.string('charge'); + table.string('description'); + table.float('amount_due'); + table.float('discount'); + table.integer('attempt_count'); + table.boolean('closed'); + table.string('currency'); + table.boolean('forgiven'); + table.bigInteger('date'); + table.bigInteger('next_attempt'); + table.boolean('paid'); + table.bigInteger('period_end'); + table.bigInteger('period_start'); + table.string('receipt_number'); + table.float('starting_balance'); + table.float('ending_balance'); + table.float('total'); + table.boolean('livemode'); + table.timestamps(true, true); + + }); + await create('charge_items', function (table) { + table.increments(); + table.integer('user_id').references('users.id'); + table.integer('service_instance_id').references('service_instances.id').onDelete('cascade'); + table.boolean('approved').defaultTo(false); + table.string('subscription_id'); + table.string('item_id'); + table.float('amount'); + table.string('currency').defaultTo('usd'); + table.string('description'); + table.bigInteger('period_start'); + table.bigInteger('period_end'); + table.timestamps(true, true); + + }) + await create('user_invoice_lines', function (table) { + table.increments(); + table.integer('invoice_id').references('user_invoices.id').onDelete('cascade'); + table.string('line_item_id'); + table.float('amount'); + table.string('currency'); + table.string('description'); + table.boolean('proration'); + table.integer('quantity'); + table.string('type'); + table.boolean('livemode'); + table.timestamps(true, true); + + }); + await create('transactions', function (table) { + table.increments(); + table.integer('invoice_id').references('user_invoices.id').onDelete('cascade'); + table.integer('user_id').references('users.id'); + table.string('charge_id'); + table.string('invoice'); + table.float('amount'); + table.boolean('refunded'); + table.float('amount_refunded'); + table.jsonb('refunds'); + table.boolean('captured'); + table.string('currency'); + table.string('dispute'); + table.boolean('paid'); + table.string('description'); + table.string('failure_code'); + table.string('failure_message'); + table.string('statement_descriptor'); + table.string('status'); + table.boolean('livemode'); + table.timestamps(true, true); + + }); + await create('user_upcoming_invoice', function (table) { + table.increments(); + table.integer('user_id').references('users.id').onDelete('cascade'); + table.bigInteger('next_payment'); + table.jsonb('invoice_json'); + table.timestamps(true, true); + console.log("Created 'user_upcoming_invoice' table."); + + }); + await create('password_reset_request', function (table) { + table.increments(); + table.integer('user_id').references('users.id').onDelete('cascade'); + table.string('hash'); + table.timestamps(true, true); + }); console.log("***** All Tables successfully created *****"); }; -module.exports = function*(database){ - yield call(buildTables, database); +module.exports = function* (database) { + yield call(buildTables, database); }; \ No newline at end of file diff --git a/plugins/database/migrations/versions/0.10.0.js b/plugins/database/migrations/versions/0.10.0.js new file mode 100644 index 00000000..70265ec1 --- /dev/null +++ b/plugins/database/migrations/versions/0.10.0.js @@ -0,0 +1,98 @@ +module.exports = { + + + up: async function (knex) { + let ServiceTemplate = require("../../../../models/base/entity")("service_templates", [], "id", knex); + + await knex.schema.createTable("tiers", table => { + table.increments(); + table.string("name"); + table.specificType('features', 'text[]').defaultTo("{}"); + table.integer('service_template_id').references('service_templates.id'); + table.timestamps(true, true); + }); + await knex.schema.createTable("payment_structure_templates", table => { + table.increments(); + table.integer('tier_id').references('tiers.id').onDelete('cascade');; + table.integer('trial_period_days'); + table.float('amount'); + table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); + table.string('currency').defaultTo('usd'); + table.string('interval'); + table.integer('interval_count').defaultTo(1); + table.boolean('subscription_prorate').defaultTo(true); + table.string('statement_descriptor'); + table.jsonb('split_configuration'); + table.timestamps(true, true); + }); + + let templates = await ServiceTemplate.find(); + let tiers = templates.map(template => { + return { + name: template.data.name + "-tier", + service_template_id: template.data.id + } + }); + + let createdTiers = await knex("tiers").insert(tiers).returning("*"); + let templateMap = templates.reduce((acc, template) => { + acc[template.data.id] = template.data; + return acc; + }, {}); + let paymentTemplates = createdTiers.map(tier => { + let template = templateMap[tier.service_template_id]; + return { + amount: template.amount, + trial_period_days: template.trial_period_days, + currency: template.currency, + interval: template.interval, + interval_count: template.interval_count, + subscription_prorate: template.subscription_prorate, + statement_descriptor: template.statement_descriptor, + split_configuration: template.split_configuration, + tier_id: tier.id, + type: template.type + } + }); + let createdPayments = await knex("payment_structure_templates").insert(paymentTemplates); + await knex.schema.alterTable("service_templates", table => { + table.dropColumns("description", "type", "overhead", "details", "amount", "currency", "interval", "interval_count", "split_configuration", "statement_descriptor", "trial_period_days", "subscription_prorate") + }); + await knex.schema.alterTable("service_instances", table => { + table.integer('payment_structure_template_id').references('payment_structure_templates.id'); + }); + return await knex; + }, + + down: async function (knex) { + let paymentStructures = await knex("payment_structure_templates").select(); + let tiers = await knex("tiers").select(); + await knex.schema.alterTable("service_templates", table => { + table.integer('trial_period_days'); + table.float('amount'); + table.string('currency').defaultTo('usd'); + table.string('interval'); + table.string('description'); + table.text('details', 'longtext'); + table.integer('interval_count').defaultTo(1); + table.boolean('subscription_prorate').defaultTo(true); + table.string('statement_descriptor'); + table.float('overhead'); + table.jsonb('split_configuration'); + table.enu('type', ['subscription', 'one_time', 'custom', "split"]).defaultTo('subscription'); + + }); + await knex.schema.alterTable("service_instances", table => { + table.dropColumns('payment_structure_template_id'); + }); + for (let struct of paymentStructures) { + let service_template_id = tiers.find(tier => tier.id === struct.tier_id).service_template_id; + delete struct.id; + delete struct.tier_id; + await knex("service_templates").update(struct).where("id", service_template_id); + } + await knex.schema.dropTable("payment_structure_templates"); + await knex.schema.dropTable("tiers"); + + } +} \ No newline at end of file diff --git a/plugins/database/populate.js b/plugins/database/populate.js index 7ed128aa..1f552b01 100644 --- a/plugins/database/populate.js +++ b/plugins/database/populate.js @@ -144,9 +144,7 @@ module.exports = function (database, initConfig) { {"option": "company_email", "value": initConfig.company_email}, {"option": "hostname", "value": initConfig.hostname} ]; - SystemOption.batchUpdate(options, function (result) { - return resolve(result); - }) + return resolve(SystemOption.batchUpdate(options)); }); }).then(options => { return createAdmin(initConfig) diff --git a/plugins/database/tests/refresh_version.js b/plugins/database/tests/refresh_version.js index 07d780d8..48c3c2fc 100644 --- a/plugins/database/tests/refresh_version.js +++ b/plugins/database/tests/refresh_version.js @@ -3,5 +3,9 @@ require('dotenv').config({path: require("path").join(__dirname, '../../../env/.e let knex = require("../../../config/db"); -//require("../migrations/versions/0.9.0").down(knex); -require("../migrations/versions/0.9.0").up(knex); \ No newline at end of file +// require("../migrations/versions/0.9.0").down(knex); +try { + require("../migrations/versions/0.10.0").down(knex).catch(error => console.log(error)); +}catch(e){ + console.error(e); +} \ No newline at end of file diff --git a/plugins/service-lifecycle/main.js b/plugins/service-lifecycle/main.js index 89c83e4f..ea7f81f2 100644 --- a/plugins/service-lifecycle/main.js +++ b/plugins/service-lifecycle/main.js @@ -13,9 +13,9 @@ function* run(config, provide, channels) { pre_reactivate : [], post_reactivate : [], pre_property_change : [], - post_property_change : [] - - + post_property_change : [], + pre_payment_structure_change: [], + post_payment_structure_change: [] } //collect lifecycle hooks @@ -94,8 +94,27 @@ function* run(config, provide, channels) { } return result; + }, + prePaymentStructureChange : async function({instance, payment_structure_template}){ + let result = {} + for(let hook of lifecycles.pre_payment_structure_change){ + let hookresult = await hook.run({instance, payment_structure_template}); + result = {...result, ...hookresult}; + } + return result; + + }, + postPaymentStructureChange : async function({instance}){ + let result = {} + for(let hook of lifecycles.post_payment_structure_change){ + let hookresult = await hook.run({instance}); + result = {...result, ...hookresult}; + } + return result; + } + }; yield provide({lifecycleManager}) diff --git a/plugins/updates/updates.js b/plugins/updates/updates.js index 1d6ae99a..41681aba 100644 --- a/plugins/updates/updates.js +++ b/plugins/updates/updates.js @@ -13,7 +13,6 @@ let run = function* (config, provide, services) { let salt = process.env.INSTANCE_SALT // let hash = require("bcryptjs").hashSync(salt, 10).toString("hex"); let checkMaster = async function () { - console.log("CHECKIN WITH BIG PAPA FOR BIG ANNOUNCEMENTS") let version = process.env.npm_package_version; let data = await analytics.getAnalyticsData(); From eca60cd9224f189c2ae52aea1e06111304caa73d Mon Sep 17 00:00:00 2001 From: bsears90 Date: Wed, 16 May 2018 18:43:52 -0400 Subject: [PATCH 003/865] Changed template form to use tiers and payment structures --- api/service-instances.js | 2 +- .../service-template-form-refactored.jsx | 941 ++++++++++++++++++ .../pages/manage-catalog-create.jsx | 2 +- .../pages/manage-catalog-duplicate.jsx | 2 +- .../components/pages/manage-catalog-edit.jsx | 2 +- 5 files changed, 945 insertions(+), 4 deletions(-) create mode 100644 views/components/elements/forms/service-template-form-refactored.jsx diff --git a/api/service-instances.js b/api/service-instances.js index 3f14c8f6..979c4528 100644 --- a/api/service-instances.js +++ b/api/service-instances.js @@ -102,7 +102,7 @@ module.exports = function(router) { let newPaymentStructure = (await PaymentStructureTemplate.find({id: req.params.payment_structure_id}))[0]; let permission_array = res.locals.permissions; let hasPermission = (permission_array.some(p => p.get("permission_name") === "can_administrate" || p.get("permission_name") === "can_manage")); - if((instance_object.data.references.payment_structure_templates[0].type === "custom" || newPaymentStructure.data.type === "custom" ) && !hasPermission){ + if((instance_object.data.type === "custom" || newPaymentStructure.data.type === "custom" ) && !hasPermission){ return res.status(403).json({error: "Unauthorized"}); } instance_object.applyPaymentStructure(req.params.payment_structure_id, true).then(function (updatedInstance) { diff --git a/views/components/elements/forms/service-template-form-refactored.jsx b/views/components/elements/forms/service-template-form-refactored.jsx new file mode 100644 index 00000000..4cd6c480 --- /dev/null +++ b/views/components/elements/forms/service-template-form-refactored.jsx @@ -0,0 +1,941 @@ +import React from 'react'; +import {Link, browserHistory} from 'react-router'; +import 'react-tagsinput/react-tagsinput.css'; +import './css/template-create.css'; +import { + Field, + Fields, + FormSection, + FieldArray, + reduxForm, + formValueSelector, + change, + unregisterField, + getFormValues, + Form +} from 'redux-form' +import {connect} from "react-redux"; +import {RenderWidget, WidgetList, PriceBreakdown, widgets} from "../../utilities/widgets"; +import {WysiwygRedux} from "../../elements/wysiwyg.jsx"; +import FileUploadForm from "./file-upload-form.jsx"; +import { + inputField, + selectField, + OnOffToggleField, + iconToggleField, + priceField, + priceToCents +} from "./servicebot-base-field.jsx"; +import {addAlert, dismissAlert} from "../../utilities/actions"; +import ServiceBotBaseForm from "./servicebot-base-form.jsx"; +import SVGIcons from "../../utilities/svg-icons.jsx"; +import Load from "../../utilities/load.jsx"; + +let _ = require("lodash"); +import {required, email, numericality, length} from 'redux-form-validators' +import slug from "slug" + +const TEMPLATE_FORM_NAME = "serviceTemplateForm" +const selector = formValueSelector(TEMPLATE_FORM_NAME); // <-- same as form name + + +function renderSplits({fields, meta: {error, submitFailed}}) { + + function onAdd(e) { + e.preventDefault(); + let number_of_payments = e.target.value + let number_of_fields = fields.length + + while (number_of_fields < number_of_payments) { + fields.push({}); + number_of_fields++; + } + while (number_of_fields > number_of_payments) { + fields.pop() + number_of_fields--; + } + return (fields); + + } + + + return ( +
+
+ Number of payments +
+ + {submitFailed && error && {error}} +
+
+ +
    + {fields.map((member, index) => ( +
  • + + +

    Payment #{index + 1}

    + + =': 0.00})} + + /> + =': 0.00})} + /> +
  • + ))} +
+
+ ) +} + + +class CustomField extends React.Component { + + constructor(props) { + super(props); + + } + + componentWillReceiveProps(nextProps) { + let props = this.props; + if (nextProps.myValues.type !== props.myValues.type) { + props.clearConfig(); + props.clearValue(); + } + if ((props.templateType && nextProps.templateType !== props.templateType)) { + props.clearPricing(); + } + } + + render() { + + let props = this.props; + let { + willAutoFocus, index, typeValue, member, myValues, privateValue, requiredValue, promptValue, configValue, + setPrivate, setRequired, setPrompt, changePrivate, changeRequired, changePrompt, templateType + } = props; + let machineName; + + if (myValues.prop_label) { + willAutoFocus = false; + machineName = slug(myValues.prop_label, {lower: true}); + + } + return ( +
+
+ +
+ +
+ +
+ +
+ {!privateValue && !requiredValue && + + } + {!privateValue && + + } + {!requiredValue && !promptValue && + + } +
+
+ {machineName && +
+ +
{machineName}
+
} + {typeValue && + } +
+
+ ) + }; +} + +CustomField = connect((state, ownProps) => { + return { + "privateValue": selector(state, "references.service_template_properties")[ownProps.index].private, + "requiredValue": selector(state, "references.service_template_properties")[ownProps.index].required, + "promptValue": selector(state, "references.service_template_properties")[ownProps.index].prompt_user, + "typeValue": selector(state, "references.service_template_properties")[ownProps.index].type, + "configValue": selector(state, `references.service_template_properties`)[ownProps.index].config, + "myValues": selector(state, `references.${ownProps.member}`), + + + } +}, (dispatch, ownProps) => { + return { + "setPrivate": (val) => { + if (val == true) { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, true)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, false)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, false)); + } else { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + } + }, + "setRequired": (val) => { + if (val == true) { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, true)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, true)); + } else { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, false)); + } + }, + "setPrompt": (val) => { + if (val == true) { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, true)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + } else { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, false)); + } + }, + "changePrivate": (event) => { + if (!event.currentTarget.value || event.currentTarget.value == 'false') { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, false)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, false)); + } + }, + "changeRequired": (event) => { + if (!event.currentTarget.value || event.currentTarget.value == 'false') { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, true)); + } + }, + "changePrompt": (event) => { + if (!event.currentTarget.value || event.currentTarget.value == 'false') { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + } + }, + "clearConfig": () => { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.config`, {})); + }, + "clearPricing": () => { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.config.pricing`, null)); + }, + "clearValue": () => { + dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.data`, null)); + } + } +})(CustomField); + + +//Custom property +class renderCustomProperty extends React.Component { + + constructor(props) { + super(props); + + this.state = { + customFields: [], + }; + + + this.onAdd = this.onAdd.bind(this); + } + + onAdd(e) { + e.preventDefault(); + const {privateValue, fields, meta: {touched, error}} = this.props; + return (fields.push({})); + } + + render() { + let props = this.props; + const {templateType, privateValue, fields, meta: {touched, error}} = props; + return ( +
+
    + {fields.map((customProperty, index) => +
  • +
    + {/*{fields.get(index).prop_label ?*/} + {/*

    {fields.get(index).prop_label}

    :

    Field #{index + 1}

    */} + {/*}*/} + +
    + +
  • + )} +
  • +
    + +
    + {/**/} + {touched && error && {error}} +
  • +
+
+ ) + }; +} + + +//The full form +let PaymentStructureTemplates = function (props) { + let {fields, tier, member, setPricingTemplates} = props; + let plans = fields.getAll() || []; + + function addPayment(e) { + e.preventDefault(); + fields.push({ + interval_count: 1, + trial_period_days: tier.trial_period_days, + type: tier.type, + // statement_descriptor: this.props.company_name.value.substring(0, 22), + amount: 0 + + }); + }; + + function deletePlan(index) { + return (e) => { + e.preventDefault(); + fields.remove(index) + } + } + + + let availableOptions = [ + {id: "month", name: "Month"}, + {id: "year", name: "Year"}, + {id: "day", name: "Day"}, + {id: "week", name: "Week"} + ]; + let optionMap = plans.map(plan => { + return availableOptions.filter(option => option.id === plan.interval || !plans.some(plan2 => plan2.interval === option.id)); + }); + // ].filter(option => { + // return !plans.some(plan => plan.interval === option.id); + // }); + if (tier.type === "custom") { + return (
+

Quotes are built for services that are customer specific. If your service is + priced + based on the customer's use-case, use this option. Once the quote service has + been + requested by the customer, you can add charges to the service at anytime. +

+
); + + } + if (tier.type === "one_time") { + let field = fields[0]; + return (=': 0.00})}/>); + + } + if (tier.type === "subscription") { + return (
+ {fields.map((field, index) => { + return (
+ + + =': 0.00})}/> + {fields.length > 1 && } +
) + })} + + {plans.length < 4 && } +
) + } else { + return
+ } +} + + +let TierBillingForm = function (props) { + let {tier, member, setPricingTemplates, serviceTypeValue} = props; + const changeServiceType = (event, newValue) => { + let pricingStructures = (tier.references && tier.references.pricing_structure_templates) || []; + setPricingTemplates(pricingStructures.map(p => { + return { + ...p, + type: newValue + } + })); + if (newValue === 'one_time') { + // props.setIntervalCount(); + // props.setInterval(); + } + else if (newValue === 'custom' || newValue === 'split') { + // props.setIntervalCount(); + // props.setInterval(); + // props.clearAmount(); + } + }; + const changeTrial = (event, newValue) => { + let pricingStructures = (tier.references && tier.references.pricing_structure_templates) || []; + setPricingTemplates(pricingStructures.map(p => { + return { + ...p, + trial_period_days: newValue + } + })); + }; + + + return (
+ + + + + {tier.type === "subscription" && } + + + + + +
) +} + +let Tiers = function (props) { + let {member, fields, selected, selectTier, overrideBilling, setPricingTemplate} = props; + selectTier = selectTier || (() => { + }); + let current = fields.get(selected); + + function deleteTier(index) { + return (e) => { + e.preventDefault(); + selectTier(0)(e); + fields.remove(index) + } + } + + function onAdd(e) { + e.preventDefault(); + selectTier(fields.length)() + return fields.push({references: {payment_structure_templates: [{}]}, name: fields.length + 1}); + } + + return (
+
    + {fields.map((field, index) => { + let liClass = "tier" + let tier = fields.get(index); + if (index === selected) { + liClass = liClass + " selected"; + + } + return (
  • + {fields.length > 1 && } +
    +

    {tier.name}

    +
    +
    +
    +
  • ) + })} +
  • + ADD +
  • + +
+ +
) + +}; +Tiers = connect((state, ownProps) => { + return { + // "privateValue": selector(state, "references.service_template_properties")[ownProps.index].private, + // "requiredValue": selector(state, "references.service_template_properties")[ownProps.index].required, + // "promptValue": selector(state, "references.service_template_properties")[ownProps.index].prompt_user, + // "typeValue": selector(state, "references.service_template_properties")[ownProps.index].type, + // "configValue": selector(state, `references.service_template_properties`)[ownProps.index].config, + // "tiers": selector(state, `references.tiers`) + + + } +}, (dispatch, ownProps) => { + return { + "setPricingTemplates": (val) => { + dispatch(change(TEMPLATE_FORM_NAME, `references.pricing_structure_templates`, val)); + }, + // "setRequired": (val) => { + // if (val == true) { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, true)); + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, true)); + // } else { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, false)); + // } + // }, + // "setPrompt": (val) => { + // if (val == true) { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, true)); + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + // } else { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, false)); + // } + // }, + // "changePrivate": (event) => { + // if (!event.currentTarget.value || event.currentTarget.value == 'false') { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.required`, false)); + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, false)); + // } + // }, + // "changeRequired": (event) => { + // if (!event.currentTarget.value || event.currentTarget.value == 'false') { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.prompt_user`, true)); + // } + // }, + // "changePrompt": (event) => { + // if (!event.currentTarget.value || event.currentTarget.value == 'false') { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.private`, false)); + // } + // }, + // "clearConfig": () => { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.config`, {})); + // }, + // "clearPricing": () => { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.config.pricing`, null)); + // }, + // "clearValue": () => { + // dispatch(change(TEMPLATE_FORM_NAME, `references.${ownProps.member}.data`, null)); + // } + } +})(Tiers); + + +class TemplateForm extends React.Component { + + constructor(props) { + super(props); + this.state = { + selectedTier: 0 + }; + this.selectTier = this.selectTier.bind(this) + } + + selectTier(index) { + let self = this; + return (event) => { + self.setState({ + selectedTier: index + }); + } + } + + render() { + let self = this; + let props = this.props; + + const changeServiceType = (event, newValue) => { + if (newValue === 'one_time') { + props.setIntervalCount(); + props.setInterval(); + } + else if (newValue === 'custom' || newValue === 'split') { + props.setIntervalCount(); + props.setInterval(); + props.clearAmount(); + } + }; + + + const {handleSubmit, pristine, reset, submitting, error, serviceTypeValue, invalid, formJSON, options} = props; + + const sectionDescriptionStyle = { + background: _.get(options, 'service_template_icon_background_color.value', '#000000'), + height: "100px", + width: "100px", + padding: "30px", + marginLeft: "50%", + transform: "translateX(-50%)", + borderRadius: "50%", + }; + + return ( + +
+
+
+
+ {!options.stripe_publishable_key && +

Publishing Disabled Until Setup + Complete - Click here to complete

} + {error &&
{error}
} +
+
+

Service Info

+ + +
+ +
+
+
+
+
+
+

Tiers

+ +
+ + + +
+ + {/*{(serviceTypeValue === 'split') &&*/} + {/*
*/} + + {/**/} + {/**/} + + {/**/} + + {/*
*/} + {/*}*/} + +
+ + +
+
+
+
+
+
+
+
+ {/*

Custom Fields

*/} + {/**/} + {/**/} + {/**/} + {/*{props.formJSON.references && props.formJSON.references.service_template_properties &&*/} + {/**/} + {/*}*/} +
+ Go + Back + +
+
+
+
+
+ + ) + }; +} + + +class ServiceTemplateForm extends React.Component { + + constructor(props) { + super(props); + this.state = { + newTemplateId: 0, + success: false, + imageSuccess: false, + iconSuccess: false + }; + this.handleResponse = this.handleResponse.bind(this); + this.handleImageSuccess = this.handleImageSuccess.bind(this); + this.handleIconSuccess = this.handleIconSuccess.bind(this); + this.submissionPrep = this.submissionPrep.bind(this); + } + + handleImageSuccess() { + this.setState({ + imageSuccess: true + }); + } + + handleIconSuccess() { + this.setState({ + iconSuccess: true + }); + } + + handleResponse(response) { + this.setState({ + newTemplateId: response.id, + success: true + }); + let successMessage = { + id: Date.now(), + alertType: 'success', + message: `${response.name} was saved successfully`, + show: true, + autoDismiss: 4000, + }; + this.props.addAlert(successMessage); + browserHistory.push(`/manage-catalog/list`); + } + + submissionPrep(values) { + //remove id's for duplicate template operation + if (this.props.params.duplicate) { + console.log("We have a duplicate and we want to remove id"); + delete values.id; + values.references.service_template_properties = values.references.service_template_properties.map(prop => { + if (prop.id) { + delete prop.id; + } + return prop; + }) + } + return values; + } + + render() { + //Todo change this. this is how we are currently making sure the redux store is populated + if (!this.props.company_name) { + return (); + } else { + let initialValues = {}; + let initialRequests = []; + let submissionRequest = {}; + let successMessage = "Template Updated"; + let imageUploadURL = `/api/v1/service-templates/${this.state.newTemplateId}/image`; + let iconUploadURL = `/api/v1/service-templates/${this.state.newTemplateId}/icon`; + + if (this.props.params.templateId) { + initialRequests.push({ + 'method': 'GET', + 'url': `/api/v1/service-templates/${this.props.params.templateId}` + }, + {'method': 'GET', 'url': `/api/v1/service-categories`, 'name': '_categories'}, + ); + if (this.props.params.duplicate) { + submissionRequest = { + 'method': 'POST', + 'url': `/api/v1/service-templates` + }; + successMessage = "Template Duplicated"; + } + else { + submissionRequest = { + 'method': 'PUT', + 'url': `/api/v1/service-templates/${this.props.params.templateId}` + }; + successMessage = "Template Updated"; + imageUploadURL = `/api/v1/service-templates/${this.props.params.templateId}/image`; + iconUploadURL = `/api/v1/service-templates/${this.props.params.templateId}/icon`; + } + } + else { + initialValues = { + category_id: 1, + interval: 'month', + published: !!this.props.fieldState.options.stripe_publishable_key, + references: { + tiers: [ + { + references: { + payment_structure_templates: [{ + interval: 'month', + interval_count: 1, + trial_period_days: 0, + type: 'subscription', + statement_descriptor: this.props.company_name.value.substring(0, 22), + amount: 0 + } + ] + }, name: "Tier 1" + } + ] + } + }; + initialRequests.push( + {'method': 'GET', 'url': `/api/v1/service-categories`, 'name': '_categories'}, + ); + submissionRequest = { + 'method': 'POST', + 'url': `/api/v1/service-templates` + }; + successMessage = "Template Created"; + } + + return ( +
+
+
+ {/*{(!this.state.imageSuccess || !this.state.iconSuccess || !this.state.success) &&*/} + {/*
*/} + + {/**/} + {/**/} + {/*
*/} + {/*}*/} +
+
+ +
+
+
+ ) + } + } +} + +function mapStateToProps(state) { + return { + alerts: state.alerts, + company_name: state.options.company_name, + fieldState: { + "options": state.options, + "serviceTypeValue": selector(state, `type`), + formJSON: getFormValues(TEMPLATE_FORM_NAME)(state), + }, + } +} + +const mapDispatchToProps = (dispatch) => { + return { + addAlert: (alert) => { + return dispatch(addAlert(alert)) + }, + dismissAlert: (alert) => { + return dispatch(dismissAlert(alert)) + }, + fieldDispatches: { + 'setIntervalCount': () => { + dispatch(change(TEMPLATE_FORM_NAME, `interval_count`, 1)) + }, + 'setInterval': () => { + dispatch(change(TEMPLATE_FORM_NAME, `interval`, 'day')) + }, + 'clearAmount': () => { + dispatch(change(TEMPLATE_FORM_NAME, `amount`, 0)) + } + } + + } +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ServiceTemplateForm); \ No newline at end of file diff --git a/views/components/pages/manage-catalog-create.jsx b/views/components/pages/manage-catalog-create.jsx index 3db2516b..099fc261 100644 --- a/views/components/pages/manage-catalog-create.jsx +++ b/views/components/pages/manage-catalog-create.jsx @@ -1,6 +1,6 @@ import React from 'react'; import {Link, browserHistory} from 'react-router'; -import ServiceTemplateForm from '../elements/forms/service-template-form.jsx'; +import ServiceTemplateForm from '../elements/forms/service-template-form-refactored.jsx'; class ManageCatalogCreate extends React.Component { diff --git a/views/components/pages/manage-catalog-duplicate.jsx b/views/components/pages/manage-catalog-duplicate.jsx index 3637c2c3..6e939069 100644 --- a/views/components/pages/manage-catalog-duplicate.jsx +++ b/views/components/pages/manage-catalog-duplicate.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ServiceTemplateForm from '../elements/forms/service-template-form.jsx'; +import ServiceTemplateForm from '../elements/forms/service-template-form-refactored.jsx'; class ManageCatalogDuplicate extends React.Component { diff --git a/views/components/pages/manage-catalog-edit.jsx b/views/components/pages/manage-catalog-edit.jsx index 23a2d951..9142f5af 100644 --- a/views/components/pages/manage-catalog-edit.jsx +++ b/views/components/pages/manage-catalog-edit.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import ServiceTemplateForm from '../elements/forms/service-template-form.jsx'; +import ServiceTemplateForm from '../elements/forms/service-template-form-refactored.jsx'; class ManageCatalogEdit extends React.Component { From fa971238b1875c8ec9cb33492e5eba7e2b0bc5ef Mon Sep 17 00:00:00 2001 From: bsears90 Date: Thu, 17 May 2018 15:20:08 -0400 Subject: [PATCH 004/865] fixed issue with duplicate and update and list --- api/entity.js | 10 +- api/service-templates.js | 17 ++- models/base/entity.js | 1 + plugins/database/initialize.js | 4 +- .../database/migrations/versions/0.10.0.js | 2 +- .../service-template-form-refactored.jsx | 103 +++++++++++++----- .../elements/forms/servicebot-base-form.jsx | 6 +- .../components/pages/manage-catalog-list.jsx | 28 ++--- 8 files changed, 119 insertions(+), 52 deletions(-) diff --git a/api/entity.js b/api/entity.js index 85f5db8d..594360f7 100644 --- a/api/entity.js +++ b/api/entity.js @@ -178,14 +178,16 @@ module.exports = function (router, model, resourceName, userCorrelator) { res.status(500).send({ error: "Error creating new " + resourceName }) } else { - if (references.length === 0 || req.body.references === undefined || req.body.references.length === 0) { + if (references.length === 0 || req.body.references === undefined || Object.keys(req.body.references).length === 0) { res.locals.json = newEntity.data; + console.log("NO REFS") store.dispatchEvent(`${model.table}_created`, newEntity) EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); next(); } else { let requestReferenceData = req.body.references; + console.log(requestReferenceData); newEntity.data.references = {}; for (let reference of references) { let referenceData = requestReferenceData[reference.model.table]; @@ -193,10 +195,10 @@ module.exports = function (router, model, resourceName, userCorrelator) { newEntity.data.references[reference.model.table] = await newEntity.updateReferences(referenceData, reference); } res.locals.json= newEntity.data; - store.dispatchEvent(`${model.table}_created`, newEntity) - EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); - next(); } + store.dispatchEvent(`${model.table}_created`, newEntity) + EventLogs.logEvent(req.user.get('id'), `${resourceName} ${newEntity.get(model.primaryKey)} was created by user ${req.user.get('email')}`); + next(); } } }); diff --git a/api/service-templates.js b/api/service-templates.js index a2688b74..3d64babe 100644 --- a/api/service-templates.js +++ b/api/service-templates.js @@ -478,9 +478,21 @@ module.exports = function (router) { router.post("/service-templates", auth(), function (req, res, next) { req.body.created_by = req.user.get("id"); - req.body.trial_period_days = req.body.trial_period_days || 0; - req.body.currency = store.getState().options.currency; + // req.body.trial_period_days = req.body.trial_period_days || 0; + // req.body.currency = store.getState().options.currency; let properties = req.body.references && req.body.references.service_template_properties; + let tiers = req.body.references && req.body.references.tiers; + + if(tiers) { + req.body.references.tiers = tiers.map(tier => { + let paymentTemplates = tier.references && tier.references.payment_structure_templates || []; + tier.references.payment_structure_templates = paymentTemplates.map(payment => { + payment.currency = store.getState().options.currency; + return payment; + }) + return tier; + }) + } if(properties){ req.body.references.service_template_properties = properties.map(prop => { return { @@ -489,6 +501,7 @@ module.exports = function (router) { }; }); } + ServiceTemplate.findAll("name", req.body.name, (templates) => { if (templates && templates.length > 0) { res.status(400).json({error: "Service template name already in use"}) diff --git a/models/base/entity.js b/models/base/entity.js index 4e479dc5..cc1638c4 100644 --- a/models/base/entity.js +++ b/models/base/entity.js @@ -206,6 +206,7 @@ var CreateEntity = function (tableName, references = [], primaryKey = 'id', data this; } else { + console.error(referenceData, reference); let ids = referenceData.reduce((acc, refInstance) => acc.concat(refInstance.id || []), []); referenceData.forEach(newChild => (newChild[reference.referenceField] = this.get(primaryKey))); diff --git a/plugins/database/initialize.js b/plugins/database/initialize.js index b5fefe2f..fa7c814e 100644 --- a/plugins/database/initialize.js +++ b/plugins/database/initialize.js @@ -162,12 +162,12 @@ let buildTables = async function (knex) { table.increments(); table.string("name"); table.specificType('features', 'text[]'); - table.integer('service_template_id').references('service_templates.id'); + table.integer('service_template_id').references('service_templates.id').onDelete('cascade'); table.timestamps(true, true); }); await create("payment_structure_templates", table => { table.increments(); - table.integer('tier_id').references('tiers.id').onDelete('cascade');; + table.integer('tier_id').references('tiers.id').onDelete('cascade'); table.integer('trial_period_days'); table.float('amount'); table.string('currency').defaultTo('usd'); diff --git a/plugins/database/migrations/versions/0.10.0.js b/plugins/database/migrations/versions/0.10.0.js index 70265ec1..c295b44a 100644 --- a/plugins/database/migrations/versions/0.10.0.js +++ b/plugins/database/migrations/versions/0.10.0.js @@ -8,7 +8,7 @@ module.exports = { table.increments(); table.string("name"); table.specificType('features', 'text[]').defaultTo("{}"); - table.integer('service_template_id').references('service_templates.id'); + table.integer('service_template_id').references('service_templates.id').onDelete("cascade"); table.timestamps(true, true); }); await knex.schema.createTable("payment_structure_templates", table => { diff --git a/views/components/elements/forms/service-template-form-refactored.jsx b/views/components/elements/forms/service-template-form-refactored.jsx index 4cd6c480..5fdf1fed 100644 --- a/views/components/elements/forms/service-template-form-refactored.jsx +++ b/views/components/elements/forms/service-template-form-refactored.jsx @@ -344,7 +344,7 @@ let PaymentStructureTemplates = function (props) { e.preventDefault(); fields.push({ interval_count: 1, - trial_period_days: tier.trial_period_days, + trial_period_days: tier.trial_period_days || 0, type: tier.type, // statement_descriptor: this.props.company_name.value.substring(0, 22), amount: 0 @@ -419,26 +419,17 @@ let PaymentStructureTemplates = function (props) { let TierBillingForm = function (props) { let {tier, member, setPricingTemplates, serviceTypeValue} = props; const changeServiceType = (event, newValue) => { - let pricingStructures = (tier.references && tier.references.pricing_structure_templates) || []; - setPricingTemplates(pricingStructures.map(p => { + let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; + setPricingTemplates(member, pricingStructures.map(p => { return { ...p, type: newValue } })); - if (newValue === 'one_time') { - // props.setIntervalCount(); - // props.setInterval(); - } - else if (newValue === 'custom' || newValue === 'split') { - // props.setIntervalCount(); - // props.setInterval(); - // props.clearAmount(); - } }; const changeTrial = (event, newValue) => { - let pricingStructures = (tier.references && tier.references.pricing_structure_templates) || []; - setPricingTemplates(pricingStructures.map(p => { + let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; + setPricingTemplates(member, pricingStructures.map(p => { return { ...p, trial_period_days: newValue @@ -446,6 +437,15 @@ let TierBillingForm = function (props) { })); }; + const formatFromPricing = (value, name) => { + console.log("VAL", value); + let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; + if((value === null || value === undefined) && pricingStructures.length > 0){ + return pricingStructures[0][name]; + } + return value; + }; + return (
- {tier.type === "subscription" && } @@ -494,7 +495,21 @@ let Tiers = function (props) { function onAdd(e) { e.preventDefault(); selectTier(fields.length)() - return fields.push({references: {payment_structure_templates: [{}]}, name: fields.length + 1}); + return fields.push({ + references: { + payment_structure_templates: [{ + trial_period_days: 0, + amount: 0, + type:"subscription", + interval: "month", + interval_count: 1 + }] + }, + name: "Tier " + (fields.length + 1), + trial_period_days: 0, + amount: 0, + type:"subscription" + }); } return (
@@ -537,8 +552,8 @@ Tiers = connect((state, ownProps) => { } }, (dispatch, ownProps) => { return { - "setPricingTemplates": (val) => { - dispatch(change(TEMPLATE_FORM_NAME, `references.pricing_structure_templates`, val)); + "setPricingTemplates": (member, val) => { + dispatch(change(TEMPLATE_FORM_NAME, `references.${member}.references.payment_structure_templates`, val)); }, // "setRequired": (val) => { // if (val == true) { @@ -783,6 +798,21 @@ class ServiceTemplateForm extends React.Component { } return prop; }) + values.references.tiers = values.references.tiers.map(tier => { + if (tier.id) { + delete tier.id; + } + if(tier.references && tier.references.payment_structure_templates){ + tier.references.payment_structure_templates = tier.references.payment_structure_templates.map(pay => { + if(pay.id){ + delete pay.id; + } + return pay; + }) + } + return tier; + }) + } return values; } @@ -792,18 +822,36 @@ class ServiceTemplateForm extends React.Component { if (!this.props.company_name) { return (); } else { + let templateId = this.props.params.templateId; let initialValues = {}; let initialRequests = []; let submissionRequest = {}; let successMessage = "Template Updated"; let imageUploadURL = `/api/v1/service-templates/${this.state.newTemplateId}/image`; let iconUploadURL = `/api/v1/service-templates/${this.state.newTemplateId}/icon`; - - if (this.props.params.templateId) { + function initializer(values){ + if(templateId){ + values.references.tiers = (values._tiers || []).map(tier => { + let paymentPlans = tier.references.payment_structure_templates; + if(paymentPlans && paymentPlans.length > 0){ + return { + ...tier, + trial_period_days: paymentPlans[0].trial_period_days, + type: paymentPlans[0].type + } + }else{ + return tier; + } + }); + } + return values; + } + if (templateId) { initialRequests.push({ 'method': 'GET', - 'url': `/api/v1/service-templates/${this.props.params.templateId}` + 'url': `/api/v1/service-templates/${templateId}` }, + {'method': 'GET', 'url': `/api/v1/tiers/search?key=service_template_id&value=${templateId}`, 'name': '_tiers'}, {'method': 'GET', 'url': `/api/v1/service-categories`, 'name': '_categories'}, ); if (this.props.params.duplicate) { @@ -816,11 +864,11 @@ class ServiceTemplateForm extends React.Component { else { submissionRequest = { 'method': 'PUT', - 'url': `/api/v1/service-templates/${this.props.params.templateId}` + 'url': `/api/v1/service-templates/${templateId}` }; successMessage = "Template Updated"; - imageUploadURL = `/api/v1/service-templates/${this.props.params.templateId}/image`; - iconUploadURL = `/api/v1/service-templates/${this.props.params.templateId}/icon`; + imageUploadURL = `/api/v1/service-templates/${templateId}/image`; + iconUploadURL = `/api/v1/service-templates/${templateId}/icon`; } } else { @@ -831,13 +879,13 @@ class ServiceTemplateForm extends React.Component { references: { tiers: [ { + trial_period_days: 0, references: { payment_structure_templates: [{ interval: 'month', interval_count: 1, trial_period_days: 0, type: 'subscription', - statement_descriptor: this.props.company_name.value.substring(0, 22), amount: 0 } ] @@ -890,6 +938,7 @@ class ServiceTemplateForm extends React.Component { submissionRequest={submissionRequest} successMessage={successMessage} handleResponse={this.handleResponse} + initializer={initializer} formProps={{ ...this.props.fieldDispatches, ...this.props.fieldState diff --git a/views/components/elements/forms/servicebot-base-form.jsx b/views/components/elements/forms/servicebot-base-form.jsx index f840cb2c..6409fa7e 100644 --- a/views/components/elements/forms/servicebot-base-form.jsx +++ b/views/components/elements/forms/servicebot-base-form.jsx @@ -137,14 +137,16 @@ class ServiceBotBaseForm extends React.Component { } return response; }); - Promise.all(allRequests).then(values => { + Promise.all(allRequests).then(async values => { //Check for errors and unauthenticated! let error = values.find(value => value.error); if (!error) { let requestValues = values.reduce((acc, value, currentIndex) => (value._name ? {...acc, [value._name]: value} : {...acc, ...value}), self.state.initialValues) - + if(self.props.initializer){ + requestValues = await self.props.initializer(requestValues); + } let initialForm = reduxForm({ form: self.props.formName || "servicebotForm", initialValues: requestValues, diff --git a/views/components/pages/manage-catalog-list.jsx b/views/components/pages/manage-catalog-list.jsx index 53d5b1ef..42fc1f96 100644 --- a/views/components/pages/manage-catalog-list.jsx +++ b/views/components/pages/manage-catalog-list.jsx @@ -216,20 +216,20 @@ class ManageCatalogList extends React.Component { width={200}> Product / Service Name - - Pricing - - - Type - + {/**/} + {/*Pricing*/} + {/**/} + {/**/} + {/*Type*/} + {/**/} Date: Tue, 22 May 2018 19:41:10 -0400 Subject: [PATCH 005/865] added ability to tier specific properties --- models/service-instance.js | 24 ++++++++++++++++--- plugins/database/initialize.js | 5 ++-- .../database/migrations/versions/0.10.0.js | 5 ++-- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/models/service-instance.js b/models/service-instance.js index de23f62f..ecf3945c 100644 --- a/models/service-instance.js +++ b/models/service-instance.js @@ -291,7 +291,12 @@ ServiceInstance.prototype.changeProperties = async function (properties) { ServiceInstance.prototype.applyPaymentStructure = async function (paymentStructureId, ignoreTrial) { let instance_object = await this.attachReferences(); - let currentStructure = instance_object.data.references.payment_structure_templates[0]; + let {payment_plan, references : {service_instance_properties, payment_structure_templates}} = instance_object.data; + let currentStructure = payment_structure_templates[0]; + let handlers = (store.getState(true).pluginbot.services.inputHandler || []).reduce((acc, handler) => { + acc[handler.name] = handler.handler; + return acc; + }, {}); if (!currentStructure) { throw "Instance has no payment structure"; @@ -319,10 +324,22 @@ ServiceInstance.prototype.applyPaymentStructure = async function (paymentStructu payment_structure_template: paymentStructureTemplate, }); } + let applicableProperties = service_instance_properties.filter(property => { + if(property.config && property.config.pricing && property.config.pricing.tiers){ + return property.config.pricing.tiers.includes(tier.data.name); + }else{ + return true; + } + }); + let newPrice = require("../lib/handleInputs").getPrice(applicableProperties, handlers, paymentStructureTemplate.data.amount); + let paymentPlan = { ...paymentStructureTemplate.data, - name : tier.data.name + "-" + paymentStructureTemplate.data.id - } + name : tier.data.name + "-" + paymentStructureTemplate.data.id, + amount : newPrice + }; + + let newInstance = await (await instance_object.changePaymentPlan(paymentPlan, ignoreTrial)).attachReferences(); console.log("before", newInstance); @@ -364,6 +381,7 @@ ServiceInstance.prototype.changePaymentPlan = async function (newPlan, ignorePla //todo: handle this better, don't like dispatching here. store.dispatchEvent("service_instance_trial_change", updatedInstance); } + } updatedInstance.data.type = updatedInstance.data.payment_plan.amount > 0 ? "subscription" : "custom"; return await updatedInstance.update(); diff --git a/plugins/database/initialize.js b/plugins/database/initialize.js index fa7c814e..13d433f1 100644 --- a/plugins/database/initialize.js +++ b/plugins/database/initialize.js @@ -160,9 +160,10 @@ let buildTables = async function (knex) { }); await create("tiers", table => { table.increments(); - table.string("name"); + table.string("name").notNullable(); table.specificType('features', 'text[]'); - table.integer('service_template_id').references('service_templates.id').onDelete('cascade'); + table.integer('service_template_id').notNullable().references('service_templates.id').onDelete('cascade'); + table.unique(['service_template_id', "name"]); table.timestamps(true, true); }); await create("payment_structure_templates", table => { diff --git a/plugins/database/migrations/versions/0.10.0.js b/plugins/database/migrations/versions/0.10.0.js index c295b44a..8e5ad6af 100644 --- a/plugins/database/migrations/versions/0.10.0.js +++ b/plugins/database/migrations/versions/0.10.0.js @@ -6,9 +6,10 @@ module.exports = { await knex.schema.createTable("tiers", table => { table.increments(); - table.string("name"); + table.string("name").notNullable(); table.specificType('features', 'text[]').defaultTo("{}"); - table.integer('service_template_id').references('service_templates.id').onDelete("cascade"); + table.integer('service_template_id').notNullable().references('service_templates.id').onDelete("cascade"); + table.unique(['service_template_id', "name"]); table.timestamps(true, true); }); await knex.schema.createTable("payment_structure_templates", table => { From bd4a5fe69605c6427f521ad48eaa06b803f26a28 Mon Sep 17 00:00:00 2001 From: bsears90 Date: Wed, 23 May 2018 16:03:41 -0400 Subject: [PATCH 006/865] null statement descriptor works now --- models/service-instance.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/service-instance.js b/models/service-instance.js index ecf3945c..c48e1779 100644 --- a/models/service-instance.js +++ b/models/service-instance.js @@ -57,7 +57,7 @@ let buildPayStructure = function (payment_object, callback) { if (plan.amount === null) { plan.amount = 0; } - plan.statement_descriptor = plan.statement_descriptor.substring(0, 22); + plan.statement_descriptor = (plan.statement_descriptor || "Subscription").substring(0, 22); console.log(plan); callback(plan); From e9b3393efca1c986b91013a385cf5bb21c6f9425 Mon Sep 17 00:00:00 2001 From: Ben Sears Date: Wed, 23 May 2018 17:04:01 -0400 Subject: [PATCH 007/865] updated tiers, put payment template stuff in another file --- plugins/client-plugins/ga/index.js | 4 +- .../service-template-form-refactored.jsx | 185 ++---------------- .../elements/forms/tier-billing-form.jsx | 144 ++++++++++++++ 3 files changed, 160 insertions(+), 173 deletions(-) create mode 100644 views/components/elements/forms/tier-billing-form.jsx diff --git a/plugins/client-plugins/ga/index.js b/plugins/client-plugins/ga/index.js index 03e84d7d..b78ca8f7 100644 --- a/plugins/client-plugins/ga/index.js +++ b/plugins/client-plugins/ga/index.js @@ -1,8 +1,6 @@ import {call, put, all, select, fork, spawn, take, takeEvery} from "redux-saga/effects"; import consume from "pluginbot/effects/consume" import ReactGA from 'react-ga'; - - let actionHandler = function(action, state){ switch(action.type){ case "@@redux-form/SET_SUBMIT_SUCCEEDED": @@ -34,6 +32,7 @@ let actionHandler = function(action, state){ function* run(config, provide, channels) { //todo pull initialize from channel? + console.log("HELLO!!!!!!"); let { initialState } = yield take("INITIALIZE"); if(initialState.options.google_analytics && initialState.options.google_analytics.value ){ ReactGA.initialize(initialState.options.google_analytics.value); @@ -48,6 +47,7 @@ function* run(config, provide, channels) { ReactGA.set({ page: action.payload.pathname}); ReactGA.pageview(action.payload.pathname); }) + } } export {run}; \ No newline at end of file diff --git a/views/components/elements/forms/service-template-form-refactored.jsx b/views/components/elements/forms/service-template-form-refactored.jsx index 5fdf1fed..62cee7a6 100644 --- a/views/components/elements/forms/service-template-form-refactored.jsx +++ b/views/components/elements/forms/service-template-form-refactored.jsx @@ -1,39 +1,19 @@ import React from 'react'; -import {Link, browserHistory} from 'react-router'; +import {browserHistory, Link} from 'react-router'; import 'react-tagsinput/react-tagsinput.css'; import './css/template-create.css'; -import { - Field, - Fields, - FormSection, - FieldArray, - reduxForm, - formValueSelector, - change, - unregisterField, - getFormValues, - Form -} from 'redux-form' +import consume from "pluginbot-react/dist/consume" +import {change, Field, FieldArray, FormSection, formValueSelector, getFormValues} from 'redux-form' import {connect} from "react-redux"; -import {RenderWidget, WidgetList, PriceBreakdown, widgets} from "../../utilities/widgets"; -import {WysiwygRedux} from "../../elements/wysiwyg.jsx"; -import FileUploadForm from "./file-upload-form.jsx"; -import { - inputField, - selectField, - OnOffToggleField, - iconToggleField, - priceField, - priceToCents -} from "./servicebot-base-field.jsx"; +import {RenderWidget, WidgetList, widgets} from "../../utilities/widgets"; +import {iconToggleField, inputField, priceField, priceToCents} from "./servicebot-base-field.jsx"; import {addAlert, dismissAlert} from "../../utilities/actions"; import ServiceBotBaseForm from "./servicebot-base-form.jsx"; -import SVGIcons from "../../utilities/svg-icons.jsx"; import Load from "../../utilities/load.jsx"; - -let _ = require("lodash"); -import {required, email, numericality, length} from 'redux-form-validators' +import {numericality, required} from 'redux-form-validators' import slug from "slug" +import {TierBillingForm} from "./tier-billing-form.jsx" +let _ = require("lodash"); const TEMPLATE_FORM_NAME = "serviceTemplateForm" const selector = formValueSelector(TEMPLATE_FORM_NAME); // <-- same as form name @@ -336,154 +316,17 @@ class renderCustomProperty extends React.Component { //The full form -let PaymentStructureTemplates = function (props) { - let {fields, tier, member, setPricingTemplates} = props; - let plans = fields.getAll() || []; - - function addPayment(e) { - e.preventDefault(); - fields.push({ - interval_count: 1, - trial_period_days: tier.trial_period_days || 0, - type: tier.type, - // statement_descriptor: this.props.company_name.value.substring(0, 22), - amount: 0 - - }); - }; - - function deletePlan(index) { - return (e) => { - e.preventDefault(); - fields.remove(index) - } - } - - - let availableOptions = [ - {id: "month", name: "Month"}, - {id: "year", name: "Year"}, - {id: "day", name: "Day"}, - {id: "week", name: "Week"} - ]; - let optionMap = plans.map(plan => { - return availableOptions.filter(option => option.id === plan.interval || !plans.some(plan2 => plan2.interval === option.id)); - }); - // ].filter(option => { - // return !plans.some(plan => plan.interval === option.id); - // }); - if (tier.type === "custom") { - return (
-

Quotes are built for services that are customer specific. If your service is - priced - based on the customer's use-case, use this option. Once the quote service has - been - requested by the customer, you can add charges to the service at anytime. -

-
); - - } - if (tier.type === "one_time") { - let field = fields[0]; - return (=': 0.00})}/>); - - } - if (tier.type === "subscription") { - return (
- {fields.map((field, index) => { - return (
- - - =': 0.00})}/> - {fields.length > 1 && } -
) - })} - - {plans.length < 4 && } -
) - } else { - return
- } -} - -let TierBillingForm = function (props) { - let {tier, member, setPricingTemplates, serviceTypeValue} = props; - const changeServiceType = (event, newValue) => { - let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; - setPricingTemplates(member, pricingStructures.map(p => { - return { - ...p, - type: newValue - } - })); - }; - const changeTrial = (event, newValue) => { - let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; - setPricingTemplates(member, pricingStructures.map(p => { - return { - ...p, - trial_period_days: newValue - } - })); - }; - const formatFromPricing = (value, name) => { - console.log("VAL", value); - let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; - if((value === null || value === undefined) && pricingStructures.length > 0){ - return pricingStructures[0][name]; - } - return value; - }; - return (
- - - - - {tier.type === "subscription" && - } - - - - - -
) -} let Tiers = function (props) { - let {member, fields, selected, selectTier, overrideBilling, setPricingTemplate} = props; + let {member, fields, selected, selectTier, services: {tierBillingOverride}, setPricingTemplate} = props; selectTier = selectTier || (() => { }); let current = fields.get(selected); - + let OverrideBilling = tierBillingOverride && tierBillingOverride[0]; function deleteTier(index) { return (e) => { e.preventDefault(); @@ -535,12 +378,13 @@ let Tiers = function (props) { - + {(OverrideBilling && ) || }
) }; Tiers = connect((state, ownProps) => { return { + // "privateValue": selector(state, "references.service_template_properties")[ownProps.index].private, // "requiredValue": selector(state, "references.service_template_properties")[ownProps.index].required, // "promptValue": selector(state, "references.service_template_properties")[ownProps.index].prompt_user, @@ -600,7 +444,7 @@ Tiers = connect((state, ownProps) => { // } } })(Tiers); - +Tiers = consume("tierBillingOverride")(Tiers) class TemplateForm extends React.Component { @@ -986,5 +830,4 @@ const mapDispatchToProps = (dispatch) => { } }; - -export default connect(mapStateToProps, mapDispatchToProps)(ServiceTemplateForm); \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(ServiceTemplateForm); diff --git a/views/components/elements/forms/tier-billing-form.jsx b/views/components/elements/forms/tier-billing-form.jsx new file mode 100644 index 00000000..b7636dec --- /dev/null +++ b/views/components/elements/forms/tier-billing-form.jsx @@ -0,0 +1,144 @@ +import {inputField, priceField, selectField} from "./servicebot-base-field.jsx"; +import {numericality, required} from "redux-form-validators"; +import React from 'react'; +import {change, Field, FieldArray, FormSection, formValueSelector, getFormValues} from 'redux-form' +let PaymentStructureTemplates = function (props) { + let {fields, tier, member, setPricingTemplates} = props; + let plans = fields.getAll() || []; + + function addPayment(e) { + e.preventDefault(); + fields.push({ + interval_count: 1, + trial_period_days: tier.trial_period_days || 0, + type: tier.type, + // statement_descriptor: this.props.company_name.value.substring(0, 22), + amount: 0 + + }); + }; + + function deletePlan(index) { + return (e) => { + e.preventDefault(); + fields.remove(index) + } + } + + + let availableOptions = [ + {id: "month", name: "Month"}, + {id: "year", name: "Year"}, + {id: "day", name: "Day"}, + {id: "week", name: "Week"} + ]; + let optionMap = plans.map(plan => { + return availableOptions.filter(option => option.id === plan.interval || !plans.some(plan2 => plan2.interval === option.id)); + }); + // ].filter(option => { + // return !plans.some(plan => plan.interval === option.id); + // }); + if (tier.type === "custom") { + return (
+

Quotes are built for services that are customer specific. If your service is + priced + based on the customer's use-case, use this option. Once the quote service has + been + requested by the customer, you can add charges to the service at anytime. +

+
); + + } + if (tier.type === "one_time") { + let field = fields[0]; + return (=': 0.00})}/>); + + } + if (tier.type === "subscription") { + return (
+ {fields.map((field, index) => { + return (
+ + + =': 0.00})}/> + {fields.length > 1 && } +
) + })} + + {plans.length < 4 && } +
) + } else { + return
+ } +} +let TierBillingForm = function (props) { + let {tier, member, setPricingTemplates, serviceTypeValue} = props; + const changeServiceType = (event, newValue) => { + let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; + setPricingTemplates(member, pricingStructures.map(p => { + return { + ...p, + type: newValue + } + })); + }; + const changeTrial = (event, newValue) => { + let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; + setPricingTemplates(member, pricingStructures.map(p => { + return { + ...p, + trial_period_days: newValue + } + })); + }; + + const formatFromPricing = (value, name) => { + console.log("VAL", value); + let pricingStructures = (tier.references && tier.references.payment_structure_templates) || []; + if ((value === null || value === undefined) && pricingStructures.length > 0) { + return pricingStructures[0][name]; + } + return value; + }; + + + return (
+ + + + + {tier.type === "subscription" && + } + + + + + +
) +} +export {TierBillingForm}; From 7bfeff4d9df677cb55747671c536f12b4cfbbee8 Mon Sep 17 00:00:00 2001 From: kalungsy Date: Wed, 23 May 2018 19:19:53 -0400 Subject: [PATCH 008/865] adds new nav icons --- public/assets/SB_Icon_Menu/Home.png | Bin 0 -> 3309 bytes public/assets/SB_Icon_Menu/Integrations.png | Bin 0 -> 2325 bytes public/assets/SB_Icon_Menu/Manage.png | Bin 0 -> 2726 bytes public/assets/SB_Icon_Menu/Settings.png | Bin 0 -> 8561 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/assets/SB_Icon_Menu/Home.png create mode 100644 public/assets/SB_Icon_Menu/Integrations.png create mode 100644 public/assets/SB_Icon_Menu/Manage.png create mode 100644 public/assets/SB_Icon_Menu/Settings.png diff --git a/public/assets/SB_Icon_Menu/Home.png b/public/assets/SB_Icon_Menu/Home.png new file mode 100644 index 0000000000000000000000000000000000000000..01c6db00fa2d8a7f537f3feaaf10041af5f8a389 GIT binary patch literal 3309 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 literal 0 HcmV?d00001 diff --git a/public/assets/SB_Icon_Menu/Integrations.png b/public/assets/SB_Icon_Menu/Integrations.png new file mode 100644 index 0000000000000000000000000000000000000000..b40193688f179ca6c8c86964dd5713ac3fc0cba4 GIT binary patch literal 2325 zcmeH}Sy+=-8ppqcK)%RgK#-C33$hj~2;(3DX%Ywt#X%8_AOZ$MQIr5-iO6CS5y8PE zs1;=qsK}PGhz$f4MMQ~|MJWlQl&~p;WegTEEJ8CkbL~y%YA(+Co%dXv_j&%$`M=LU z=YStcdy~;7006Xo$X*9k6#r*|8mhfEsP2{uu!Mso0#Mg$A_oAl&&LZNcm_6K{3Ln@ zQ9tX$w3Y4i3n5S3S?p=QVU5eQSv3)#H9VgV<_D+bkhglAFFe*UY7D;md3f5LqhW~O zvN6u>rjct>@7MSZ%2$joxj9RIpBiA!6c)%6>9^de=K%x+z(5=TN3*`_$PR?x8`$!* z`8g)DSun^Q&>2V`Xp+3n+9HycB?uFQzYU#E-n|xR(qhLx%^~N0tNtA&wPLBOBB$Y; z1HAdotk-;tSBpFe;bx^Pwh$Az&XJ8J*R+wJt>Vf|H|Lc*)QzpaJ6pNg9?8(Ee_BD( zkg{DvsPGr|;&HdmJ0H8PLdG!r-!G0$NX=wET~g=Ha-DE-LQ&-$xstim=H3uxPlh@> zbPfw%KgR&8?ZXLa9V@=B47=vIGBejNLXU{nWwNVxEFsnq!58F@44?hb4x`V+vHPM0nRVMOz+=STtqyh$LO8G zE;WQa=GLbUDP8+tUv<dVvrbp%aS6 zq2B`t3k3Rq)5d#44VyPyBkz}qP(>qE))>8A+gT8=^t!%z)vTtFCmEp}7aC+r|A7b= zh&s-MI;7Vw4IMGuZ>$o~o|tb^FPHP8&Fda^Kg~iZ?v7=8W6<+3V@JT0za1QGv|N4l5yOKN zR{+kAsCKtsFS>Kud{y^FD6?~D<&;yp*^er%tg7A=|FQYOff#QBT9&ji!d&?jRgxz8 zfsmiFaj3<;mAZC2O(uyPke{uLwU`{a$ z+C=vq5-5I+DmLaMTFkUQJ%-=PD6;F%_;6zG24R$dG+)ZgWY*|+FLSk3E}fhhN~&nX zI(`@q!eezmjZj~1YE<^dSKB@e{^VU8*{O&>RX^*7BED$H$}Pt#JNy7rb@xe0Rd&MrAx{Uhc-viv3aPlk1TOwf=iHCELbc>=`&0 z7BK&lj%7VWwfz^0QFYN_pq<36j-}>OCHt-%pW#OM?qn?ot6og;r1ubA+YQi2P&>gY!;$arR z`5VqUGaL?%Hz(8RH~_B&ZE1IaAr?a1oLp32S%Z%n%Lh2hTGN}xD&wn(GZnr7eoe~l z)&`+$7>%H`VnG?FNbfaNF9J@OH9Z{qv2c5W8XSxOZlW_#P`2z%UJDF{*u~lh{=e;C zBeMIaLhMZ>ocRXw=c_qklW>?fR! zMj04i(&m$ILS<6;t1rD%Dh|rR(V`SDtb(F`D6+2kQvga_bUU# ef5SzzwZc3gdofKResWTka{wQrpI05>80Syzhro#d literal 0 HcmV?d00001 diff --git a/public/assets/SB_Icon_Menu/Manage.png b/public/assets/SB_Icon_Menu/Manage.png new file mode 100644 index 0000000000000000000000000000000000000000..a7681d7a99c3eeeef9204771ad6efbfd6aa0fc69 GIT binary patch literal 2726 zcma);`#%$W8^>pcHN&vvnB`D&$|RATX=@ZlIUklo%Aw};Fd~}}i7+>n!&q|497@xo z#%_la)pDqqd&-#*qM>@$^LqY(`-kuMy1qYL*ALhG^ZH(&OlL=o1Vj-6001Pgr|et= zvG30Tiwf=r4|pl|F-uBz1$FHNFDPEmw9e0toEqk@bLHb(rB|(?rC2i zf)xM=O96lo03qR%7U~-Q>i7qL9JO3 zG}YR{KdEfJ4cjUn^Bqe=(5+epp+T|_tg8T!bSew?QHl_ZUWs@G*19~o|4QM+35 zYje}ZM%2@nzy4OLZKZ!CdWH6S?A@~w+U7Uwi?e5rXrQt>(ViI(o@}&|ST*U2vA#<8 zXE-uI926d0s`=^tCWus%p2=L6b>j_L2tlJ9ge7)AFg+H`f&I#?csC7FzUd0aTN8KW=N^}y$4LS7yQUu)Fwgx3WEBc2#pdR` zq6*bPhp=&TOdJ?+8T8@h^qdb8&VrC6ILh2NV67CtQ`mpvC#QL1#)B8#zC6CM^3B`B zZ^$FetsFSjO!2SURqtk7qgp8SW2drtTF968UeaD$yeh1ERDi~I2=;2x62e!naO zGZ9VV_dJDt3QMJ_K`^Lc_o&nq61g(cSjn^ZTy@p@Z3G2(BH#8qxQbb0%aXiGGoWjU zGy*JpVPQm-8tI%||J=qCzikuPpNc>0KAa3NaK3qsc3)q+F|n`9La8bhKUBy$xzc0J z_hHU8jj2+|mFA!p@Tt%I8-*vc(A8l17tyCx6S6D=8i->2ouMwBl0fw$h_iR>aS_{| zE6tI(q?K1dzR?w(xLC7gc=kMon2NdQzkcvmP~>h!Q{W>nN3J#Mf$pc>iq*r2@z8+z z7RlZycs3k1=6z<_rfVQeMUk;dDHWj&3}o%x76CJ!tqAYg9`{5AnOgo5M?V|mN+t2& zZ-*z%hz$+^t~e3+5av-m@Qz3$0LH;@=cniDbfH#-q+<(`DmtlRgkV-76T`5_7coVC z$SC-KpBwU!0$tQ)b*;{|03Mt(B0pC9P4J0{?_+ayK%we;#>WW$=Gx>2w0q2flA8p6 zSawS6QBs%+eIVhav&sfxXmak!K9JubiSd!h#Z7I{n*vEkVo7>Ln$S#DTEAIQQ>*-E zw*-^xx2z_*Xo?yO%yl2@dUb(zWrIsGK-UUe{8`~!%)=_f^Hs$xWV&LoN;fcyBti0% z7+GwOI2#O)1$M~Oi->jNb{Q7ncq8g4k?%Z^EO%n(K+>&ZsFq=Zo@NsESg$!LjJSP) zF_zTc_}SAy$l!ya)=JgY{IzqhjJ1qiI9pp5`pZr4M2?Kgo!I}fX*q{}?66G2n<|Y) z%{Q7WnoKT-!^@K6o2=r0{TMlv7#9!~HS} zc8TjHHRRg14yvrl%{V<^lq

>k87XDEvo`zV-03E-*j2&~4qyS%pb3oB1)IhM3=a zEhh$Sq(K?Mm;LpC`M)0@c#-#I1&6MXoZ>J!t+lt!GsmebHwp#ty+kZXl-Qb_OP_)d}?idal zsEnd#1{5O~U%zXAd}b}QV5gaLBD2ST*kPa9X!0LK^y3n(i}}U{n6Jz1yAvlLU=((% zC>oVTG-KN;@3;y3r+I7B^!u~5^j{6q1-F${gX!)r?R?&JPw>3&GW>1f1Z4hF4uJUv zcN{0yBufxAO&e6|2Jz4-j`|q);PZew=wn>moQHy0ijFXS$eei&_-sgzRn+&R4pZHe zfUpV_l1s-FiJ%(lr0?(LXMCw-+wNcXJK(H@C_kDE^*#`B%MA@>%iFrx;f@qjLoE)iqgRQuUE&mo?pv|Vj zvf1*wFX(?nbuWn{uI6rl1O*-kM~8ek)WKrRoY|XdgPK{o_M9$I(vt zC=FjC)#)O>iQRYPe{<8jPp~uQHlkwA$%#^5St-S4

%aw;kcxb7|h1O%7b()y(Y1gqYBW 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 literal 0 HcmV?d00001 From 77987bbf2afaefd75a6c91654ead31ec009df2dd Mon Sep 17 00:00:00 2001 From: bsears90 Date: Wed, 23 May 2018 19:57:52 -0400 Subject: [PATCH 009/865] null statement descriptor works now --- models/service-instance.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/models/service-instance.js b/models/service-instance.js index c48e1779..c0491e25 100644 --- a/models/service-instance.js +++ b/models/service-instance.js @@ -58,8 +58,6 @@ let buildPayStructure = function (payment_object, callback) { plan.amount = 0; } plan.statement_descriptor = (plan.statement_descriptor || "Subscription").substring(0, 22); - console.log(plan); - callback(plan); }; From 8d263c9cd3598b9206ca6cb634ac8c1ac311edc5 Mon Sep 17 00:00:00 2001 From: kalungsy Date: Thu, 24 May 2018 19:34:48 -0400 Subject: [PATCH 010/865] adds new app-dashboard layout, uses new nav-servicebot component --- views/components/app.jsx | 45 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/views/components/app.jsx b/views/components/app.jsx index 84cc885a..e340c2cc 100644 --- a/views/components/app.jsx +++ b/views/components/app.jsx @@ -1,6 +1,7 @@ import React from 'react'; import Fetcher from "./utilities/fetcher.jsx" -import NavBootstrap from "./layouts/nav-bootstrap.jsx" +import NavServiceBot from "./layouts/nav-servicebot.jsx" +import NavBoostrap from "./layouts/nav-bootstrap.jsx" import Footer from "./layouts/footer.jsx" import {browserHistory} from 'react-router'; import {connect} from "react-redux"; @@ -13,6 +14,8 @@ class App extends React.Component { super(props); this.state = {backgroundColor: '#000000'}; this.handleLogout = this.handleLogout.bind(this); + + this.isDashboardView = this.isDashboardView.bind(this); } componentDidMount(){ @@ -28,6 +31,18 @@ class App extends React.Component { }) } + isDashboardView() { + console.log("pros", this.props); + let location = this.props.location.pathname; + let publicViewPaths = ['^\/{1}$', '^\/?service-catalog\/+\\d\/request+$', '^\/?login+$']; + if (matchInArray(location, publicViewPaths)) { + console.log("matched"); + return false; + } + return true; + } + + render () { let self = this; let background = (this.props.options && this.props.options.background_color) ? this.props.options.background_color.value : '#ff0400'; @@ -35,12 +50,25 @@ class App extends React.Component { document.getElementById('servicebot-loader').classList.add('move-out'); } + this.isDashboardView(); return(

{this.props.modal && this.props.modal} - - {self.props.children} + {this.isDashboardView() ? +
+ +
+ {self.props.children} +
+
: +
+ +
+ {self.props.children} +
+
+ }
); @@ -64,3 +92,14 @@ let mapDispatchToProps = function(dispatch){ } export default connect(mapStateToProps, mapDispatchToProps)(App); +function matchInArray(string, expressions) { + + for (let i = 0; i < expressions.length; i++) { + if (string.match(expressions[i])) { + return true; + } + } + + return false; + +}; \ No newline at end of file From 30c1dd749d0e405f81ca366f131721dc4fc76d11 Mon Sep 17 00:00:00 2001 From: kalungsy Date: Thu, 24 May 2018 19:35:38 -0400 Subject: [PATCH 011/865] adds css class for new layout design --- views/components/pages/notifications.jsx | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/views/components/pages/notifications.jsx b/views/components/pages/notifications.jsx index 2607d35e..fdf6b9b4 100644 --- a/views/components/pages/notifications.jsx +++ b/views/components/pages/notifications.jsx @@ -110,7 +110,7 @@ class NavNotification extends React.Component{ } return ( -
+
{totalUnread ?
  • {`You have ${totalUnread} unread notifications`}
  • : }