mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-24 13:02:16 +00:00
merge of features from hosted branch (#7)
major merge of features from the hosted branch that was created temporarily during the initial launch of jambonz.org
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
if (!process.env.JAMBONES_HOSTING) return;
|
||||
|
||||
const bent = require('bent');
|
||||
const crypto = require('crypto');
|
||||
const assert = require('assert');
|
||||
const domains = new Map();
|
||||
const debug = require('debug')('jambonz:api-server');
|
||||
|
||||
const checkAsserts = () => {
|
||||
assert.ok(process.env.DME_API_KEY, 'missing env DME_API_KEY for dns operations');
|
||||
assert.ok(process.env.DME_API_SECRET, 'missing env DME_API_SECRET for dns operations');
|
||||
assert.ok(process.env.DME_BASE_URL, 'missing env DME_BASE_URL for dns operations');
|
||||
};
|
||||
|
||||
const createAuthHeaders = () => {
|
||||
const now = (new Date()).toUTCString();
|
||||
const hash = crypto.createHmac('SHA1', process.env.DME_API_SECRET);
|
||||
hash.update(now);
|
||||
return {
|
||||
'x-dnsme-apiKey': process.env.DME_API_KEY,
|
||||
'x-dnsme-requestDate': now,
|
||||
'x-dnsme-hmac': hash.digest('hex')
|
||||
};
|
||||
};
|
||||
|
||||
const getDnsDomainId = async(logger, name) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
const get = bent(process.env.DME_BASE_URL, 'GET', 'json', headers);
|
||||
try {
|
||||
const result = await get('/dns/managed');
|
||||
debug(result, 'getDnsDomainId: all domains');
|
||||
if (Array.isArray(result.data)) {
|
||||
const domain = result.data.find((o) => o.name === name);
|
||||
if (domain) return domain.id;
|
||||
debug(`getDnsDomainId: failed to find domain ${name}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the DNS records for a given subdomain
|
||||
* We will add an A record and an SRV record for each SBC public IP address
|
||||
* Note: this assumes we have manually added DNS A records:
|
||||
* sbc01.root.domain, sbc0.root.domain, etc to dnsmadeeasy
|
||||
*/
|
||||
const createDnsRecords = async(logger, domain, name, value, ttl = 3600) => {
|
||||
checkAsserts();
|
||||
try {
|
||||
if (!domains.has(domain)) {
|
||||
const domainId = await getDnsDomainId(logger, domain);
|
||||
if (!domainId) return false;
|
||||
domains.set(domain, domainId);
|
||||
}
|
||||
const domainId = domains.get(domain);
|
||||
|
||||
value = Array.isArray(value) ? value : [value];
|
||||
const a_records = value.map((v) => {
|
||||
return {
|
||||
type: 'A',
|
||||
gtdLocation: 'DEFAULT',
|
||||
name,
|
||||
value: v,
|
||||
ttl
|
||||
};
|
||||
});
|
||||
const srv_records = [
|
||||
{
|
||||
type: 'SRV',
|
||||
gtdLocation: 'DEFAULT',
|
||||
name: `_sip._udp.${name}`,
|
||||
value: `${name}`,
|
||||
port: 5060,
|
||||
priority: 10,
|
||||
weight: 100,
|
||||
ttl
|
||||
}
|
||||
];
|
||||
const headers = createAuthHeaders();
|
||||
const records = [...a_records, ...srv_records];
|
||||
const post = bent(process.env.DME_BASE_URL, 'POST', 201, 400, headers);
|
||||
logger.debug({records}, 'Attemting to create dns records');
|
||||
const res = await post(`/dns/managed/${domainId}/records/createMulti`,
|
||||
[...a_records, ...srv_records]);
|
||||
|
||||
if (201 === res.statusCode) {
|
||||
const str = await res.text();
|
||||
return JSON.parse(str);
|
||||
}
|
||||
logger.error({res}, 'Error creating records');
|
||||
} catch (err) {
|
||||
logger.error({err}, 'Error retrieving domains');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteDnsRecords = async(logger, domain, recIds) => {
|
||||
checkAsserts();
|
||||
const headers = createAuthHeaders();
|
||||
const del = bent(process.env.DME_BASE_URL, 'DELETE', 200, headers);
|
||||
try {
|
||||
if (!domains.has(domain)) {
|
||||
const domainId = await getDnsDomainId(logger, domain);
|
||||
if (!domainId) return false;
|
||||
domains.set(domain, domainId);
|
||||
}
|
||||
const domainId = domains.get(domain);
|
||||
const url = `/dns/managed/${domainId}/records?${recIds.map((r) => `ids=${r}`).join('&')}`;
|
||||
await del(url);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.error({err}, 'Error deleting records');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
getDnsDomainId,
|
||||
createDnsRecords,
|
||||
deleteDnsRecords
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
const formData = require('form-data');
|
||||
const Mailgun = require('mailgun.js');
|
||||
const mailgun = new Mailgun(formData);
|
||||
const validateEmail = (email) => {
|
||||
// eslint-disable-next-line max-len
|
||||
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(email);
|
||||
};
|
||||
|
||||
const emailSimpleText = async(logger, to, subject, text) => {
|
||||
const mg = mailgun.client({
|
||||
username: 'api',
|
||||
key: process.env.MAILGUN_API_KEY
|
||||
});
|
||||
if (!process.env.MAILGUN_API_KEY) throw new Error('MAILGUN_API_KEY env variable is not defined!');
|
||||
if (!process.env.MAILGUN_DOMAIN) throw new Error('MAILGUN_DOMAIN env variable is not defined!');
|
||||
|
||||
try {
|
||||
const res = await mg.messages.create(process.env.MAILGUN_DOMAIN, {
|
||||
from: 'jambonz Support <support@jambonz.org>',
|
||||
to,
|
||||
subject,
|
||||
text
|
||||
});
|
||||
logger.debug({res}, 'sent email');
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error sending email');
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateEmail,
|
||||
emailSimpleText
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
const crypto = require('crypto');
|
||||
const algorithm = 'aes-256-ctr';
|
||||
const iv = crypto.randomBytes(16);
|
||||
const secretKey = crypto.createHash('sha256')
|
||||
.update(String(process.env.JWT_SECRET))
|
||||
.digest('base64')
|
||||
.substr(0, 32);
|
||||
|
||||
const encrypt = (text) => {
|
||||
const cipher = crypto.createCipheriv(algorithm, secretKey, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
|
||||
const data = {
|
||||
iv: iv.toString('hex'),
|
||||
content: encrypted.toString('hex')
|
||||
};
|
||||
return JSON.stringify(data);
|
||||
};
|
||||
|
||||
const decrypt = (data) => {
|
||||
const hash = JSON.parse(data);
|
||||
const decipher = crypto.createDecipheriv(algorithm, secretKey, Buffer.from(hash.iv, 'hex'));
|
||||
const decrpyted = Buffer.concat([decipher.update(Buffer.from(hash.content, 'hex')), decipher.final()]);
|
||||
return decrpyted.toString();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
encrypt,
|
||||
decrypt
|
||||
};
|
||||
+8
-1
@@ -16,8 +16,15 @@ class DbErrorUnprocessableRequest extends DbError {
|
||||
}
|
||||
}
|
||||
|
||||
class DbErrorForbidden extends DbError {
|
||||
constructor(msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DbError,
|
||||
DbErrorBadRequest,
|
||||
DbErrorUnprocessableRequest
|
||||
DbErrorUnprocessableRequest,
|
||||
DbErrorForbidden
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"trial": [
|
||||
{
|
||||
"category": "voice_call_session",
|
||||
"quantity": 20
|
||||
},
|
||||
{
|
||||
"category": "device",
|
||||
"quantity": 0
|
||||
}
|
||||
],
|
||||
"free": [
|
||||
{
|
||||
"category": "voice_call_session",
|
||||
"quantity": 1
|
||||
},
|
||||
{
|
||||
"category": "device",
|
||||
"quantity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
const assert = require('assert');
|
||||
const bent = require('bent');
|
||||
const postJSON = bent('POST', 'json', 200);
|
||||
const getJSON = bent('GET', 'json', 200);
|
||||
const {emailSimpleText} = require('./email-utils');
|
||||
const {DbErrorForbidden} = require('../utils/errors');
|
||||
|
||||
const doGithubAuth = async(logger, payload) => {
|
||||
assert.ok(process.env.GITHUB_CLIENT_SECRET, 'env var GITHUB_CLIENT_SECRET is required');
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
const obj = await postJSON('https://github.com/login/oauth/access_token', {
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GITHUB_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri
|
||||
});
|
||||
if (!obj.access_token) {
|
||||
logger.error({obj}, 'Error retrieving access_token from github');
|
||||
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
|
||||
throw new Error(obj.error || 'error retrieving access_token');
|
||||
}
|
||||
logger.debug({obj}, 'got response from github for access_token');
|
||||
|
||||
/* use the access token to get basic public info as well as primary email */
|
||||
const userDetails = await getJSON('https://api.github.com/user', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
});
|
||||
|
||||
const emails = await getJSON('https://api.github.com/user/emails', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
});
|
||||
const primary = emails.find((e) => e.primary);
|
||||
if (primary) Object.assign(userDetails, {
|
||||
email: primary.email,
|
||||
email_validated: primary.validated
|
||||
});
|
||||
|
||||
logger.info({userDetails}, 'retrieved user details from github');
|
||||
return userDetails;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error authenticating via github');
|
||||
throw new DbErrorForbidden(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
const doGoogleAuth = async(logger, payload) => {
|
||||
assert.ok(process.env.GOOGLE_OAUTH_CLIENT_SECRET, 'env var GOOGLE_OAUTH_CLIENT_SECRET is required');
|
||||
|
||||
try {
|
||||
/* exchange the code for an access token */
|
||||
const obj = await postJSON('https://oauth2.googleapis.com/token', {
|
||||
client_id: payload.oauth2_client_id,
|
||||
client_secret: process.env.GOOGLE_OAUTH_CLIENT_SECRET,
|
||||
code: payload.oauth2_code,
|
||||
state: payload.oauth2_state,
|
||||
redirect_uri: payload.oauth2_redirect_uri,
|
||||
grant_type: 'authorization_code'
|
||||
});
|
||||
if (!obj.access_token) {
|
||||
logger.error({obj}, 'Error retrieving access_token from github');
|
||||
if (obj.error === 'bad_verification_code') throw new Error('bad verification code');
|
||||
throw new Error(obj.error || 'error retrieving access_token');
|
||||
}
|
||||
logger.debug({obj}, 'got response from google for access_token');
|
||||
|
||||
/* use the access token to get basic public info as well as primary email */
|
||||
const userDetails = await getJSON('https://www.googleapis.com/oauth2/v2/userinfo', null, {
|
||||
Authorization: `Bearer ${obj.access_token}`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'jambonz 1.0'
|
||||
});
|
||||
|
||||
logger.info({userDetails}, 'retrieved user details from google');
|
||||
return userDetails;
|
||||
} catch (err) {
|
||||
logger.info({err}, 'Error authenticating via google');
|
||||
throw new DbErrorForbidden(err.message);
|
||||
}
|
||||
};
|
||||
const doLocalAuth = async(logger, payload) => {
|
||||
const {name, email, password, email_activation_code} = payload;
|
||||
const text = `Hi there
|
||||
|
||||
Welcome to jambonz! Your account activation code is ${email_activation_code}
|
||||
|
||||
Best,
|
||||
|
||||
The jambonz team`;
|
||||
|
||||
if ('test' !== process.env.NODE_ENV || process.env.MAILGUN_API_KEY) {
|
||||
await emailSimpleText(logger, email, 'Account activation code', text);
|
||||
}
|
||||
return {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
email_activation_code
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
doGithubAuth,
|
||||
doGoogleAuth,
|
||||
doLocalAuth
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
const crypto = require('crypto');
|
||||
const { argon2i } = require('argon2-ffi');
|
||||
const util = require('util');
|
||||
|
||||
const getRandomBytes = util.promisify(crypto.randomBytes);
|
||||
|
||||
const generateHashedPassword = async(password) => {
|
||||
const salt = await getRandomBytes(32);
|
||||
const passwordHash = await argon2i.hash(password, salt);
|
||||
return passwordHash;
|
||||
};
|
||||
|
||||
const verifyPassword = async(passwordHash, password) => {
|
||||
const isCorrect = await argon2i.verify(passwordHash, password);
|
||||
return isCorrect;
|
||||
};
|
||||
|
||||
const hashString = (s) => crypto.createHash('md5').update(s).digest('hex');
|
||||
|
||||
module.exports = {
|
||||
generateHashedPassword,
|
||||
verifyPassword,
|
||||
hashString
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
//const PNF = require('google-libphonenumber').PhoneNumberFormat;
|
||||
//const phoneUtil = require('google-libphonenumber').PhoneNumberUtil.getInstance();
|
||||
|
||||
const validateNumber = (number) => {
|
||||
if (typeof number !== 'string') throw new Error('phone number must be a string');
|
||||
if (!/^\d+$/.test(number)) throw new Error('phone number must only include digits');
|
||||
};
|
||||
|
||||
const e164 = (number) => {
|
||||
if (number.startsWith('+')) return number.slice(1);
|
||||
return number;
|
||||
/*
|
||||
const num = phoneUtil.parseAndKeepRawInput(number, 'US');
|
||||
if (!phoneUtil.isValidNumber(num)) throw new Error(`not a valid US telephone number: ${number}`);
|
||||
return phoneUtil.format(num, PNF.E164).slice(1);
|
||||
*/
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateNumber,
|
||||
e164
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
const ttsGoogle = require('@google-cloud/text-to-speech');
|
||||
const sttGoogle = require('@google-cloud/speech').v1p1beta1;
|
||||
const Polly = require('aws-sdk/clients/polly');
|
||||
const AWS = require('aws-sdk');
|
||||
const fs = require('fs');
|
||||
|
||||
const testGoogleTts = async(logger, credentials) => {
|
||||
const client = new ttsGoogle.TextToSpeechClient({credentials});
|
||||
await client.listVoices();
|
||||
};
|
||||
|
||||
const testGoogleStt = async(logger, credentials) => {
|
||||
const client = new sttGoogle.SpeechClient({credentials});
|
||||
const config = {
|
||||
sampleRateHertz: 8000,
|
||||
languageCode: 'en-US',
|
||||
model: 'default',
|
||||
};
|
||||
const audio = {
|
||||
content: fs.readFileSync(`${__dirname}/../../data/test_audio.wav`).toString('base64'),
|
||||
};
|
||||
const request = {
|
||||
config: config,
|
||||
audio: audio,
|
||||
};
|
||||
|
||||
// Detects speech in the audio file
|
||||
const [response] = await client.recognize(request);
|
||||
if (!Array.isArray(response.results) || 0 === response.results.length) {
|
||||
throw new Error('failed to transcribe speech');
|
||||
}
|
||||
};
|
||||
|
||||
const testAwsTts = (logger, credentials) => {
|
||||
const polly = new Polly(credentials);
|
||||
return new Promise((resolve, reject) => {
|
||||
polly.describeVoices({LanguageCode: 'en-US'}, (err, data) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const testAwsStt = (logger, credentials) => {
|
||||
const transcribeservice = new AWS.TranscribeService(credentials);
|
||||
return new Promise((resolve, reject) => {
|
||||
transcribeservice.listVocabularies((err, data) => {
|
||||
if (err) return reject(err);
|
||||
logger.info({data}, 'retrieved language models');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
testGoogleTts,
|
||||
testGoogleStt,
|
||||
testAwsTts,
|
||||
testAwsStt
|
||||
};
|
||||
@@ -0,0 +1,224 @@
|
||||
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
|
||||
};
|
||||
Reference in New Issue
Block a user