mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2025-12-19 05:47:46 +00:00
261 lines
8.5 KiB
JavaScript
261 lines
8.5 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 formurlencoded = require('form-urlencoded');
|
|
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 STRIPE_BASE_URL = process.env.STRIPE_BASE_URL || 'http://127.0.0.1';
|
|
const getJSON = async(path) => {
|
|
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...basicAuth()
|
|
}
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Error retrieving ${path} from stripe: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
};
|
|
|
|
const postForm = async(path, body) => {
|
|
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
...basicAuth()
|
|
},
|
|
body
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Error posting to ${path} from stripe: ${response.status}`);
|
|
}
|
|
return await response.text();
|
|
};
|
|
const deleteJSON = async(path) => {
|
|
const response = await fetch(`${STRIPE_BASE_URL}${path}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...basicAuth()
|
|
}
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Error deleting ${path} from stripe: ${response.status}`);
|
|
}
|
|
return await response.json();
|
|
};
|
|
//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
|
|
};
|