Модуль выплат

A Payout Integration Documentation

This document outlines the structure and key components required to integrate a payout plugin into our system.

File Structure

Your payout should be organized in the following directory structure under the pluginsExternal/payouts directory. The directory is imported into the system as a plugin and should contain the following files:

pluginsExternal/payouts/PAYOUT_NAME/
  • configure.js: This is where you define the basic setup for your payout plugin, such as API keys, API Secret, and other necessary configuration options.

  • icon.png: A graphical representation of the payout. This icon is used throughout the system to visually identify your payout plugin.

  • index.js: The core implementation file for the payout plugin. This file contains the logic and functionality required to interact with the payout's API and our system.

  • callbacks.js: A file that contains the callback functions for the payout plugin. These functions are called by the system to handle various events, such as successful payouts, and failed payouts

  • templates/: A directory containing HTML and JavaScript templates for the payout's admin and client interfaces.

    • admin.html: A template file that defines the configuration interface seen by administrators when they set up the payout in the system.

    • admin.js: A script that handles the data binding and interaction logic for the admin configuration page.

Templates

The templates/ directory contains HTML and JavaScript files that define how the payout's configuration . This directory should contain the following files:

  • admin.html: This file defines the configuration interface seen by administrators when they set up the payout in the system. It should include form elements for setting up the payout, such as API keys, API secrets, and other necessary parameters.

  • admin.js: This file contains the logic for data binding and interaction on the admin configuration page. It should handle form submission, validation, and other necessary tasks to set up the payout.

Admin Config Page (admin.html)

The admin configuration page is where administrators set up the payout plugin in the system. This page should include form elements for setting up the payout, such as API keys, API secrets, and other necessary parameters that the payout requires using Vue.js.

Structural Explanation:

<div>
  <!-- Display Payout Name --> 
  <p>PAYOUT_NAME Payout</p>
  <!-- Display Supported Currencies -->
  <h3>Support currency:</h3>
  <li>
    <ul>Crypto</ul>
    <ul>BTC</ul>
  </li>

  <hr>
  <h3>Config:</h3>

  <label>API Key:</label>
  <br>
  <input placeholder="apiKey" v-model="apiKey">
  <button @click="saveConfig('apiKey',apiKey)">Save</button>
  <br>
  <label>Secret Key:</label>
  <br>
  <input :placeholder="this.config.apiSecret" v-model="apiSecret">
  <button @click="saveConfig('apiSecret',apiSecret)">Save</button>
  <br>
  <label>Debug log?</label>
  <br>
  <select v-model="debug">
    <option value="1">Yes</option>
    <option value="0">No</option>
  </select>
  <button @click="saveConfig('debug',debug)">Save</button>
  <hr>

  <h3>IP server:</h3>
  <p>{{ this.publicIpV4 }}</p>
  <br>
  <h3>Public IPv6:</h3>
  <span>{{ this.publicIpV6 || "Not supported" }}</span>

  <h3>Webhook URL:</h3>
  <span>{{ this.schema }}://{{ this.domain }}{{ this.server_path }}payout_call/{{ this.payoutKey }}/url_final/</span>

</div>
  • PAYOUT_NAME Payout: Display the name of the payout.

  • Support currency:: Display the supported currencies for the payout.

  • Config:: Display the configuration options for the payout where administrators can input the API key, API secret, and other necessary parameters

    • API Key:: Input field for the API key.

    • Secret Key:: Input field for the API secret.

    • Debug log?: Select field for enabling debug logging.

    • saveConfig: Function to save the configuration options, such as API key, API secret, and debug log.

  • Public IP and Webhook URL: Displays the public IP address and webhook URL for the payout incase it is needed.

Admin Config Export (admin.js)

The admin configuration script should handle the data binding and interaction logic for the admin configuration page. It should handle form submission, value display, and other necessary tasks to set up the payout.

Key Components:

const fs = require("fs");

