mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-05 03:31:56 +00:00
ed51d8b13f
major merge of features from the hosted branch that was created temporarily during the initial launch of jambonz.org
225 lines
7.8 KiB
JavaScript
225 lines
7.8 KiB
JavaScript
if (!process.env.JAMBONES_HOSTING) return;
|
|
|
|
const assert = require('assert');
|
|
assert.ok(process.env.STRIPE_API_KEY || process.env.NODE_ENV === 'test',
|
|
'missing env STRIPE_API_KEY for billing operations');
|
|
assert.ok(process.env.STRIPE_BASE_URL || process.env.NODE_ENV === 'test',
|
|
'missing env STRIPE_BASE_URL for billing operations');
|
|
|
|
const bent = require('bent');
|
|
const formurlencoded = require('form-urlencoded').default;
|
|
const qs = require('qs');
|
|
const toBase64 = (str) => Buffer.from(str || '', 'utf8').toString('base64');
|
|
const basicAuth = () => {
|
|
const header = `Basic ${toBase64(process.env.STRIPE_API_KEY)}`;
|
|
return {Authorization: header};
|
|
};
|
|
const postForm = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'POST', 'string',
|
|
Object.assign({'Content-Type': 'application/x-www-form-urlencoded'}, basicAuth()), 200);
|
|
const getJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'GET', 'json', basicAuth(), 200);
|
|
const deleteJSON = bent(process.env.STRIPE_BASE_URL || 'http://127.0.0.1', 'DELETE', 'json', basicAuth(), 200);
|
|
//const debug = require('debug')('jambonz:api-server');
|
|
|
|
const listProducts = async(logger) => await getJSON('/products?active=true');
|
|
|
|
const listPrices = async(logger) => await getJSON('/prices?active=true&expand[]=data.tiers&expand[]=data.product');
|
|
|
|
const retrievePricesForProduct = async(logger, id) =>
|
|
await getJSON(`/prices?product=${id}&active=true&expand[]=data.tiers&expand[]=data.product`);
|
|
|
|
const retrieveProduct = async(logger, id) =>
|
|
await getJSON(`/products/${id}`);
|
|
|
|
|
|
const retrieveCustomer = async(logger, id) =>
|
|
await getJSON(`/customers/${id}`);
|
|
|
|
const retrieveUpcomingInvoice = async(logger, customer_id, subscription_id, items) => {
|
|
const params = Object.assign(
|
|
{customer: customer_id},
|
|
subscription_id ? {subscription: subscription_id} : {},
|
|
items ? {subscription_items: items} : {});
|
|
const queryString = qs.stringify(params, {encode: false});
|
|
logger.debug({params, qs}, 'retrieving upcoming invoice');
|
|
return await getJSON(`/invoices/upcoming?${queryString}`);
|
|
};
|
|
|
|
const retrieveInvoice = async(logger, id) =>
|
|
await getJSON(`/invoices/${id}`);
|
|
|
|
const createCustomer = async(logger, account_sid, email, name) => {
|
|
const obj = {
|
|
email,
|
|
metadata: {account_sid}
|
|
};
|
|
if (name) obj.name = name;
|
|
logger.debug({obj}, 'provisioning customer');
|
|
const result = await postForm('/customers', formurlencoded(obj));
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
const updateCustomer = async(logger, id, obj) => {
|
|
logger.debug({obj}, `updating customer ${id}`);
|
|
const result = await postForm(`/customers/${id}`, formurlencoded(obj));
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
const deleteCustomer = async(logger, id) =>
|
|
await deleteJSON(`/customers/${id}`);
|
|
|
|
const attachPaymentMethod = async(logger, payment_method_id, customer_id) => {
|
|
const obj = {
|
|
customer: customer_id
|
|
};
|
|
const result = await postForm(`/payment_methods/${payment_method_id}/attach`,
|
|
formurlencoded(obj));
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
const detachPaymentMethod = async(logger, payment_method_id) => {
|
|
const result = await postForm(`/payment_methods/${payment_method_id}/detach`);
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
const createSubscription = async(logger, customer, metadata, items) => {
|
|
assert.ok(Array.isArray(items) && items.length > 0);
|
|
const obj = {
|
|
customer,
|
|
metadata,
|
|
items
|
|
};
|
|
const result = await postForm('/subscriptions?expand[]=latest_invoice&expand[]=latest_invoice.payment_intent',
|
|
formurlencoded(obj));
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
/*
|
|
const deleteInvoiceItem = async(logger, id) => {
|
|
return JSON.parse(await deleteJSON(`/invoiceitems/${id}`));
|
|
};
|
|
*/
|
|
|
|
const updateSubscription = async(logger, id, items) => {
|
|
assert.ok(Array.isArray(items) && items.length > 0);
|
|
const obj = {
|
|
proration_behavior: 'always_invoice',
|
|
payment_behavior: 'pending_if_incomplete',
|
|
items
|
|
};
|
|
const result = await postForm(`/subscriptions/${id}`,
|
|
formurlencoded(obj));
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
|
|
const payInvoice = async(logger, id) => {
|
|
const result = await postForm(`/invoices/${id}/pay`);
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
const payOutstandingInvoicesForCustomer = async(logger, customer_id) => {
|
|
let success = true;
|
|
const customer = await retrieveCustomer(logger, customer_id);
|
|
const {subscriptions} = customer;
|
|
logger.debug({subscriptions}, 'payOutstandingInvoicesForCustomer - subscriptions');
|
|
if (subscriptions && subscriptions.data.length > 0) {
|
|
const promises = subscriptions.data
|
|
.filter((s) => ['incomplete', 'past_due'].includes(s.status) || s.pending_update)
|
|
.map((s) => payInvoice(logger, s.latest_invoice));
|
|
const invoices = await Promise.all(promises);
|
|
if (invoices.find((i) => 'paid' !== i.status)) {
|
|
success = false;
|
|
}
|
|
}
|
|
return success;
|
|
};
|
|
|
|
const retrieveSubscription = async(logger, id) =>
|
|
await getJSON(`/subscriptions/${id}?expand[]=latest_invoice`);
|
|
|
|
const cancelSubscription = async(logger, id) =>
|
|
await deleteJSON(`/subscriptions/${id}`);
|
|
|
|
const retrievePaymentMethod = async(logger, id) => await getJSON(`/payment_methods/${id}`);
|
|
|
|
const calculateInvoiceAmount = async(logger, products) => {
|
|
assert.ok(Array.isArray(products) && products.length, 'calculateInvoiceAmount: products must be array');
|
|
assert.ok(!products.find((p) => !p.priceId || !p.quantity), 'calculateInvoiceAmount: invalid products array');
|
|
|
|
const prices = await Promise.all(products.map((p) => {
|
|
return getJSON(`/prices/${p.priceId}?expand[]=tiers`);
|
|
}));
|
|
logger.debug({prices, products}, 'calculateInvoiceAmount retrieved prices');
|
|
|
|
const total = prices.reduce((acc, pr) => {
|
|
const product = products.find((product) => product.priceId === pr.id);
|
|
logger.debug({product}, 'calculating price for line item');
|
|
if (pr.billing_scheme === 'per_unit') {
|
|
const lineItemCost = pr.unit_amount * product.quantity;
|
|
logger.debug(`per-unit pricing: ${product.quantity} * ${pr.unit_amount} = ${lineItemCost} usd`);
|
|
return acc + lineItemCost;
|
|
}
|
|
else if (pr.billing_scheme === 'tiered') {
|
|
const tier = pr.tiers.find((t) => product.quantity <= t.up_to || t.up_to === null);
|
|
if (typeof tier.flat_amount === 'number') {
|
|
const lineItemCost = tier.flat_amount;
|
|
logger.debug({tier}, `tiered pricing, flat amount: ${product.quantity} = ${lineItemCost} usd`);
|
|
return acc + lineItemCost;
|
|
}
|
|
else {
|
|
const lineItemCost = tier.unit_amount * product.quantity;
|
|
logger.debug({tier},
|
|
`tiered pricing, per-unit based: ${product.quantity} * ${tier.unit_amount} = ${lineItemCost} usd`);
|
|
return acc + (tier.unit_amount * product.quantity);
|
|
}
|
|
}
|
|
else {
|
|
// TODO: handle volume pricing
|
|
assert(false, `calculateInvoiceAmount: billing_scheme ${pr.billing_scheme} not implemented!!`);
|
|
}
|
|
|
|
}, 0);
|
|
logger.debug(`calculateInvoiceAmount total cost ${total}`);
|
|
return {amount: total, currency: prices[0].currency};
|
|
};
|
|
|
|
const createPaymentIntent = async(logger,
|
|
{account_sid, stripe_customer_id, amount, email, currency, stripe_payment_method_id}) => {
|
|
const obj = {
|
|
amount,
|
|
currency,
|
|
customer: stripe_customer_id,
|
|
payment_method: stripe_payment_method_id,
|
|
receipt_email: email,
|
|
metadata: {
|
|
account_sid
|
|
},
|
|
setup_future_usage: 'off_session'
|
|
};
|
|
const result = await postForm('/payment_intents', formurlencoded(obj));
|
|
return JSON.parse(result);
|
|
};
|
|
|
|
module.exports = {
|
|
listProducts,
|
|
listPrices,
|
|
createCustomer,
|
|
retrieveCustomer,
|
|
updateCustomer,
|
|
deleteCustomer,
|
|
createSubscription,
|
|
retrieveSubscription,
|
|
cancelSubscription,
|
|
updateSubscription,
|
|
retrievePaymentMethod,
|
|
calculateInvoiceAmount,
|
|
createPaymentIntent,
|
|
attachPaymentMethod,
|
|
detachPaymentMethod,
|
|
retrieveUpcomingInvoice,
|
|
payOutstandingInvoicesForCustomer,
|
|
retrieveInvoice,
|
|
retrieveProduct,
|
|
retrievePricesForProduct
|
|
};
|