src/actions/plan/update.js
const { ActionTransport } = require('@microfleet/core');
const Promise = require('bluebird');
const Errors = require('common-errors');
const set = require('lodash/set');
const assign = require('lodash/assign');
const mergeWith = require('lodash/mergeWith');
const findIndex = require('lodash/findIndex');
const get = require('get-value');
const each = require('lodash/each');
// helpers
const {
PLANS_DATA, PLANS_INDEX, FREE_PLAN_ID, PLAN_ALIAS_FIELD,
} = require('../../constants');
const { serialize, deserialize } = require('../../utils/redis');
const { merger } = require('../../utils/plans');
const { cleanupCache } = require('../../list-utils');
const key = require('../../redis-key');
// constants
const DATA_HOLDERS = {
monthly: 'month',
yearly: 'year',
};
const FIELDS_TO_UPDATE = [PLAN_ALIAS_FIELD, 'title', 'hidden', 'meta', 'level'];
function joinPlans(plans) {
const plan = mergeWith({}, ...plans, merger);
return { plan, plans };
}
function prepareUpdate(subscription, plans, period) {
const index = findIndex(plans, (it) => get(it, ['plan', 'payment_definitions', '0', 'frequency'], '').toLowerCase() === period);
const planData = plans[index];
if (subscription.models) {
set(planData, ['subs', 0, 'models'], subscription.models);
}
if (subscription.modelPrice) {
set(planData, ['subs', 0, 'price'], subscription.modelPrice);
}
}
function updateSubscriptions(plans, subscriptions) {
each(DATA_HOLDERS, (period, containerKey) => {
const subscription = subscriptions[containerKey];
if (subscription) {
prepareUpdate(subscription, plans, period);
}
});
}
function setField(_plans, path, value) {
const plans = Array.isArray(_plans) ? _plans : [_plans];
return plans.forEach((plan) => set(plan, path, value));
}
function createSaveToRedis(message) {
const { redis } = this;
return Promise
.bind(this, message.id.split('|'))
.map((id) => redis.hgetall(key(PLANS_DATA, id)).then(deserialize))
.then(function updatePlansInRedis(plans) {
const additionalData = {};
if ('subscriptions' in message) {
updateSubscriptions(plans, message.subscriptions);
}
if ('description' in message) {
setField(plans, 'plan.description', message.description);
}
FIELDS_TO_UPDATE.forEach((field) => {
if (field in message) {
additionalData[field] = message[field];
}
});
return { plans, additionalData };
});
}
function saveToRedis({ plans, additionalData }) {
const { redis } = this;
const data = joinPlans(plans);
const currentAlias = data.plan.alias;
const saveDataFull = assign(data.plan, additionalData);
const aliasedId = saveDataFull.alias;
const pipeline = redis.pipeline();
const planKey = key(PLANS_DATA, aliasedId);
// if we are changing alias - that requires checking if new alias already exists
if (aliasedId !== currentAlias) {
pipeline.srem(PLANS_INDEX, currentAlias);
pipeline.rename(key(PLANS_DATA, currentAlias), planKey);
}
const serializedData = serialize(saveDataFull);
pipeline.sadd(PLANS_INDEX, aliasedId);
pipeline.hmset(planKey, serializedData);
if (saveDataFull.id !== aliasedId) {
pipeline.hmset(key(PLANS_DATA, saveDataFull.id), serializedData);
}
// free plan id contains only 1 plan and it has same id as alias
if (aliasedId !== FREE_PLAN_ID) {
plans.forEach((planData) => {
const saveData = assign(planData, additionalData);
pipeline.hmset(key(PLANS_DATA, planData.id), serialize(saveData));
});
}
return pipeline.exec().return(saveDataFull);
}
/**
* @api {amqp} <prefix>.plan.state Update plan
* @apiVersion 1.0.0
* @apiName planUpdate
* @apiGroup Plan
*
* @apiDescription Update paypal plan with a special case for a free plan
* **WARNING**: this method is prone to race conditions, and, therefore, requires a lock to be
* used before updating data
*
* @apiSchema {jsonschema=plan/update.json} apiRequest
* @apiSchema {jsonschema=response/plan/update.json} apiResponse
*/
/**
* TODO: add lock when updating aliases
*
* @param {Object} message
* @return {Promise}
*/
function planUpdate({ params }) {
const { redis } = this;
const { alias, id } = params;
if (id !== FREE_PLAN_ID && id.indexOf('|') === -1) {
return Promise.reject(new Errors.HttpStatusError(400, `invalid plan id: ${id}`));
}
// message.alias can never be equal to FREE_PLAN_ID, because it's check in json-schema
// therefore we only need to check if message.alias already exists
return Promise
.bind(this, params)
.tap(() => {
if (!alias) {
return null;
}
return redis.sismember(PLANS_INDEX, alias).then((isMember) => {
if (isMember) {
throw new Errors.HttpStatusError(409, `alias ${alias} already exists`);
}
return null;
});
})
.tap(() => {
return redis.exists(key(PLANS_DATA, id)).then((exists) => {
if (!exists) {
throw new Errors.HttpStatusError(404, `plan ${id} does not exist`);
}
return null;
});
})
.then(createSaveToRedis)
.then(saveToRedis)
.tap(() => cleanupCache.call(this, PLANS_INDEX));
}
planUpdate.transports = [ActionTransport.amqp];
module.exports = planUpdate;