module.exports = {
  template: fs.readFileSync(__dirname + '/admin.html', { encoding: 'utf8' }),
  props: {
    schema: { type: "String" },
    domain: { type: "String" },
    server_path: { type: "String" },
    publicIpV4: { type: "String" },
    publicIpV6: { type: "String" },
    payoutKey: { type: "String" },
    config: { type: "Object" }
  },
  data() {
    return {
      loadStatus: true,
      debug: '0',
      apiKey: '',
      apiSecret: '',
    }
  },

  created() {
    this.debug = this.config.debug;
    this.apiKey = this.config.apiKey;
  },
  mounted() { },
  methods: {
    saveConfig(key, value) {
      this.$rest.api('admin/merchant-and-payout/set-config-payout', {
        payoutKey: this.payoutKey,
        key,
        value
      })
        .then(res => {
          if (res.success)
            return alert('Success');
          alert('Error');
        })
        .catch(err => {
          console.error('admin/merchant-and-payout/set-config-payout', err);
          alert('Error');
        })
    },
  }
};
  • Template Loading: The template property loads the admin.html file into the Vue instance, allowing for seamless integration of the HTML template with the Vue component.

  • Props: The props object defines the properties that are passed to the Vue component from the parent component. These properties include the schema, domain, server path, public IP addresses, payout key, and configuration object.

  • Data: The data function initializes the data properties used in the Vue component, such as the debug flag, API key, and API secret. These properties are reactive and can be updated based on user input. You can also set default values for these properties.

  • Lifecycle Methods:

    • created: The created lifecycle method is called when the Vue component is created. You may have noticed that the debug and apiKey properties are initialized with values but not the apiSecret. This is because the apiSecret is a sensitive value that should not be stored in the component's data.

    • mounted: The mounted lifecycle method is called when the Vue component is mounted to the DOM. You can perform additional setup tasks in this method.

  • Methods:

    • saveConfig: The saveConfig method is called when the user clicks the "Save" button for any configuration option. It sends a request to the server to save the configuration value for the specified key. If the request is successful, an alert is shown to indicate success; otherwise, an error alert is displayed.

Configuration

The configure.js file is where you define the basic setup for your payout plugin, such as API keys, API Secret, and other necessary configuration options. This file should export an object with the following structure:

module.exports.type = 'payout';
module.exports.title = 'PAYOUT_NAME';
module.exports.name = 'PAYOUT_NAME';
module.exports.required_npm = [];
module.exports.allow_XML = [];

module.exports.default_config = {
  debug: '0',
  apiKey: '',
  apiSecret: '',
};
module.exports.required_config = {
  debug: 1,
  apiKey: 1,
  apiSecret: 2, // hide
};
  • type: The type of plugin, which should be set to 'payout' for a payout plugin.

  • title: he title of the payout plugin, which is displayed in the system's admin interface.

  • name: The name of the payout plugin, which is used to identify the plugin internally.

  • required_npm: An array of npm packages that are required for the payout plugin to function. If the plugin does not require any additional npm packages, you can leave this array empty.

  • allow_XML: An array of XML endpoints that the payout plugin can interact with. If the plugin does not interact with any XML endpoints, you can leave this array empty.

  • default_config: An object that defines the default configuration values for the payout plugin. Such as debug mode and API key...

  • required_config: An object that defines the configuration options that are required for the payout plugin to function. The values in this object correspond to the configuration options defined in the admin.html file.

    • debug: The debug mode configuration option to enable or disable debug logging.

    • apiKey: The API key configuration option to interact with the payout API.

    • apiSecret: The API secret configuration option to authenticate requests to the payout API.

    • 1: Indicates that the configuration value is loaded and displayed in the admin interface.

    • 2: Indicates that the configuration value is hidden in the admin interface and displayed as a password field.

Icon

The icon.png file should be a PNG image with a minimum size of 150x150 pixels. This icon will represent your payout plugin in the system's user interface, making it easily recognizable to administrators.

Implementation

The index.js file is the core implementation file for the payout plugin. This file contains the logic and functionality required to interact with the payout's API and our system. The implementation file should export a class with the following structure:

Constructor

