lib/recurly/pricing/checkout/attachment.js
import Emitter from 'component-emitter';
import Promise from 'promise';
import SubscriptionPricing from '../subscription';
import dom from '../../../util/dom';
import flatten from '../../../util/flatten';
import groupBy from '../../../util/group-by';
import { ignoreNotFound } from '../subscription/attachment';
import uid from '../../../util/uid';
const debug = require('debug')('recurly:pricing:checkout:attachment');
const INIT_RUN = 'init-all';
/**
* Binds a DOM tree to CheckoutPricing values
*
* @param {CheckoutPricing} pricing
* @param {HTMLElement} container Element on which to attach
*/
export default class Attachment extends Emitter {
constructor (pricing, container) {
super();
this.pricing = pricing;
this.recurly = pricing.recurly;
this.container = dom.element(container);
if (!this.container) throw new Error('invalid dom element');
this.onInputChange = this.onInputChange.bind(this);
this.updateOutput = this.updateOutput.bind(this);
this.pricing.on('change', this.updateOutput);
this.elements.all.forEach(elem => {
elem.addEventListener('change', this.onInputChange);
elem.addEventListener('propertychange', this.onInputChange);
});
this.onInputChange(INIT_RUN);
}
get elements () {
if (this._elements) return this._elements;
let elements = { all: [].slice.call(this.container.querySelectorAll('[data-recurly]')) };
elements.all.forEach(node => {
const name = dom.data(node, 'recurly');
if (!(name in elements)) elements[name] = [];
elements[name].push(node);
});
this._elements = elements;
return elements;
}
/**
* Handles input element changes
*/
onInputChange (event) {
debug('onInputChange');
const elems = this.elements;
// Collect subscription properties that are not given a subscription id,
// and assign them one, assuming they are all meant to apply to one
// subscription
const orphanedSubscriptionId = uid();
flatten([elems.plan, elems.plan_quantity, elems.addon, elems.tax_code]).forEach(el => {
if (!el) return;
if (dom.data(el, 'recurlySubscription')) return;
dom.data(el, 'recurlySubscription', orphanedSubscriptionId);
});
// Collect subscription elements, grouped by their ids,
// then removes elements not paired to a subscription
const subscriptionGroups = groupBy(elems.all, el => dom.data(el, 'recurlySubscription'));
delete subscriptionGroups.undefined;
// Remove abandoned subscriptions
this.pricing.items.subscriptions.forEach(sub => {
if (!subscriptionGroups[sub.id]) this.pricing.remove({ subscription: sub });
});
// Remove abandoned adjustments
const adjustmentElemIds = (elems.adjustment || []).map(el => dom.data(el, 'recurlyAdjustment'));
this.pricing.items.adjustments.forEach(adj => {
if (!~adjustmentElemIds.indexOf(adj.id)) this.pricing.remove({ adjustment: adj });
});
// Builds a SubscriptionPricing instance per subscription element group
Promise.all(Object.keys(subscriptionGroups).map(id => {
const subscriptionElems = groupBy(subscriptionGroups[id], el => dom.data(el, 'recurly'));
// Look for an existing subscription for this code
let subscription = this.pricing.findSubscriptionById(id);
if (!subscription) {
subscription = new SubscriptionPricing(this.recurly, { id });
this.pricing.subscription(subscription);
}
const quantity = dom.value(subscriptionElems.plan_quantity) || 1;
return subscription
// Plan
.plan(dom.value(subscriptionElems.plan), { quantity })
.then(() => {
// Add-ons
if (!subscriptionElems.addon) return;
return Promise.all(subscriptionElems.addon.map(addonElem => {
const code = dom.data(addonElem, 'recurlyAddon');
const addonQuantity = dom.value(addonElem);
return subscription
.addon(code, { quantity: addonQuantity })
.catch(e => this.pricing.error(e));
}));
})
.then(() => {
// Tax code
if (!subscriptionElems.tax_code) return;
return subscription.tax({ tax_code: dom.value(subscriptionElems.tax_code) });
})
.reprice();
}))
.then(() => {
// Adjustments
if (!elems.adjustment) return;
return Promise.all(elems.adjustment.map(adjustmentElem => {
const id = dom.data(adjustmentElem, 'recurlyAdjustment');
const itemCode = dom.data(adjustmentElem, 'recurlyAdjustmentItemCode');
const amount = dom.data(adjustmentElem, 'recurlyAdjustmentAmount');
const quantity = dom.value(adjustmentElem) || 0;
const currency = dom.data(adjustmentElem, 'recurlyAdjustmentCurrency');
const taxCode = dom.data(adjustmentElem, 'recurlyAdjustmentTaxCode');
const taxExempt = dom.data(adjustmentElem, 'recurlyAdjustmentTaxExempt');
return this.pricing.adjustment({
id, itemCode, amount, quantity, currency, taxCode, taxExempt
});
}));
})
.then(() => {
// Currency
if (!elems.currency) return;
return this.pricing.currency(dom.value(elems.currency));
})
.then(() => {
// Coupon
if (!elems.coupon) return;
return this.pricing
.coupon(dom.value(elems.coupon).trim())
.then(null, ignoreNotFound);
})
.then(() => {
// Gift card
if (!elems.gift_card) return;
return this.pricing
.giftCard(dom.value(elems.gift_card).trim())
.then(null, ignoreNotFound);
})
.then(() => {
// Address
if (elems.country || elems.postal_code) {
return this.pricing.address({
country: dom.value(elems.country),
postal_code: dom.value(elems.postal_code)
});
}
})
.then(() => {
// Shipping Address
if (elems['shipping_address.country'] || elems['shipping_address.postal_code']) {
return this.pricing.shippingAddress({
country: dom.value(elems['shipping_address.country']),
postal_code: dom.value(elems['shipping_address.postal_code'])
});
}
})
.then(() => {
// Taxes
let taxParams = {};
if (elems['tax_amount.now'] || elems['tax_amount.next']) {
taxParams.amount = {
now: (dom.value(elems['tax_amount.now']) || 0),
next: (dom.value(elems['tax_amount.next']) || 0)
};
}
if (elems.vat_number) taxParams.vat_number = dom.value(elems.vat_number);
return this.pricing.tax(taxParams);
})
.then(() => {
return this.pricing.reprice();
})
.then(() => {
if (event === INIT_RUN) this.emit('ready');
})
.done();
}
/**
* Updates output elements
*
* - TODO: Should there be output elements for each item in `price[when].items`?
*
*/
updateOutput (price) {
const elems = this.elements;
dom.value(elems.currency_code, price.currency.code);
dom.value(elems.currency_symbol, price.currency.symbol);
['subscriptions', 'adjustments', 'discount', 'subtotal', 'taxes', 'total'].forEach(value => {
dom.value(elems[value + '_now'], price.now[value]);
dom.value(elems[value + '_next'], price.next[value]);
});
dom.value(elems['gift_card_now'], price.now.giftCard);
dom.value(elems['gift_card_next'], price.next.giftCard);
if (elems.addon_price) {
elems.addon_price.forEach(elem => {
const subscriptionId = dom.data(elem, 'recurlySubscription');
const subscription = subscriptionId && this.pricing.findSubscriptionById(subscriptionId);
if (!subscription) return;
if (!subscription.isValid) return;
const addonPrice = subscription.price.base.addons[dom.data(elem, 'recurlyAddon')];
if (addonPrice) dom.value(elem, addonPrice);
});
}
}
detach () {
this.pricing.off('change', this.updateOutput);
this.elements.all.forEach(elem => {
elem.removeEventListener('change', this.onInputChange);
elem.removeEventListener('propertychange', this.onInputChange);
});
}
}