import faker from 'faker'; import moment from 'moment-timezone'; import nql from '@tryghost/nql'; import {Response} from 'miragejs'; import { extractFilterParam, paginateModelCollection, withPermissionsCheck } from '../utils'; import {underscore} from '@ember/string'; const ALLOWED_ROLES = [ 'Owner', 'Administrator' ]; export function mockMembersStats(server) { server.get('/members/stats/count', withPermissionsCheck(ALLOWED_ROLES, function (db, {queryParams}) { let {days} = queryParams; let firstSubscriberDays = faker.datatype.number({min: 30, max: 600}); if (days === 'all-time') { days = firstSubscriberDays; } else { days = Number(days); } let total = 0; if (firstSubscriberDays > days) { total += faker.datatype.number({max: 1000}); } // simulate sql GROUP BY where days with 0 subscribers are missing let dateCounts = {}; let i = 0; while (i < days) { let date = moment().subtract(i, 'days').format('YYYY-MM-DD'); let count = faker.datatype.number({min: 0, max: 30}); if (count !== 0) { dateCounts[date] = count; } i += 1; } // similar to what we'll need to do on the server let totalOnDate = {}; let j = days - 1; while (j >= 0) { let date = moment().subtract(j, 'days').format('YYYY-MM-DD'); totalOnDate[date] = total + (dateCounts[date] || 0); total += (dateCounts[date] || 0); j -= 1; } return { total, resource: 'members', data: Object.keys(totalOnDate).map((key, idx, arr) => { return { date: key, free: arr[key], paid: 0, comped: 0 }; }) }; })); } export default function mockMembers(server) { server.post('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}) { const attrs = this.normalizedRequestAttrs(); return members.create(attrs); })); server.get('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {queryParams}) { let {filter, search, page, limit} = queryParams; page = +page || 1; limit = +limit || 15; let collection = members.all(); if (filter) { try { const nqlFilter = nql(filter, { expansions: [ { key: 'label', replacement: 'labels.slug' }, { key: 'tier', replacement: 'tiers.slug' } ] }); collection = collection.filter((member) => { const serializedMember = {}; // mirage model keys match our main model keys so we need to transform // camelCase to underscore to match the filter format Object.keys(member.attrs).forEach((key) => { serializedMember[underscore(key)] = member.attrs[key]; }); // similar deal for associated models ['labels', 'tiers', 'subscriptions', 'newsletters'].forEach((association) => { serializedMember[association] = []; member[association].models.forEach((associatedModel) => { const serializedAssociation = {}; Object.keys(associatedModel.attrs).forEach((key) => { serializedAssociation[underscore(key)] = associatedModel.attrs[key]; }); serializedMember[association].push(serializedAssociation); }); }); return nqlFilter.queryJSON(serializedMember); }); } catch (err) { console.error(err); // eslint-disable-line throw err; } } if (search) { const query = search.toLowerCase(); collection = collection.filter((member) => { return member.name.toLowerCase().indexOf(query) !== -1 || member.email.toLowerCase().indexOf(query) !== -1; }); } return paginateModelCollection('members', collection, page, limit); })); server.del('/members/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {queryParams}) { if (!queryParams.filter && !queryParams.search && queryParams.all !== 'true') { return new Response(422, {}, {errors: [{ type: 'IncorrectUsageError', message: 'DELETE /members/ must be used with a filter, search, or all=true query parameter' }]}); } let membersToDelete = members.all(); if (queryParams.filter) { let labelFilter = extractFilterParam('label', queryParams.filter); membersToDelete = membersToDelete.filter((member) => { let matches = false; labelFilter.forEach((slug) => { if (member.labels.models.find(l => l.slug === slug)) { matches = true; } }); return matches; }); } let count = membersToDelete.length; membersToDelete.destroy(); return { meta: { stats: { successful: count } } }; })); server.get('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, {params}) { let {id} = params; let member = members.find(id); return member || new Response(404, {}, { errors: [{ type: 'NotFoundError', message: 'Member not found.' }] }); })); server.put('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members, tiers, subscriptions}, {params}) { const attrs = this.normalizedRequestAttrs(); const member = members.find(params.id); // API accepts `tiers: [{id: 'x'}]` which isn't handled natively by mirage if (attrs.tiers.length > 0) { attrs.tiers.forEach((p) => { const tier = tiers.find(p.id); if (!member.tiers.includes(tier)) { // TODO: serialize tiers through _active_ subscriptions member.tiers.add(tier); subscriptions.create({ member, tier, comped: true, plan: { id: '', nickname: 'Complimentary', interval: 'year', currency: 'USD', amount: 0 }, status: 'active', startDate: moment().toISOString(), defaultPaymentCardLast4: '****', cancelAtPeriodEnd: false, cancellationReason: null, currentPeriodEnd: moment().add(1, 'year').toISOString(), price: { id: '', price_id: '', nickname: 'Complimentary', amount: 0, interval: 'year', type: 'recurring', currency: 'USD', tier: { id: '', tier_id: tier.id } }, offer: null }); member.save(); } }); } const tierIds = (attrs.tiers || []).map(p => p.id); member.tiers.models.forEach((tier) => { if (!tierIds.includes(tier.id)) { member.subscriptions.models.filter(sub => sub.tier.id === tier.id).forEach((sub) => { member.subscriptions.remove(sub); }); member.tiers.remove(tier); } }); // these are read-only properties so make sure we don't overwrite data delete attrs.tiers; delete attrs.subscriptions; return member.update(attrs); })); server.del('/members/:id/', withPermissionsCheck(ALLOWED_ROLES, function ({members}, request) { const id = request.params.id; members.find(id).destroy(); })); server.get('/members/upload/', withPermissionsCheck(ALLOWED_ROLES, function () { return new Response(200, { 'Content-Disposition': 'attachment', filename: `members.${moment().format('YYYY-MM-DD')}.csv`, 'Content-Type': 'text/csv' }, ''); })); server.post('/members/upload/', withPermissionsCheck(ALLOWED_ROLES, function ({labels}, request) { const label = labels.create(); // TODO: parse CSV and create member records for (const kvPair of request.requestBody.entries()) { const [key, value] = kvPair; console.log({key, value}); // eslint-disable-line } return new Response(201, {}, { meta: { import_label: label, stats: {imported: 1, invalid: []} } }); })); server.get('/members/events/', withPermissionsCheck(ALLOWED_ROLES, function ({memberActivityEvents}, {queryParams}) { let {limit, filter, page} = queryParams; limit = +limit || 15; page = +page || 1; let collection = memberActivityEvents.all(); collection = collection.sort((a, b) => { return Number(b.id) - Number(a.id); }); if (filter) { try { const nqlFilter = nql(filter, { expansions: [ { key: 'data.created_at', replacement: 'created_at' } ] }); collection = collection.filter((event) => { const serializedEvent = {}; // mirage model keys match our main model keys, so we need to transform // camelCase to underscore to match the filter format Object.keys(event.attrs).forEach((key) => { serializedEvent[underscore(key)] = event.attrs[key]; }); return nqlFilter.queryJSON(serializedEvent); }); } catch (err) { console.error(err); // eslint-disable-line throw err; } } return paginateModelCollection('members', collection, page, limit); })); mockMembersStats(server); }