The constructor is responsible for initializing the payout plugin with the necessary configurations and options. It should accept the configuration object as a parameter and set up the plugin accordingly.

Imports:

  • id_key: The unique identifier for the payout plugin, derived from the directory name.

  • config: The configuration object for the payout plugin, which contains the default configuration values and required configuration options.

  • BigNumber: A library for handling large numbers and decimal precision.

  • axios: A library for making HTTP requests to the payout API.

  • i18n: A module for internationalization and localization.

  • crypto: The Node.js crypto module for cryptographic operations.

  • Bluebird: A promise library for handling asynchronous operations.

Parameters:

  • options: An object containing the payout's configuration settings, such as the API key and debug mode.

const id_key = __dirname.split('/').slice(-1)[0];
const config = require('../../../modules/config/payout.config.js')(id_key);
const BigNumber = require('bignumber.js');
const axios = require('axios');
const i18n = require('../../../modules/i18n');
const crypto = require('node:crypto');
const Promise = require('bluebird');

class PAYOUT_NAME {
  constructor(options) {
    this.allowIPs = [];
    this.id_key = id_key;
    this.conf = {};
    if (options && options.apiKey) 
      this.conf.apiKey = options.apiKey;
    
    if (options && options.apiSecret) 
      this.conf.apiSecret = options.apiSecret;
    
    if (options && options.debug) 
      this.conf.debug = options.debug;
  }

//   Other methods and logic for interacting with the payout API
}
  • allowIPs: An array to store the IP addresses that are allowed to interact with the payout API. This can be used to restrict access to the API based on IP addresses.

  • id_key: The unique identifier for the payout plugin, derived from the directory name where the plugin is located.

  • conf: An object to store the configuration settings for the payout plugin, such as the API key, API secret, and debug mode.

Get Fields

The getFields method is responsible for preparing and returning the fields required for the given currency. This method returns an array of objects, each representing a field required for the payout process. For example, the fields could include the recipient's address, amount, and any additional information required by the payout API.

async getFields(lang, xml) {
  const currencyProvider = this.getProviderByXML(xml)
  if (!currencyProvider) {
    return null
  }

  let fields = [];

  if (currencyProvider.currency === 'crypto') {
    fields.push({
      _id: "account_crypto",
      name: i18n(lang, "en")._t(`payout/field/crypto_address`, {currency: currencyProvider.currency}) || 'Address',
      placeholder: 'Address...',
      regexp: '^.{3,}$',
      regexp_error: i18n(lang, "en")._t(`payout/field/error/crypto_address`, {currency: currencyProvider.currency}) || "Address incorrect",
      required: true
    })
  } else 
    fields.push({
      _id: "card_number",
      name: (i18n(lang, "en")._t(`payout/field/card_number`) || "Card Number") + ` ${currencyProvider.name}`,
      placeholder: "0000000000000000",
      regexp: "^[0-9]{16,18}$",
      regexp_error:
        (i18n(lang, "en")._t(`payout/field/error/card_number`) ||
        "card number incorrect") + ` ${currencyProvider.name}`,
      required: true,
    });
  return fields;
}
  • lang: The language code for the localization of field names and error messages.

  • xml: The XML endpoint for the payout request, which can be used to determine the currency provider.

  • currencyProvider: The currency provider object for the given currency, which contains information about the currency and type.

  • fields: An array to store the field objects required for the payout process.

    • _id: The unique identifier for the field, used to identify the field in the system.

    • name: The name of the field, which is displayed to the user in the system's interface.

    • placeholder: The placeholder text for the field, providing guidance to the user on what to input.

    • regexp: A regular expression pattern to validate the field's input.

    • regexp_error: The error message to display if the field input does not match the regular expression pattern.

    • required: A boolean flag indicating whether the field is required for the payout process.

  • i18n: The localization module to translate field names and error messages based on the language code.

  • getProviderByXML: A method to retrieve the currency provider object based on the XML endpoint provided.

  • return: The array of field objects required for the payout process, based on the currency provider and XML endpoint.

API Query

