mirror of
https://github.com/jambonz/jambonz-api-server.git
synced 2026-07-24 21:12:10 +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:
@@ -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',
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user