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:
Dave Horton
2021-06-17 15:56:21 -04:00
committed by GitHub
parent ab7c69c0e8
commit ed51d8b13f
105 changed files with 10330 additions and 1601 deletions
+176
View File
@@ -1,10 +1,57 @@
const debug = require('debug')('jambonz:api-server');
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const {promisePool} = require('../db');
const uuid = require('uuid').v4;
const {encrypt} = require('../utils/encrypt-decrypt');
const retrieveSql = `SELECT * from accounts acc
LEFT JOIN webhooks AS rh
ON acc.registration_hook_sid = rh.webhook_sid`;
const insertPendingAccountSubscriptionSql = `INSERT account_subscriptions
(account_subscription_sid, account_sid, pending, stripe_subscription_id,
stripe_payment_method_id, last4, exp_month, exp_year, card_type)
VALUES (?,?,1,?,?,?,?,?,?)`;
const activateSubscriptionSql = `UPDATE account_subscriptions
SET pending=0, effective_start_date = CURRENT_TIMESTAMP, stripe_subscription_id = ?
WHERE account_subscription_sid = ?
AND pending=1`;
const queryPendingSubscriptionSql = `SELECT * FROM account_subscriptions
WHERE account_sid = ?
AND effective_end_date IS NULL
AND pending=1`;
const deactivateSubscriptionSql = `UPDATE account_subscriptions
SET pending=1, pending_reason = ?
WHERE account_sid = ?
AND effective_end_date IS NULL
AND pending=0`;
const updatePaymentInfoSql = `UPDATE account_subscriptions
SET last4 = ?, exp_month = ?, exp_year = ?, card_type = ?
WHERE account_sid = ?
AND effective_end_date IS NULL`;
const insertAccountProductsSql = `INSERT account_products
(account_product_sid, account_subscription_sid, product_sid, quantity)
VALUES (?,?,?,?);
`;
const replaceOldSubscriptionSql = `UPDATE account_subscriptions
SET effective_end_date = CURRENT_TIMESTAMP, change_reason = ?
WHERE account_sid = ?
AND effective_end_date IS NULL
AND account_subscription_sid <> ?`;
const retrieveActiveSubscriptionSql = `SELECT *
FROM account_subscriptions
WHERE account_sid = ?
AND effective_end_date IS NULL
AND pending = 0`;
function transmogrifyResults(results) {
return results.map((row) => {
const obj = row.acc;
@@ -73,6 +120,111 @@ class Account extends Model {
});
}
static async updateStripeCustomerId(sid, customerId) {
await promisePool.execute(
'UPDATE accounts SET stripe_customer_id = ? WHERE account_sid = ?',
[customerId, sid]);
}
static async getSubscription(sid) {
const [r] = await promisePool.execute(retrieveActiveSubscriptionSql, [sid]);
debug(r, `Account.getSubscription ${sid}`);
return r.length > 0 ? r[0] : null;
}
static async deactivateSubscription(logger, account_sid, reason) {
logger.debug('deactivateSubscription');
/**
* Two cases:
* (1) A subscription renewal fails. In this case we deactivate subscription
* and the customer is down until they provide payment.
* (2) A customer adds capacity during the month, and the pro-rated amount fails.
* In this case, we leave the new subscription in a pending state
* The customer continues (for the rest of the month at least) at
* previous capacity levels.
*/
const [r] = await promisePool.query(queryPendingSubscriptionSql, account_sid);
if (r.length > 0) {
/* leave new subscription pending */
await promisePool.execute(
'UPDATE account_subscriptions set pending_reason = ? WHERE account_subscription_sid = ?',
[reason, r[0].account_subscription_sid]);
logger.debug('deactivateSubscription - leave pending subscription in pending state');
}
else {
/* deactivate their current active subscription */
const [r] = await promisePool.execute(deactivateSubscriptionSql, [reason, account_sid]);
logger.debug('deactivateSubscription - deactivated subscription; customer will not have service');
return 1 == r.affectedRows;
}
}
static async activateSubscription(logger, account_sid, subscription_id, reason) {
logger.debug('activateSubscription');
const [r] = await promisePool.query(queryPendingSubscriptionSql, account_sid);
if (0 === r.length) return false;
const [r2] = await promisePool.execute(activateSubscriptionSql,
[subscription_id, r[0].account_subscription_sid]);
if (0 === r2.affectedRows) return false;
/* disable the old subscription, if any */
const [r3] = await promisePool.execute(replaceOldSubscriptionSql, [
reason, account_sid, r[0].account_subscription_sid]);
debug(r3, 'Account.activateSubscription - replaced old subscription');
/* update account.plan to paid, if it isnt already */
await promisePool.execute(
'UPDATE accounts SET plan_type = \'paid\' WHERE account_sid = ?',
[account_sid]);
return true;
}
static async updatePaymentInfo(logger, account_sid, pm) {
const {card} = pm;
const last4_encrypted = encrypt(card.last4);
await promisePool.execute(updatePaymentInfoSql,
[last4_encrypted, card.exp_month, card.exp_year, card.brand, account_sid]);
}
static async provisionPendingSubscription(logger, account_sid, products, payment_method, subscription_id) {
logger.debug('provisionPendingSubscription');
const account_subscription_sid = uuid();
const {id, card} = payment_method;
/* add a row to account_subscription */
let last4_encrypted = null;
if (card) {
last4_encrypted = encrypt(card.last4);
}
const [r] = await promisePool.execute(insertPendingAccountSubscriptionSql, [
account_subscription_sid,
account_sid,
subscription_id || null,
id,
last4_encrypted,
card ? card.exp_month : null,
card ? card.exp_year : null,
card ? card.brand : null
]);
debug(r, 'Account.activateSubscription - insert account_subscriptions');
if (r.affectedRows !== 1) {
throw new Error(`failed inserting account_subscriptions for accunt_sid ${account_sid}`);
}
/* add a row for each product to account_products */
await Promise.all(products.map((product) => {
const {product_sid, quantity} = product;
const account_products_sid = uuid();
return promisePool.execute(insertAccountProductsSql, [
account_products_sid, account_subscription_sid, product_sid, quantity
]);
}));
return account_subscription_sid;
}
}
Account.table = 'accounts';
@@ -103,6 +255,30 @@ Account.fields = [
{
name: 'device_calling_application_sid',
type: 'string',
},
{
name: 'is_active',
type: 'number',
},
{
name: 'created_at',
type: 'date',
},
{
name: 'plan_type',
type: 'string',
},
{
name: 'stripe_customer_id',
type: 'string',
},
{
name: 'webhook_secret',
type: 'string',
},
{
name: 'disable_cdrs',
type: 'number',
}
];
+37 -2
View File
@@ -1,9 +1,45 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
const sql = 'SELECT * from phone_numbers WHERE account_sid = ?';
class PhoneNumber extends Model {
constructor() {
super();
}
static retrieveAll(account_sid) {
if (!account_sid) return super.retrieveAll();
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, account_sid, (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
/**
* retrieve an application
*/
static retrieve(sid, account_sid) {
if (!account_sid) return super.retrieve(sid);
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(`${sql} AND phone_number_sid = ?`, [account_sid, sid], (err, results, fields) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
}
PhoneNumber.table = 'phone_numbers';
@@ -20,8 +56,7 @@ PhoneNumber.fields = [
},
{
name: 'voip_carrier_sid',
type: 'string',
required: true
type: 'string'
},
{
name: 'account_sid',
+63
View File
@@ -0,0 +1,63 @@
const Model = require('./model');
class PredefinedCarrier extends Model {
constructor() {
super();
}
}
PredefinedCarrier.table = 'predefined_carriers';
PredefinedCarrier.fields = [
{
name: 'predefined_carrier_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'requires_static_ip',
type: 'number'
},
{
name: 'e164_leading_plus',
type: 'number'
},
{
name: 'requires_register',
type: 'number'
},
{
name: 'register_username',
type: 'string'
},
{
name: 'register_sip_realm',
type: 'string'
},
{
name: 'register_password',
type: 'string'
},
{
name: 'tech_prefix',
type: 'string'
},
{
name: 'inbound_auth_username',
type: 'string'
},
{
name: 'inbound_auth_password',
type: 'string'
},
{
name: 'diversion',
type: 'string'
},
];
module.exports = PredefinedCarrier;
+28
View File
@@ -0,0 +1,28 @@
const Model = require('./model');
class Product extends Model {
constructor() {
super();
}
}
Product.table = 'products';
Product.fields = [
{
name: 'product_sid',
type: 'string',
primaryKey: true
},
{
name: 'name',
type: 'string',
required: true
},
{
name: 'category',
type: 'string',
required: true
},
];
module.exports = Product;
+13
View File
@@ -1,9 +1,18 @@
const Model = require('./model');
const {promisePool} = require('../db');
const retrieveSql = 'SELECT * from sip_gateways WHERE voip_carrier_sid = ?';
class SipGateway extends Model {
constructor() {
super();
}
/**
* list all sip gateways for a voip_carrier
*/
static async retrieveForVoipCarrier(voip_carrier_sid) {
const [rows] = await promisePool.query(retrieveSql, voip_carrier_sid);
return rows;
}
}
SipGateway.table = 'sip_gateways';
@@ -26,6 +35,10 @@ SipGateway.fields = [
name: 'port',
type: 'number'
},
{
name: 'netmask',
type: 'number'
},
{
name: 'inbound',
type: 'number'
+55
View File
@@ -0,0 +1,55 @@
const Model = require('./model');
const {getMysqlConnection} = require('../db');
class Smpp extends Model {
constructor() {
super();
}
/**
* list all SBCs either for a given service provider, or those not associated with a
* service provider (i.e. community SBCs)
*/
static retrieveAll(service_provider_sid) {
const sql = service_provider_sid ?
'SELECT * from smpp_addresses WHERE service_provider_sid = ?' :
'SELECT * from smpp_addresses WHERE service_provider_sid IS NULL';
const args = service_provider_sid ? [service_provider_sid] : [];
return new Promise((resolve, reject) => {
getMysqlConnection((err, conn) => {
if (err) return reject(err);
conn.query(sql, args, (err, results) => {
conn.release();
if (err) return reject(err);
resolve(results);
});
});
});
}
}
Smpp.table = 'smpp_addresses';
Smpp.fields = [
{
name: 'smpp_address_sid',
type: 'string',
primaryKey: true
},
{
name: 'ipv4',
type: 'string',
required: true
},
{
name: 'port',
type: 'number'
},
{
name: 'service_provider_sid',
type: 'string'
}
];
module.exports = Smpp;
+60
View File
@@ -0,0 +1,60 @@
const Model = require('./model');
const {promisePool} = require('../db');
const retrieveSql = 'SELECT * from smpp_gateways WHERE voip_carrier_sid = ?';
class SmppGateway extends Model {
constructor() {
super();
}
/**
* list all sip gateways for a voip_carrier
*/
static async retrieveForVoipCarrier(voip_carrier_sid) {
const [rows] = await promisePool.query(retrieveSql, voip_carrier_sid);
return rows;
}
}
SmppGateway.table = 'smpp_gateways';
SmppGateway.fields = [
{
name: 'smpp_gateway_sid',
type: 'string',
primaryKey: true
},
{
name: 'voip_carrier_sid',
type: 'string'
},
{
name: 'ipv4',
type: 'string',
required: true
},
{
name: 'port',
type: 'number'
},
{
name: 'netmask',
type: 'number'
},
{
name: 'inbound',
type: 'number'
},
{
name: 'outbound',
type: 'number'
},
{
name: 'is_primary',
type: 'number'
},
{
name: 'use_tls',
type: 'number'
}
];
module.exports = SmppGateway;
+92
View File
@@ -0,0 +1,92 @@
const Model = require('./model');
const {promisePool} = require('../db');
const retrieveSql = 'SELECT * from speech_credentials WHERE account_sid = ?';
const retrieveSqlForSP = 'SELECT * from speech_credentials WHERE service_provider_sid = ?';
class SpeechCredential extends Model {
constructor() {
super();
}
/**
* list all credentials for an account
*/
static async retrieveAll(account_sid) {
const [rows] = await promisePool.query(retrieveSql, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid);
return rows;
}
static async disableStt(account_sid) {
await promisePool.execute('UPDATE speech_credentials SET use_for_stt = 0 WHERE account_sid = ?', [account_sid]);
}
static async disableTts(account_sid) {
await promisePool.execute('UPDATE speech_credentials SET use_for_tts = 0 WHERE account_sid = ?', [account_sid]);
}
static async ttsTestResult(sid, success) {
await promisePool.execute(
'UPDATE speech_credentials SET last_tested = NOW(), tts_tested_ok = ? WHERE speech_credential_sid = ?',
[success, sid]);
}
static async sttTestResult(sid, success) {
await promisePool.execute(
'UPDATE speech_credentials SET last_tested = NOW(), stt_tested_ok = ? WHERE speech_credential_sid = ?',
[success, sid]);
}
}
SpeechCredential.table = 'speech_credentials';
SpeechCredential.fields = [
{
name: 'speech_credential_sid',
type: 'string',
primaryKey: true
},
{
name: 'account_sid',
type: 'string',
},
{
name: 'service_provider_sid',
type: 'string',
},
{
name: 'vendor',
type: 'string',
required: true,
},
{
name: 'credential',
type: 'string',
},
{
name: 'use_for_tts',
type: 'number'
},
{
name: 'use_for_stt',
type: 'number'
},
{
name: 'tts_tested_ok',
type: 'number'
},
{
name: 'stt_tested_ok',
type: 'number'
},
{
name: 'last_used',
type: 'date'
},
{
name: 'last_tested',
type: 'date'
}
];
module.exports = SpeechCredential;
+62 -1
View File
@@ -1,9 +1,22 @@
const Model = require('./model');
const {promisePool} = require('../db');
const retrieveSql = 'SELECT * from voip_carriers vc WHERE vc.account_sid = ?';
const retrieveSqlForSP = 'SELECT * from voip_carriers vc WHERE vc.service_provider_sid = ?';
class VoipCarrier extends Model {
constructor() {
super();
}
static async retrieveAll(account_sid) {
if (!account_sid) return super.retrieveAll();
const [rows] = await promisePool.query(retrieveSql, account_sid);
return rows;
}
static async retrieveAllForSP(service_provider_sid) {
const [rows] = await promisePool.query(retrieveSqlForSP, service_provider_sid);
return rows;
}
}
VoipCarrier.table = 'voip_carriers';
@@ -26,6 +39,10 @@ VoipCarrier.fields = [
name: 'account_sid',
type: 'string',
},
{
name: 'service_provider_sid',
type: 'string',
},
{
name: 'application_sid',
type: 'string'
@@ -49,7 +66,51 @@ VoipCarrier.fields = [
{
name: 'register_password',
type: 'string'
}
},
{
name: 'tech_prefix',
type: 'string'
},
{
name: 'inbound_auth_username',
type: 'string'
},
{
name: 'inbound_auth_password',
type: 'string'
},
{
name: 'diversion',
type: 'string'
},
{
name: 'is_active',
type: 'number'
},
{
name: 'smpp_system_id',
type: 'string'
},
{
name: 'smpp_password',
type: 'string'
},
{
name: 'smpp_inbound_system_id',
type: 'string'
},
{
name: 'smpp_inbound_password',
type: 'string'
},
{
name: 'smpp_enquire_link_interval',
type: 'number'
},
{
name: 'smpp_system_id',
type: 'string'
},
];
module.exports = VoipCarrier;