The class should contain methods to interact with the payout API, such as making requests to the API, handling responses, and processing data. These methods should be asynchronous and return promises to handle asynchronous operations.

API Query Method

The api_query method is a generic method for making requests to the payout API. It accepts the method name and data as parameters, constructs the request, signs the request with the API secret, and sends the request to the API. The method returns a promise that resolves with the response data or rejects with an error.

async api_query(method_name, data) {
  const API_HOST = 'https://api.example.com/payout';
  const isDebugMode = this.conf.debug || (await config.get('debug') === "1");
  const API_KEY = this.conf.apiKey || await config.get('apiKey');
  const API_SECRET = this.conf.apiSecret || await config.get('apiSecret');

  if (!API_KEY || !API_SECRET) {
    return Promise.reject({ message: 'Config for PAYOUT_NAME is not installed.', code: 6000011 })
  }

  let method = method_name.split(':');
  let typeMethod = 'POST';

  if (method.length === 2) {
    typeMethod = method[0];
    method = method[1];
  } else {
    method = method[0];
  }
  const unixTimeRequest = Date.now();

  const sign = crypto
            .createHmac('sha256', API_SECRET)
            .update(`${unixTimeRequest}${JSON.stringfy(data)}`)
            .digest('hex');

  const optReq = {
    url: API_HOST + method,
    method: typeMethod,
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + API_KEY,
      Signatur: sign,
    },
    data,
  };

  if (isDebugMode) console.time("{Payout} -> PAYOUT_NAME -> api: " + API_HOST + method_name + " - nonce: " + unixTimeRequest + " took: ");

  return axios(optReq)
    .then((res) => res.data)
    .then((body) => {
      if (isDebugMode) console.timeEnd("{Payout} -> PAYOUT_NAME -> api: " + API_HOST + method_name + " - nonce: " + unixTimeRequest + " took: ");

      if (!body)
          return Promise.reject({ message: "Error response is empty -> PAYOUT_NAME",code: 5000341 });
        
      return body.data || body;
    })
    .catch((error) => {
      console.error("{Payout} -> PAYOUT_NAME -> error :", error.data ? error.data.message : error.message);
      
      if (error && error.message && error.code && !error.response)
        return Promise.reject({ message: error.message, code: error.code });
      
      const resultError = error.response || error;
      if (
        resultError &&
        resultError.data &&
        resultError.data.message &&
        resultError.status
      )
        return Promise.reject({
          message: resultError.data.message,
          statusCode: resultError.status,
          code: 600000500,
        });
      return Promise.reject({
        message: `Unknown error: ${error.message} - ${data.request}`,
        code: 5000342,
      });
    });
}

Parameters:

  • method_name: The name of the API method to call, including the HTTP method (e.g., POST:/withdraw).

  • data: The data to send in the API request, such as payout details or order information.

  • API_HOST: The base URL for the payout API.

  • isDebugMode: A boolean flag to determine whether debug mode is enabled for the payout plugin.

  • API_KEY: The API key for authenticating requests to the payout API.

  • API_SECRET: The API secret for signing requests to the payout API.

  • method_name: The name of the API method to call, which includes the HTTP method and endpoint.

  • data: The data to send in the API request.

  • typeMethod: The HTTP method to use for the API request, defaulting to POST.

  • unixTimeRequest: The current Unix timestamp for the request.

  • sign: The signature for the request, generated using crypto.createHmac with the API secret and request data.

  • optReq: The options object for the axios request, including the URL, method, headers, and data.

  • return: A promise that makes the API request, handles the response, and resolves with the data or rejects with an error.

  • Error Handling: The method includes error handling logic to catch and handle errors from the API request, including logging errors and returning structured error objects.

  • Debug Logging: The method includes debug logging to measure the time taken for the API request and log the request details.

Transfer Method

The transfer method is responsible for processing a payout request, including validating the input data, making the API request to the payout provider, checking the balance, and handling the response. This method should return a promise that resolves with the payout details or rejects with an error if the payout fails.

