Jump to content

Seb

Administrators
  • Posts

    670
  • Joined

  • Last visited

Posts posted by Seb

  1. Hi Siggles,

    We use different fraud scanning tools to set the scores so it's usually pretty accurate -- it's mostly based on IP addresses and addresses and distance between the two. So a VPN would often trigger

    Typically people set review around 75 and then the block quite high (~95) and then adjust it if they are getting a large number of fraud orders. Reviewing will allow payment to be made but stop provisioning until the order is accepted.

    Thanks

    Seb

     

  2. Hi 

    We did a lot of analysis on the pricing and it makes it cheaper for almost every single user of ours -- including dropping a number of people from paid to free. We've increased staff users from 1 to 3 on the free plan. However we also haven't changed the plans of any existing clients -- they remain on the existing plans and can change if they want (and most would do given that the new pricing is almost always cheaper)

    I see the point on webhooks not being available on our starter plan and we'll see if we can enable those on that -- the problem is that can be quite expensive for us to provide as it's very bandwidth intensive, so we might need to limit the number available. Obviously we're a SAAS solution so we're not just a software business - it costs us to host the service too.

    As to monthly invoice totals, I think they are very reasonable - for a business turning over $3.6 million we'd only charge $1500 a month. 

  3. Hi Ryan

    Yes you can do this via the API. The state of our API is that it is ready but our documentation is not. Our frontend app just uses our API- so basically the API is what we use ourself to develop. We do have nascent API docs at https://demoapi.upmind.io/doc but in reality if you were building this the easiest solution is to do the below steps in our admin area and just examine and copy the network request in the browser.

    You would make an API call to create a client, then add a product with a zero price monthly

    Then when they cancel you would just update the product to have a price. Or you could have two products and upgrade/downgrade between them.

    So for instance

     

    1. Add a client

    2. Add a product (with zero price)

    3. When they cancel do a modify product to a different product

     

     

  4. We made an important decision to be a SAAS product as it enables us to make a much better product- but some important things:

    1. We're currently working on data residency options so you can choose different locations for your data when you sign up.

    2. You can get all of your data at any time -- go to settings -> Accounting & Reports and you can enable 'External storage'. This sends your key data daily to an S3 bucket of your choice -- more options coming soon for locations.

    3. We're working on provisioning v2 at the moment which you can run yourself, so all the server info you have is then stored wherever you like (should you want this)

    4. We never store any financial data like card information - this is all stored with the payment gateways you choose and we just store token.

    • Like 1
  5. At upmind we're just software, so you add in your own payment gateway credentials

    We support multiple gateways - but most common are PayPal or Stripe.

    So you would sign up for your own account with them and then they will give you an API key that you enter into Upmind.

    We have guides on how you can configure those gateways here:

    https://docs.upmind.com/docs/how-to-add-payment-gateways

    https://docs.upmind.com/docs/how-to-add-stripe-as-a-payment-method

    https://docs.upmind.com/docs/how-to-add-paypal-as-a-payment-method

     

    The payouts then come from those payment gateways -- we don't touch the actual payments or money at all

  6. Guys we have loads and loads underway. I know we're pretty bad at updating about the things we release at the moment but that's about to improve with a first set of newsletters this week. But for example this week:

    - Payment in foreign currencies
    - Dynamic exchange rates to those currencies
    - Adding margin to those exchange rates
    - Parent and child client functionality (so accounts can own other accounts)
    - Coingate crypto payments
    - Lots of performance improvements

    This week we're also doing OpenPay (Mexican payments), Retentions, Support Improvements, Stripe Indian e-madates

    For the *big* updates I know you are waiting for -- the new cart, provisioning v2 - there are big updates coming soon

    • Like 5
    • Sad 1
  7. Hi Rohit

    We're the billing system here not the hosting provider, so we wouldn't be the ones controlling the backups etc.

    If the hosting still exists on the server you can try running the unsuspend command. If not you would have to restore from backups or ask your hosting provider for backups

    Thanks

    Seb

  8. When importing to Upmind, you can receive an invalid utf8 data error which happens when data is encoded incorrectly in WHMCS. This throws error on the API so we can't export data. Here is a script you can run (at your own risk!) on your WHMCS database to fix it. Please read the instructions caerefully.

     

    <?php
    
    /**
     * Admin script to repair invalid utf8 data in the requested table.
     *
     * Installation:
     * 1. These instructions assume you have the default admin directory path of /admin
     *    so if you have a custom admin directory, please adjust the path as necessary.
     * 2. Copy this file into the /admin directory of your WHMCS installation.
     * 3. Run the script from the command line or via the web interface, as documented below.
     *
     * Usage tips:
     * - Although this script has been carefully written and tested on production
     *   databases, it's advised to take a backup of your database before running it,
     *   since changes are irreversible.
     * - This script operates on a single database table so you may need to run it on
     *   multiple tables depending on the places you're seeing invalid UTF8 data.
     * - The most common place to see invalid UTF8 data is in the tblhosting.password
     *   column. Because this column is encrypted at rest, you need to instruct the
     *   script to explicitly fix passwords, which involves the script decrypting,
     *   fixing, and re-encrypting them.
     * - It's always advised to run the script using the --dry/?dry=1 flag first to
     *   view changes which would be made before finally running the script again with
     *   the --fix/?fix=1 flag to actually update the database with the changes.
     * - When running the script via the web interface, you will need to provide the
     *   token which is displayed on the page when you run the script in dry mode.
     *   This is to prevent accidental executions and CSRF.
     *
     * CLI Usage:
     * php fix_invalid_utf8_data.php {table-name} [--dry] [--fix] [--fix-passwords]
     *
     * Web Usage:
     * GET admin/fix_invalid_utf8_data.php?table={table-name}&dry={0/1}&fix={0/1}&fix_passwords={0/1}&token={token}
     *
     * @author Harry Lewis <[email protected]>
     * @copyright Upmind 2023
     */
    
    function is_cli(): bool
    {
        return defined('STDIN');
    }
    
    /**
     * Write a message to stdout.
     */
    function stdout($str = ''): void
    {
        print($str . PHP_EOL);
    }
    
    /**
     * Write an error message and end the script.
     */
    function error($str, int $httpCode = 500): void
    {
        stdout($str);
    
        if (is_cli()) {
            exit(1);
        }
    
        http_response_code($httpCode);
        exit();
    }
    
    restore_error_handler();
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
    ini_set('log_errors', 0);
    
    if (is_cli()) {
        require __DIR__ . '/../init.php';
    
        // CLI - no auth required
        if (!isset($argv[1])) {
            return error(sprintf('Usage: php %s {table-name} [--dry] [--fix] [--fix-passwords]', $argv[0]));
        }
    
        $table = $argv[1];
        $fix = in_array('--fix', $argv);
        $dry = in_array('--dry', $argv);
        $fixPasswords = in_array('--fix-passwords', $argv);
    } else {
        define('ADMINAREA', true);
        require __DIR__ . '/../init.php';
    
        // Web - auth required
        $aInt = new WHMCS\Admin('Database Status'); // admin permissions tied to this string
        $aInt->title = 'Fix Invalid UTF8 Data';
        $aInt->sidebar = 'config';
        $aInt->icon = 'database';
        $aInt->helplink = 'database';
        $aInt->requireAuthConfirmation(); // requires admin pw confirmation
    
        echo '<pre>';
    
        if (!isset($_GET['table'])) {
            return error('Please specify a table to fix in ?table={table-name}', 422);
        }
    
        $table = $_GET['table'];
        $fix = boolval($_GET['fix'] ?? false);
        $dry = boolval($_GET['dry'] ?? false);
        $fixPasswords = boolval($_GET['fix_passwords'] ?? false);
    
        if ($fix) {
            check_token("WHMCS.admin.default");
        }
    }
    
    /** @var WHMCS\Database */
    $db = DI::make('db');
    /** @var PDO */
    $pdo = $db->getPdo();
    
    if (!in_array($table, $db->listTables())) {
        return error('Table not found: ' . $table, 422);
    }
    
    stdout('Selected table: ' . $table . '...');
    
    $query = $pdo->query(sprintf('SHOW COLUMNS FROM `%s` WHERE `Type` LIKE "varchar%%" OR `Type` LIKE "char%%" OR `Type` LIKE "%%text%%" OR `Key` LIKE "PRI";', $table));
    $columnsData = $query->fetchAll(PDO::FETCH_ASSOC);
    
    $fixColumns = [];
    foreach ($columnsData as $column) {
        if ($column['Key'] == 'PRI') {
            $idColumn = $column['Field'];
            continue;
        }
    
        $fixColumns[] = $column['Field'];
    }
    
    if (!isset($idColumn)) {
        return error(sprintf('No primary key found for table %s', $table));
    }
    
    if (empty($fixColumns)) {
        return error(sprintf('No string columns found to fix for table %s', $table));
    }
    
    stdout(sprintf('Identified %d columns to fix in table %s: %s', count($fixColumns), $table, implode(', ', $fixColumns)));
    
    $query = $pdo->query(sprintf('SELECT count(*) FROM `%s`', $table));
    $rowCount = $query->fetchColumn();
    $repaired = 0;
    
    if ($rowCount == 0) {
        return error(sprintf('No rows found in table %s', $table));
    }
    
    if (!$fix && !$dry) {
        is_cli()
            ? stdout(sprintf('Execute again with --dry to do a test run'))
            : stdout(sprintf('Found %d rows in table %s. Execute again with &dry=1 to do a test run', $rowCount, $table));
        return;
    }
    
    stdout();
    
    if ($dry) {
        stdout(sprintf('--- DRY RUN ---'));
    } elseif ($fix) {
        stdout(sprintf('--- FIXING DATA ---'));
    }
    
    stdout(sprintf('Found %d rows in table %s. Repairing data...', $rowCount, $table));
    
    $limit = 200;
    $offset = 0;
    
    $selectColumns = array_map(function (string $column) {
        return sprintf('`%s`', $column);
    }, array_merge([$idColumn], $fixColumns));
    
    $updateColumns = array_map(function (string $column) {
        return sprintf('`%s` = :%s', $column, $column);
    }, $fixColumns);
    
    $selectQuery = $pdo->prepare(sprintf('SELECT %s FROM `%s` LIMIT :limit OFFSET :offset', implode(', ', $selectColumns), $table));
    $selectQuery->bindParam(':limit', $limit, PDO::PARAM_INT);
    $selectQuery->bindParam(':offset', $offset, PDO::PARAM_INT);
    $selectQuery->execute();
    
    $updateQuery = $pdo->prepare(sprintf('UPDATE `%s` SET %s WHERE `%s` = :primary_key', $table, implode(', ', $updateColumns), $idColumn));
    
    while ($rows = $selectQuery->fetchAll(PDO::FETCH_ASSOC)) {
        $offset += $limit;
        stdout(sprintf('  ...processing %s/%s', min($offset, $rowCount), $rowCount));
    
        foreach ($rows as $rawRow) {
            $row = $rawRow;
    
            if ($fixPasswords && !empty($row['password'])) {
                try {
                    $decryptedPassword = decrypt($row['password']);
                    $passwordJson = json_encode($decryptedPassword, JSON_INVALID_UTF8_SUBSTITUTE);
                    $repairedPassword = json_decode($passwordJson);
    
                    if ($repairedPassword !== $decryptedPassword) {
                        $row['password'] = encrypt($repairedPassword);
                    }
                } catch (\Throwable $e) {
                    $row['password'] = '';
                }
            }
    
            $rowJson = json_encode($row, JSON_INVALID_UTF8_SUBSTITUTE);
            $repairedRow = json_decode($rowJson, true);
    
            if ($repairedRow !== $rawRow) {
                stdout(sprintf('    - Repairing %s %s %s: %s', $table, $idColumn, $row[$idColumn], $rowJson));
    
                $updateQuery->bindValue(':primary_key', $row[$idColumn]);
    
                foreach ($fixColumns as $column) {
                    $updateQuery->bindValue(sprintf(':%s', $column), $repairedRow[$column]);
                }
    
                if (!$dry) {
                    $updateQuery->execute();
                }
    
                $repaired++;
            }
        }
    
        $selectQuery->execute();
    }
    
    stdout('Done!');
    stdout();
    
    if ($dry) {
        is_cli()
            ? stdout(sprintf('Would have repaired %d rows in table %s. Execute again with --fix to repair the data', $repaired, $table))
            : stdout(sprintf('Would have repaired %d rows in table %s. Execute again with &fix=1&token=%s to repair the data', $repaired, $table, generate_token('plain')));
    } else {
        stdout(sprintf('Repaired %d rows in table %s', $repaired, $table));
    }
    
    return 0;

     

    • Like 2
  9. We are actually going to be changing our pricing relatively soon to make the free plan allow more users. We've got what we think is a much more sensible pricing model coming soon.

    I do think our pricing is extremely extremely good value (especially as it's free for most of our users) -- but there are obviously edge cases like where you use freelancers where it can get expensive that wasn't intended.

    • Like 1
  10. Hi Codent

    If they have made a payment (manually I guess?) then you can add that as a payment in the system - under billing -> account credits -> top up, and then you can allocate it against the raised invoice.

    We wouldn't void the credit note / mark the invoice as unpaid again, but the payment can just be allocated against the new invoice.

    Thanks

    Seb

     

  11. Our approach is to make as many things open source as possible. Provisioning is open source ,and payment gateways will be open source.

    We are currently building a JS widget framework that others can then develop to build their own themes and skins. Code will be open sourced also.

    • Like 2
×
×
  • Create New...