The transfer method should accept an order object as a parameter, which contains the details of the payout request, such as the order ID, amount, currency, and recipient information.

This method should perform the following steps:

  • Validate the order object to ensure it contains the required fields.

  • Retrieve the currency provider based on the currency XML.

  • Check the available balance for the payout/witdraw.

  • Determine the recipient information based on the currency type (crypto address or card number).

  • Make the API request to the payout provider to process the payout.

  • Handle the response from the payout provider and return the payout details.

Currency Provider

The getProviderByXML method is responsible for retrieving the currency provider object based on the XML endpoint provided in the order object. This method should return the currency provider object, which contains information about the currency and type.


getProviderByXML(xml) {
  if (!xml) return null;

  if (xml.indexOf('CARDUAH') === 0) {
    return {
      currency: 'UAH',
      type: 'Card',
    };
  }
  if (xml.indexOf('USDTERC20') === 0) {
    return {
      type: 'crypto',
      currency: 'USDT',
      network: 'ERC20',
    };
  }
  return null;
}
  • XML is used to determine the currency provider for the order, it is a standard representation of the currency. Sites like jsons.info and bestchange.com can be used to get the XML of the currency.

  • return: The currency provider object containing the currency and type information based on the XML endpoint provided.

    • currency: The currency symbol for the payout.

    • type: The type of payout, such as crypto or card.

    • network: The network type for crypto payouts, such as ERC20 or BEP20.


  /**
   * @param order {object} - order
   * @param order.uid {number} - order uid
   * @param order.outAmount {number} - order outAmount
   * @param order.outXML {string} - order xml currency out
   * @param order.outSymbol {string} - order symbol currency out
   * @param order.outValues {object} - order values param
   * @returns {Promise<*>}
   */
  transfer(order) {
    return (async () => {
      if (!order.uid)
        return Promise.reject('Error uid order.');
      if (!order.outXML)
        return Promise.reject('Error xml order.');
      if (!order.outAmount || isNaN(order.outAmount))
        return Promise.reject('Error outAmount order.');

      const currencyProvider = this.getProviderByXML(order.outXML);
      if (!currencyProvider)
        return Promise.reject('Error currency provider order.');
      
      const balance = await this.api_query("GET:/balance", {}).then(r => Number(r.balance));
      if (new BigNumber(order.outAmount).gt(balance)) {
        return Promise.reject('Error available_balance is low for transfer. ' + balance + " < " + Number(order.outAmount));
      }

      // If crypto
      const cryptoAddress = order.outValues.account_crypto;
      if (!cryptoAddress)
        return Promise.reject('Error crypto address order.');

      // If card
      const cardNumber = order.outValues.card_number;
      if (!cardNumber)
        return Promise.reject('Error card number order.');

      const transaction = await this.api_query("POST:/transfer", {
        amount: order.outAmount,
        currency: currencyProvider.currency,
        // If crypto
        address: cryptoAddress,
        // Or card
        card_number: cardNumber,

        merchant_transaction_id: String(order._id),
        webhook_url: `${process.env.API_URL}payout_call/${id_key}/url_final/`
      });

      if (!transaction || !transaction.transaction_id)
        return Promise.reject('Error transaction order.');

      return {
        ok: 2,
        transaction: String(transaction.transaction_id),
        amount: order.outAmount,
        currency: order.outSymbol.toUpperCase(),
        to: transaction.address,
      };
    })();
  }

  module.exports = PAYOUT_NAME;
  • order: The order object containing the details of the payout request, such as the order ID, amount, currency, and recipient information.

  • currencyProvider: The currency provider object for the payout currency, which contains information about the currency and type.

  • balance: The available balance for the payout provider, retrieved using the GET:/balance API method.

  • Receiver Information:

    • cryptoAddress: The recipient's crypto address for the payout, retrieved from the order values.

    • cardNumber: The recipient's card number for the payout, retrieved from the order values.

  • transaction: The transaction object returned from the POST:/transfer API method, which contains the transaction ID and recipient information.

  • webhook_url: The webhook URL for the payout provider to send notifications about the payout status.

  • return: A promise that resolves with the payout details, including the transaction ID, amount, currency, and recipient information, or rejects with an error if the payout fails.

    • ok: The status code for the payout.

      • 2 indicates a successful payout but wait approve (by payout-system).

      • 1 indicates a successful payout.

    • transaction: The transaction ID for the payout.

    • amount: The amount of the payout.

    • currency: The currency symbol for the payout.

    • to: The recipient address or card number for the payout.

  • Error Handling: The method includes error handling logic to check for missing or invalid order details, insufficient balance, and errors from the API request.

  • Transaction Details: The method constructs the transaction object with the necessary details for the payout, including the amount, currency, recipient information, and webhook URL.

Optional Cron Method

If the payout system doesn't support the webhook, you can use the initCron method to check the status of pending payouts and update the payout status based on the response from the payout provider. This method is called periodically by the system to process pending payouts and handle any updates or errors.

initCron({ db, updateHistory }) {
  return {
    callWhenInit: true,
    schedule: '*/45 * * * * *',
    fn: async () => {
      const waitTxs = await db.OrderPayoutTxs.countDocuments({
        active: true,
        payoutModule: id_key,
      });
      if (waitTxs <= 0) return;
      
      const payoutTxs = await db.OrderPayoutTxs
        .find({
          active: true,
          payoutModule: id_key,
        })
        .populate("order")
        .lean();

      await Promise.mapSeries(payoutTxs, async (tx) => {
        const transfer = await this.api_query(`GET:/transaction/${tx.transaction}`, {});
        if (!transfer || !transfer.uuid)
          return null;
        const orderPayout = tx.order;
        if (!orderPayout) {
          await db.OrderPayoutTxs.updateOne({ _id: tx._id }, { $set: { active: false } });
          return null;
        }
        if (orderPayout.status !== 'inProgressPayout') {
          await db.OrderPayoutTxs.updateOne({ _id: tx._id }, { $set: { active: false } });
          console.error('Order found but status is not inProgressPayout. PAY_TX_ID:', tx._id);
          return null;
        }


        if (transfer.status === "DONE") {
          await updateHistory({
            orderId: orderPayout._id,
            oldStatus: orderPayout.status,
            status: 'done',
            payoutStatus: 'done',
            orderNote: String(transfer.uuid),
            comment: 'Success payout: (' + String(transfer.uuid) + ') Amount: ' + String(transfer.amount) + (transfer.uuid ? ', TxId:' + String(transfer.uuid) : ".")
          })
            .catch(err => {
              console.error("Error update status order (cron->PAYOUT_NAME): ", err);
              return null;
            });
          await db.OrderPayoutTxs.updateOne({ _id: tx._id }, { $set: { active: false } });
          return 1;
        }

        if (transfer.status === "CANCELED") {
          await updateHistory({
            orderId: orderPayout._id,
            oldStatus: orderPayout.status,
            status: 'errorPayout',
            payoutStatus: 'error',
            comment: 'Error payout: (' + String(transfer.status) + ') Amount: ' + String(transfer.amount) + (transfer.uuid ? ', TxId:' + String(transfer.uuid) : ".")
          }).catch(err => {
            console.error("Error update status order (cron->PAYOUT_NAME): ", err);
            return null;
          });
          await db.OrderPayoutTxs.updateOne({ _id: tx._id }, { $set: { active: false } });
          return 2;
        }

        return null;
      });

      return "ok";
    }
  }
}

Parameters:

  • db: The database object for interacting with the database, so that any order that hasn't been completed can be updated.

  • updateHistory: A function to update the order history with the payout status and details. Any error that occurs during the update process is caught and logged. Accepts an object with the following properties:

    • orderId: The ID of the order to update.

    • oldStatus: The previous status of the order.

    • status: The new status of the order.

    • payoutStatus: The payout status of the order.

    • orderNote: A note to add to the order history.

    • comment: A comment to add to the order history.

  • callWhenInit: A boolean flag to indicate whether the cron job should be called when the system initializes.

  • schedule: The cron schedule for running the job, specified in cron format.

  • fn: The function to execute when the cron job runs. This function retrieves pending payouts, checks their status with the payout provider, and updates the order history based on the response.

  • waitTxs: The count of number of pending payouts to process, when it is zero, the function returns.

  • payoutTxs: The list of pending payout transactions to process, retrieved from the database and populated with order details.

    • Iterate: over each pending payout transaction and check its status with the payout provider using the GET:/transaction API method.

  • transfer: The response from the payout provider, which contains the status and details of the specific payout transaction.

  • orderPayout: The order associated with the payout transaction, used to update the order status and history. If the order is not found or has an incorrect status, the transaction is marked as inactive.

  • transfer.status: The status of the payout transaction, which can be DONE for successful payouts or CANCELED for failed payouts or can be any other status depending on the payout provider.

  • updateHistory: The function to update the order history with the payout status and details, including the transaction ID, amount, and status. And comment about the payout status for the admin.

  • Update Order Status: The order status is updated based on the payout status, and the payout transaction is marked as inactive.

  • return: The function returns a promise that resolves with the status of the payout processing, such as ok for successful processing of current payouts.

    • 1 indicates a successful payout.

    • 2 indicates a failed payout.

    • null indicates no action taken for the payout transaction.

  • Error Handling: The method includes error handling logic to catch and log any errors that occur during the processing of pending payouts.

Optional Callbacks

The callbacks.js file contains the callback functions for the payout plugin. These functions are called by the system to handle various events, such as successful payouts, failed payouts, and webhook notifications from the payout provider.

Final URL Callback

The url_final function is called by the payout provider to notify the system about the final status of the payout. This function should update the order status and history based on the payout status received from the provider.

const id_key = __dirname.split('/').slice(-1)[0];
const config = require('../../../modules/config/payout.config.js')(id_key);
const axios = require('axios');
const crypto = require('node:crypto');
const db = require('../../../db/index.js');
const updateHistory = require('../../../modules/updateHistory.js');

/**
 * If the payout provider supports an endpoint to get the transaction status, the `api_query` method can be defined and used to check the status of the transaction.
 */

// Implement the `api_query` method here

module.exports = {
  /** @url /payout_call/payout_name/url_final/ */
  url_final: async (req, res, param) => {
    res.set('Content-Type', 'text/plain');
    if (req.METHOD !== "POST") {
      res.writeHead(400);
      res.end("error method req");
      return;
    }

    if (!param.post || !param.post.id || !param.post.status) {
      console.error("{Payout} -> PAYOUT_NAME -> callback final. No Data");
      res.writeHead(400);
      res.end("No Data");
      return;
    }

    const { id } = param.post;

    const transfer = await api_query("POST:/api/payouts/one", {
      payoutId: id,
    }).catch(err => {
      console.error("{Payout} -> PAYOUT_NAME -> callback final. Error get payout: ", err);
      res.writeHead(400);
      res.end("Error get payout");
      return;
    });

    if (!transfer) return;

    const { transactionId, status } = transfer;
    
    const payoutTxs = await db.OrderPayoutTxs.findOne({
      transaction: id,
      active: true,
      payoutModule: id_key,
    }).populate("order");

    if (!payoutTxs) {
      console.error("{Payout} -> PAYOUT_NAME -> callback final. No order found");
      res.writeHead(400);
      res.end("No order found");
      return;
    }

    const orderPayout = payoutTxs.order;

    if (orderPayout.status !== 'inProgressPayout') {
      await db.OrderPayoutTxs.updateOne({ _id: payoutTxs._id }, { $set: { active: false } });
      console.error('{Payout} -> PAYOUT_NAME -> callback final. Order found but status is not inProgressPayout. PAY_TX_ID:', payoutTxs._id);
      res.writeHead(200);
      res.end("alreadyProcessedTx");
      return null;
    }

    if (status === "DONE") {
      await updateHistory({
        orderId: orderPayout._id,
        oldStatus: orderPayout.status,
        status: 'done',
        payoutStatus: 'done',
        orderNote:  transactionId,
        comment: 'Success payout: (' + transfer.id + ') Amount: ' + transfer.transferAmount + ', Hash:' +  transactionId
      })
        .catch(err => {
          console.error("Error update status order (callback->PAYOUT_NAME): ", err);
          return null;
        });
      await db.OrderPayoutTxs.updateOne(
        { _id: payoutTxs._id },
        { $set: { active: false } }
      );
      res.writeHead(200);
      res.end("ok");
      return;
    } else if (status === "FAILED") {
      await updateHistory({
        orderId: orderPayout._id,
        oldStatus: orderPayout.status,
        status: 'errorPayout',
        payoutStatus: 'error',
        comment: 'Error payout: (' + transfer.id + ')' +  transactionId
      }).catch(err => {
        console.error("Error update status order (callback->PAYOUT_NAME): ", err);
        return null;
      });
      await db.OrderPayoutTxs.updateOne(
        { _id: payoutTxs._id },
        { $set: { active: false } }
      );
      res.writeHead(200);
      res.end("errorPayout");
      return;
    }
    res.writeHead(200);
    res.end("skip");
    return;
  }
};
  • imports: The necessary modules and functions for handling the callback, such as the configuration, axios, crypto, database, and updateHistory.

  • api_query: The method to make requests to the payout provider's API to retrieve the transaction status.

Parameters:

  • req: The express request object containing the incoming data from the merchant's API.

  • res: The express response object used to send a response back to the merchant's API.

  • param: The parameters object containing the parsed request data, such as post for POST data and get for query parameters.

  • POST Method Check: The function checks if the request method is POST and returns an error response if it is not.

  • Data Validation: The function checks if the required data, such as the transaction ID and status, is present in the request data.

  • Transfer Details: The function retrieves the transaction details from the payout provider using the POST:/api/payouts/one API method which contains the transaction ID and status.

  • payoutTxs: The pending payout transaction associated with the transaction ID, retrieved from the database and populated with the order details.

  • orderPayout: The order associated with the payout transaction, has the status inProgressPayout if not found or has an incorrect status, the transaction is marked as inactive.

  • Update Order Status: The order status is updated based on the payout status, if the payout is successful, the status is set to done, if the payout fails, the status is set to errorPayout.

  • updateHistory: A function to update the order history with the payout status and details. Any error that occurs during the update process is caught and logged. Accepts an object with the following properties:

    • orderId: The ID of the order to update.

    • oldStatus: The previous status of the order.

    • status: The new status of the order.

    • payoutStatus: The payout status of the order.

    • orderNote: A note to add to the order history.

    • comment: A comment to add to the order history.

  • Return Response: The function sends a response back to the payout provider with the status of the payout processing, such as ok for successful processing of the payout.

    • 200 indicates a successful response.

    • 400 indicates an error response.

  • Error Handling: The function includes error handling logic to catch and log any errors that occur during the processing of the payout callback.

Notes

  • The payout plugin should be thoroughly tested to ensure that it sends the funds to the correct recipient and handles errors gracefully.

  • The plugin should be well-documented with clear instructions on how to configure and use it.

  • The plugin should be secure and follow best practices for handling sensitive data such as API keys and secrets.

  • The plugin should be optimized for performance to ensure fast and reliable payouts.

  • The plugin works either with callback or cron job to check the status of pending payouts and update the payout status based on the response from the payout provider.

  • The transfer method is called by the admin interface to process a payout request, and it displays the payout details and comments to the admin wether the payout is successful or failed.

  • The getFields method returns to the client the fields required for the given currency, such as the recipient's address, amount, and any additional information required by the payout API.

  • The url_final callback function is called by the payout provider to notify the system about the final status of the payout, and it updates the order status and history based on the payout status received from the provider.

  • The initCron method is used to periodically check the status of pending payouts and update the payout status based on the response from the payout provider. This method is called by the system to process pending payouts and handle any updates or errors.

Last updated