4.2. Script types

4.2.1. Library

Libraries are scripts which are included by other scripts, in order to reuse code, and are never used directly by other functionality in Cyclos.

Each script (including other libraries) can have any number of libraries as dependencies. However circular dependencies between libraries (for example, A depends on B, which depends on C, which depends on A) are forbidden (validated when saving a library).

The order in which the code on libraries is included in the final code respects the dependencies, but doesn't guarantee ordering between libraries in the same level. For example, if there are both C and B libraries which depend on A, it is guaranteed that A is included before B and C, but either B or C could be included right after A. So, in the example, your code shouldn't rely that B comes before C. In this case, the library C should depend on B to force the A, B, C order.

Contrary to other script types, libraries don't have bound variables per se: the bindings will be the same as the script including the library.

Also, as libraries are just included in other scripts, no direct examples are provided here. The provided example scripting solutions, however, use libraries.

4.2.2. Custom field validation

These scripts are used to validate a custom field value. The field can be of any type (users, advertisements, user records, transactions and so on). The script code has the following variables bound (besides the default bindings)

The script should return one of the following:

  • A boolean, indicates that the value is either valid / invalid. When invalid, the general "<Field name> is invalid" error will be displayed;
  • A string, means the field is invalid, and the string is the error message. To concatenate the field name directly, use the {0} placeholder, like: "{0} has an unexpected value";
  • Any other result will be considered valid.

4.2.2.1. Examples

E-mail

To have a custom field which is validated as an e-mail, use the following script:

import org.apache.commons.validator.routines.EmailValidator

return EmailValidator.getInstance().isValid(value)

IBAN account number

To validate an IBAN account number as a custom field, the following script can be used:

import org.apache.commons.validator.routines.checkdigit.IBANCheckDigit

return IBANCheckDigit.IBAN_CHECK_DIGIT.isValid(value.replaceAll("\\s", ""))

CPF Validation

In Brazil, people are identified by a number called CPF (Cadastro de Pessoas Fisicas). It has 2 veryfing digits, which have a known formula to calculate. Here's the example for validating it in Cyclos:

import static java.lang.Integer.parseInt

def boolean validateCPF(String cpf) {
    // Strip non-numeric chars
    cpf = cpf.replaceAll("[^0-9]", "")

    // Obvious checks: needs to be 11 digits, and not all be the same digit
    if (cpf.length() != 11 || cpf.toSet().size() == 1) {
        return false
    }

    int add = 0
    // Check for verifier digit 1
    for (int i = 0; i < 9; i++) add += parseInt(cpf[i]) * (10 - i)
    int rev = 11 - (add % 11)
    if (rev == 10 || rev == 11) rev = 0
    if (rev != parseInt(cpf[9])) return false

    add = 0;
    // Check for verifier digit 2
    for (int i = 0; i < 10; i++) add += parseInt(cpf[i]) * (11 - i)
    rev = 11 - (add % 11)
    if (rev == 10 || rev == 11) rev = 0
    if (rev != parseInt(cpf[10])) return false

    return true
}

return validateCPF(value)

4.2.3. Dynamic custom field handling

These scripts are used to generate the possible values for custom fields of type 'dynamic selection'. Each possible value is an instance of org.cyclos.model.system.fields.DynamicFieldValueVO. The field can be of any type (users, advertisements, user records, transactions and so on).

The script code has the following variables bound (besides the default bindings):

Also, depending on the custom field nature, there are the following additional bindings:

User (profile) fields:

Advertisement fields:

Record fields:

Transaction fields:

Custom operation fields:

Dynamic document fields:

In all cases, the script must return either one or a collection of:

  • List of array of Strings: In this case, each element will have only values, and the corresponding labels will be the same values.
  • org.cyclos.model.system.fields.DynamicFieldValueVO (or compatible object / Map): The dynamic field value, containing a value (the internal value) and a label (the display value). The value must be not blank, or an error will be raised. If the label is blank, will show the same text as the value. Also, the first dynamic value with 'defaultValue' set to true will show up by default in the form.

4.2.3.1. Examples

User profile field – values depending on the user group

This examples returns distinct values according to the user group. It should be used by an user custom field (also called profile fields).

import org.cyclos.model.system.fields.DynamicFieldValueVO

def values = []
// Common values
values << new DynamicFieldValueVO("common1", "Common value 1")
values << new DynamicFieldValueVO("common2", "Common value 2")
values << new DynamicFieldValueVO("common3", "Common value 3")
if (user.group.internalName == "business") {
    // Values only available for businesses
    values << new DynamicFieldValueVO("business1", "Business value 1")
    values << new DynamicFieldValueVO("business2", "Business value 2")
    values << new DynamicFieldValueVO("business3", "Business value 3")
} else if (user.group.internalName == "consumer") {
    // Values only available for consumers
    values << new DynamicFieldValueVO("consumer1", "Consumer value 1")
    values << new DynamicFieldValueVO("consumer2", "Consumer value 2")
    values << new DynamicFieldValueVO("consumer3", "Consumer value 3")
}
return values

4.2.4. Account number generation

This kind of script is responsible for generating account numbers, in case more control than the default (random generation) is needed. The script code has the following variables bound (besides the default bindings):

The script should return a string, which should match the mask set in the configuration (if any). If the script returns null or a blank string, no number is assigned for that account.

The script doesn't need to check if the account number already exists. This is done internally. If the number is already used, the script is called again (up to 10 times, then, an error is raised).

4.2.4.1. Examples

Controlling the prefix according to the currency and user group

In this example, the mask ##/####### is expected for the account number. The prefix is composed of 2 digits:

  • The first one is 0 if the currency is unit, or 1 otherwise.
  • The second one is 0 for system, 1 for business, 2 for consumers of 9 otherwise.

The rest are 7 random digits.

import org.apache.commons.lang3.RandomStringUtils
import org.cyclos.entities.users.User

// Either unit or euro
String prefix = type.currency.internalName == 'internal_units' ? '0' : '1'

if (owner instanceof User) {
    switch (owner.group.internalName) {
        case 'business':
            prefix += '1'
            break
        case 'consumers':
            prefix += '2'
            break
        default:
            prefix += '9'
    }
} else {
    prefix += '0'
}

return prefix + "/" + RandomStringUtils.randomNumeric(7)

4.2.5. Account fee calculation

These scripts are used to calculate the amount of an account fee (a fee which is charged periodically or manually over many accounts, according to the 'charged account fees' setting in member products). The script code has the following variables bound (besides the default bindings):

The script should return a number, which will be rounded to the currency's decimal digits. If null or zero is returned, the fee is not charged.

4.2.5.1. Examples

Charge a different amount according to the user rank

This example allows choosing a distinct account fee amount based on a profile field of the paying user. It is assumed a custom field of type single selection with the internal name rank. It should have 3 possible values, with internal names bronze, silver and gold.

// Depending on an user custom field, we'll pick the fee amount
def amounts = [bronze: 10, silver: 7, gold: 5]
def user = scriptHelper.wrap(account.owner)
def rank = user.rank?.internalName ?: "bronze"
return amounts [rank]

4.2.6. Transfer fee calculation

These scripts are used to calculate the amount of a transfer fee (a fee triggered by another transfer). The script code has the following variables bound (besides the default bindings):

The script should return a number, which will be rounded to the currency's decimal digits. If null or zero is returned, the fee is not charged.

4.2.6.1. Examples

Charging a fee according to an user profile field

This example allows choosing a distinct fee amount based on a profile field of the paying user. It is assumed a custom field of type single selection with the internal name rank. It should have 3 possible values, with internal names bronze, silver and gold. The script then chooses a different percentage according to the user rank.

if (transfer.fromSystem) {
    // Only charge users
    return 0
}

// Depending on an user custom field, we'll pick the fee amount
def percentages = [bronze: 0.07, silver: 0.05, gold: 0.02]
def from = scriptHelper.wrap(transfer.fromOwner)
def rank = from.rank?.internalName ?: "bronze"
def percentage = percentages[rank]
return transfer.amount * percentage

4.2.7. Transfer status handling

These scripts are used to determine to which status(es) a transfer may be set after the current status. By default, if no script is used, the possible next statuses (as configured in the transfer status details page) will be available. Using a script, however, allows using finer-grained controls. For example, an specific status could be allowed only by specific administrators, or only under special conditions (for example, checking the account balance or any other condition).

The script code has the following variables bound (besides the default bindings):

The script should return one of the following:

  • A single org.cyclos.entities.banking.TransferStatus (only that status is available as next);
  • An array / list / iterator of org.cyclos.entities.banking.TransferStatus (all are available as next, possibly empty);
  • Null – assumes the default behavior: the possible next configured in the status are assumed.

4.2.7.1. Examples

Restricting a specific status for administrators

In this example, any user can change a transfer status in a given flow. However, only administrators can set a transfer to the status with internal name finished.

// Only administrators can set the status to finished
return status.possibleNext.findAll { st ->
    sessionData.admin || st.internalName != "finished"
}

4.2.8. Password handling

These scripts are used to check passwords. In order to use them, the password type's password mode needs to be "Script". The script code has the following variables bound (besides the default bindings):

The script should return a boolean, indicating whether the password is ok or not.

4.2.8.1. Examples

Matching passwords to the script parameters

This is a very simple example, which checks for passwords according to the script parameters. The parameters can be set either in the script itself or in the password type. This example is very insecure, and shouldn't be used in production. Normally, scripts to check passwords would connect to third party applications, but this is just a very basic example.

// Just read the password value from the script parameters
return scriptParameters[user.username] == password

4.2.9. Extension points

These scripts are used on extension points (user, user record, transfer, …), and are attached to specific events (create, update, remove, chargeback, …). The extension point scripts have 2 functions:

  • The data has already been validated, but not saved yet. In this function, we know that the data entered by users is valid, but the main event has not been saved yet.

  • The data has been saved, but not committed to database yet. For example, if the script code throws an Exception, the database transaction will be rolled-back, and no data will be persisted.

Here are some example scenarios for performing custom logic, or integrating Cyclos with external systems using extension points:

  • limit. When an user is performing a payment, an extension point of type transaction could be used, in the function invoked after validation, to check the current balance. It the balance is not enough for the payment and the user has credit limit, a payment from a system account could be done automatically to the user, completing the amount for the payment.

  • A XA transaction could be done with an external system by creating data in the external database in the function which runs after validating, then preparing the commit in the function after the data is saved, and finally registering both a commit and a rollback listener (see the ScriptHelper in default bindings) to either commit or rollback the prepared transaction.

  • It is also possible to 'bind' Cyclos entities with extension points. For example a payment could create a new user record of a specific type and set some values in the record. When a user record value is changed this could trigger another action, for example changing the (bookkeeping) status of a payment.

  • A simple notification of performed payments could be implemented by registering a commit listener (see the ScriptHelper in default bindings) to implement the notification.

  • The profile information of an user needs to be mirrored in an external system. In this case, an user extension point, with the create / update events can be used to send this information. Additional information on addresses and phones can use the same mechanism (they are different extension points). Finally, a change status event for users, to the status REMOVED indicates that the user has been removed.

  • There could be payment custom fields which are not filled-in by users when performing payments, but by extension points of type transaction. Payment custom fields may be configured to not show up in the form, only automatically via extension points.

  • An extension point on a new Cyclos avertisment could publish the advertisment as well in an third party system.

These are just some examples. There are many possible uses for the extension points. In the future we will publish usefull extension points at this site.

All extension points have the following additional variables bound to its execution:

The following types of extension points exist:

4.2.9.1. User extension point

Extension points which monitor events on users. Additional bindings:

Events:

  • create: An user is being registered. IMPORTANT: When e-mail validation is enabled, the user will be pending until confirming the e-mail. If you have e-mail confirmation enabled, this event might not be what you need, but activate instead.
  • activate: An user is being activated for the first time. For example, if e-mail validation is enabled, after the user confirming the e-mail address this event will be triggered. However, the initial status for users (set in group) might be, for example, disabled. In that case, only when the user is first activated this event will be triggered.
  • update: An user profile (name, username, e-mail or custom fields) is being edited. Additional bindings:
  • changeGroup: The user's group is being changed.
  • changeStatus: The user's status is being changed. Argument Map:

4.2.9.2. Address extension point

Extension points which monitor events on addresses. Additional bindings:

Events:

  • create:An address is being created.
  • update: An address is being updated. Additional bindings:
    • currentCopy: A detached copy of the address being edited, as org.cyclos.entities.users.UserAddress
  • delete: An address is being deleted.

4.2.9.3. Phone extension point

Extension points which monitor events on user phones. Additional bindings:

Events:

  • create: A phone is being created.
  • update: A phone is being updated. Additional bindings:
    • currentCopy: A detached copy of the phone being edited, as org.cyclos.entities.users.Phone
  • delete: A phone is being deleted.

4.2.9.4. User record extension point

Extension points which monitor events on user records. Additional bindings:

Events:

  • create: An user record is being created.
  • update: An user record is being created. Additional bindings:
  • delete: An user record is being created.

4.2.9.5. Advertisement extension point

Extension points which monitor events on advertisements. Additional bindings:

Events:

  • create: An advertisement is being created.
  • update: An advertisement is being updated. Additional bindings:
  • delete: An advertisement is being deleted.

4.2.9.6. Transaction extension point

Extension points which monitor events on performed transactions.

The following additional bindings are available for both preview and confirm events:

Events:

4.2.9.7. Transaction authorization extension point

Extension points which monitor transaction authorization actions. Additional bindings:

Events:

  • authorize: The transaction is being authorized. Be careful: there might be more authorization levels which need to be authorized before the transaction is finally processed. Additional bindings:
  • deny: The transaction is being denied by the authorizer.
  • cancel: The transaction is being canceled by the performed.

4.2.9.8. Transfer extension point

Argument Map (common for all events):

  • transfer: The transfer being affected.

Events:

4.2.9.9. Examples

Granting extra credit (on demand) before payments

This example allows, with a custom profile field, to define an extra credit limit the user can use on demand. When performing a payment, if the available balance is not enough, a payment is performed from a system account to the user, up to the limit specified in that profile field. Once the payment is done, the profile field is subtracted. This example expects the system account to have the internal name debit_units, and it should have a payment transfer type to the user account. That payment transfer type should have the internal name extra_credit. Finally, the custom profile field needs to have the internal name availableCredit, and needs to be of type decimal, and enabled for the user. Then create an extension point of type Transaction, enabled and for the confirm event. This example only works for payments without fees.

import org.cyclos.entities.banking.Account
import org.cyclos.entities.banking.PaymentTransferType
import org.cyclos.entities.banking.SystemAccountType
import org.cyclos.model.banking.accounts.SystemAccountOwner
import org.cyclos.model.banking.transactions.PerformPaymentDTO
import org.cyclos.model.banking.transfertypes.TransferTypeVO

// Only process direct payments. Scheduled payments are skipped
if (!(performTransaction instanceof PerformPaymentDTO)) {
    return
}

// Get the available credit as a profile field
def payer = scriptHelper.wrap(fromOwner)
BigDecimal availableCredit = payer.availableCredit?.abs()
if (availableCredit == null || availableCredit < 0.01) {
    // Nothing to do - no available credit
    return
}

// Get the account and balance
Account account = accountService.load(fromOwner, paymentType.from)
BigDecimal availableBalance = accountService.getAvailableBalance(account, null)
BigDecimal needs = performTransaction.amount - availableBalance
if (needs > 0 && needs <= availableCredit) {
    // Needs some extra credit, and has it available - make a payment from system
    // Find the system account and payment type
    SystemAccountType systemAccountType = entityManagerHandler.find(
            SystemAccountType, "debit_units")
    PaymentTransferType paymentType =  entityManagerHandler.find(
            PaymentTransferType, "extra_credit", systemAccountType)
    PerformPaymentDTO credit = new PerformPaymentDTO()
    credit.from = SystemAccountOwner.instance()
    credit.to = fromOwner
    credit.type = new TransferTypeVO(paymentType.id)
    credit.amount = needs
    paymentService.perform(credit)
    // Now there should be enough credit to perform the payment

    // Update the user available credit
    payer.availableCredit -= needs
}

Send an e-mail on every payment

This example allows, for the selected payment types in the extension point details, to send an e-mail to an speficic address.

import javax.mail.internet.InternetAddress

import org.cyclos.model.ValidationException
import org.cyclos.server.utils.MessageProcessingHelper
import org.springframework.mail.javamail.MimeMessageHelper

// Get the e-mail subject and body
def tx = scriptHelper.wrap(transaction)
def vars = [
    payer: tx.fromOwner.name,
    amount: formatter.format(tx.currencyAmount),
    date: formatter.formatAsDate(new Date()),
    time: formatter.formatAsTime(new Date())
]
def subject = MessageProcessingHelper.processVariables(scriptParameters.subject, vars)
if (subject == null || subject.empty) {
    throw new ValidationException("Missing the 'subject' script parameter")
}
def body = MessageProcessingHelper.processVariables(scriptParameters.message, vars)
if (body == null || body.empty) {
    throw new ValidationException("Missing the 'message' script parameter")
}
def toEmail = tx.email
def fromEmail = sessionData.configuration.smtpConfiguration.fromAddress
def sender = mailHandler.mailSender

// Send the message after commit, so we guarantee the transaction is persisted
// when the e-mail is sent
scriptHelper.addOnCommit {
    def message = sender.createMimeMessage()
    def helper = new MimeMessageHelper(message)
    helper.to = new InternetAddress(toEmail)
    helper.from = new InternetAddress(fromEmail)
    helper.subject = subject
    helper.text = body
    // Send the message
    sender.send message
}

4.2.10. Custom operations

These scripts are invoked when an user runs a custom operation. A custom operation is configured to return different data types, and the script must behave accordingly (see System – Operations for more details).

Custom operations can have different scopes:

  • System: Those are executed by administrators (with granted permissions), directly from the main menu.
  • User: Custom operations which are related to an user, and can either be executed by the own user (with granted permissions), from the main menu or run by administrator or brokers (also, with granted permissions) when viewing the user profile. In both cases, the custom operation needs to be enabled to users via member products. For example, there might be operations which applies only to businesses, not consumers, and even administrators with permission to run them shouldn't be able to run them over consumers. It is enforced that administrators / brokers will only be able to run custom operations over users they manage.
  • Menu: These custom operations are executed by a custom menu entry. This is the only possible custom operation scope that can be run by guests. A classical example of this is a "Contact us" page.
  • Internal: An internal custom operation is meant to be executed when the user clicks a row in another custom operation which returns a table with results.
  • Advertisement: Custom operations which are executed over an advertisement.

Bound variables:

  • customOperation: The org.cyclos.entities.system.CustomOperation
  • user: The org.cyclos.entities.users.User. Only present if the custom operation's scope is user.
  • ad: The org.cyclos.entities.marketplace.BasicAd. Only present if the custom operation's scope is advertisement.
  • inputFile: The org.cyclos.model.utils.FileInfo. Only present if the custom operation is configured to accept a file upload, and if a file was selected.
  • formParameters: A java.util.Map<String, Object>, keyed by the form field internal name. The value depends on the field type. Could be a string, a number, a boolean, a date, a org.cyclos.entities.system.CustomFieldPossibleValue or a collection of org.cyclos.entities.system.CustomFieldPossibleValue.
  • pageContext: The org.cyclos.model.system.operations.CustomOperationPageContext indicating if an operation which returns a result page is being called directly, to print to PDF or to export as CSV.
  • currentPage: An integer indicating the current page, when getting paged results. Starts with zero. Only available if the result type is result page.
  • pageSize: An integer indicating the requested page size when getting paged results. Only available if the result type is result page.
  • returnUrl: Only if the custom operation return type is external redirect. Contains the url (as string) which Cyclos expects the external site to redirect the user after the operation completes.
  • parameterStorage: Only if the custom operation return type is external redirect. Contains an ObjectParameterStorage which is shared in both the first script and the callback handling script. This object is enhanced with propertyMissing methods, to support "syntactic sugar" on Groovy scripts, like parameterStorage.name = value. When this form is used, it is assumed that the input / output are plain strings.
  • externalRedirectExecution: Only if the custom operation return type is external redirect. Contains the ExternalRedirectExecution which stores the context for this execution.
  • request: The org.cyclos.model.utils.RequestInfo. Only if the custom operation return type is external redirect. Contains the information about the current request, so the script function which handles the callback can identify the context to complete the process.

Return value: The required return value depends on the custom operation result type:

  • Notification: The script must return a string which will be shown as a notification to the user. If the string starts with the following special prefixes: [INFO], [WARN] or [ERROR], those prefixes are removed from the notification and the notification style for the corresponding types is chosen (for example, shows a yellow notification with a warning icon when [WARN]). If no such prefixes, assumes an information notification.
  • Plain text or Rich text: The script should return a org.cyclos.model.system.operations.CustomOperationContentResult, or equivalent object. The result has a title and a content. Alternatively, if only a string is returned, the custom operation name is displayed as title.
  • File download: The script should return an instance of org.cyclos.model.utils.FileInfo, or an object or Map with the same properties. The properties are:
    • content: Required. The file content. May be an InputStream, a File or a String (containing the file content itself).
    • contentType: Required. The MIME type, such as text/plain, text/html, image/jpeg, application/pdf, etc.
    • name: Optional file name, which will be used by browsers to suggest the file name to save.
    • length: Optional file length, which may aid browsers to monitor the progress of file downloads.
  • Page results: The script should return an instance of org.cyclos.model.system.operations.CustomOperationPageResult, or an object or Map with the same properties. The properties are:
    • columns: Either this or headers should be returned. Contains each column definition. Each column is a org.cyclos.model.system.operations.PageResultColumn or equivalent object. Each column can define a result property to display (otherwise it is assumed that each result is an array, accessed by index). Additionally, defines the header, width, align, vertical align.
    • headers: Can be returned instead of columns. A list containing the column headers. Is supported to ease simple cases and to maintain compatibility with scripts written from Cyclos versions before 4.5.
    • rows: Optional. A list of objects, each containing properties. Each colum match the corresponding object property to display each cell. An object can have additional properties, which can be used to pass parameters to the url when clicking a row.
    • results: Optional. Can be returned instead of rows. A list of lists, containing the table cells. The inner lists should have the same size as the columns.
    • totalCount: Optional. The total count of records. For example, if all matching records are 1000, but the page size is 20, the results would normally have 20 records, and the total count would be 1000. This allows paginating through the results. When not returned, the results won't be paginable.
  • URL: The script should return a org.cyclos.model.system.operations.CustomOperationUrlResult, or equivalent object. The result has the url and whether the client should open a new window for it or not (show in the same window as the Cyclos application). Alternatively, if only a string is returned, it is used as URL and the newWindow property is false. Most browsers block popups by default, and opening in a new window is considered a popup by browsers. Hence, when opening a new window, on the first execution, users might be prompted whether the popup is allowed. Then they can allow and try the operation again.
  • External redirect: The first script function must return a string, representing a valid URL. That URL will be used to redirect the user to the external site. One of the variables bound to the context is the callback URL that the script must pass to the external site, so the user is redirected to that URL after the external processing finishes. Cyclos offers a parameter storage for the first script to store data. The same storage will be available when the second script code runs, that is, after the external site redirects the user back to Cyclos. The second script runs with the same authentication as the first one, so the operation can continue, and should return an HTML notification for the user. As the return url will make the Cyclos application have no context (which is maintained as JavaScript in the browser page), the user will see the home page with that notification. There is a limit of a few hours (4-5) for the same execution context to be valid between the first and the second scripts.

4.2.10.1. Examples

Contact us page

This example allows creating a "contact us" page, which sends an e-mail to a specified address. To use it, you will need the following content in the script parameters box:

to=admin@project.org
from=noreply@project.org
subject=Contact form
message=The message was sent.\nThank you for your contact.

mailHeader=An user has sent a contact form with the following data:
mailFrom=From:
mailEmail=E-Mail:
mailSubject=Subject:
mailMessage=Message:

invalidEmail=Invalid e-mail address

Then, use the following script code:

import javax.mail.internet.InternetAddress

import org.cyclos.impl.utils.validation.validations.EmailValidation
import org.cyclos.model.ValidationException
import org.springframework.mail.javamail.MimeMessageHelper

def sender = mailHandler.mailSender
def message = sender.createMimeMessage()
def helper = new MimeMessageHelper(message)

if (!EmailValidation.isValid(formParameters.email)) {
    throw new ValidationException(scriptParameters.invalidEmail);
}

helper.to = new InternetAddress(scriptParameters.to)
helper.from = new InternetAddress(scriptParameters.from)
helper.subject = scriptParameters.subject
helper.text = """
${scriptParameters.mailHeader}
${scriptParameters.mailFrom} ${formParameters.from}
${scriptParameters.mailEmail} ${formParameters.email}
${scriptParameters.mailSubject} ${formParameters.subject}
${scriptParameters.mailMessage} ${formParameters.message}
"""
sender.send message

return scriptParameters.message

Generating an account number for all accounts which doesn't have a number yet

If the account number (a feature new to Cyclos 4.4) is enabled, existing accounts will not have numbers automatically assigned. However, a custom operation can be created and executed a single time, assigning a number to all accounts (even system accounts) which don't have a number yet. To accomplish this, create a custom operation script with the following code:

import static org.cyclos.impl.utils.QueryHelper.processBatch

import org.cyclos.entities.banking.QAccount

def a = QAccount.account
def accounts = entityManagerHandler
        .from(a)
        .where(a.number.isNull())
        .iterate(a)

int affected = 0
processBatch(entityManagerHandler, accounts) { account ->
    def number = accountService.generateNumber(account.type, account.owner)
    account.number = number
    affected++
}

return "Generated the account number for ${affected} accounts"
Returning a string (notification / rich / plain text) and external redirect

Examples of a custom operation which returning a text (a notification in that case) can be found in the loan solution example. An example of an external redirect is the PayPal integration example.

Returning a file

This is an example where the user selects a document to download. It is assumed that the custom operation has a form field of type single selection with internal name file. Then, each possible value should have the internal name corresponding to a pdf file in a given folder. Once the user chooses the file, it is downloaded.

import org.cyclos.model.ValidationException

// Assume there is a pdf file for each possible value of the field
String fileName = formParameters.file.internalName
String dir = scriptParameters.dir ?: "/usr/share/documents"
File file = new File(dir, "${fileName}.pdf")
if (!file.exists()) {
    throw new ValidationException("File not found")
}
return [
    content: file,
    contentType: "application/pdf",
    name: file.name,
    length: file.length(),
    lastModified: file.lastModified()
]

Returning a result list

In this example, an user can see the other users he has traded with (either performed or received payments). The custom operation needs to have user scope and result type list. Also it needs to have the URL action as Cyclos location, and the location needs to be 'user_profile'. Finally, set as URL parameters the value 'id' (without quotes). For more details, see the next section.

import org.cyclos.entities.banking.QTransaction
import org.cyclos.entities.users.QUser

import com.querydsl.core.types.Projections

// This bean will hold the projection of results
class ResultBean {
    Long id
    String username
    String name
    Number tx

    // All long numbers are passed through IdMask.
    // Store the count as int instead, or it would be modified on serialization.
    public void setTx(Number tx) {
        this.tx = tx?.intValue()
    }
}

QTransaction t = QTransaction.transaction
QUser u = QUser.user

// Base query (group by, order by, limits and projection will be added later)
def query = entityManagerHandler
        .from(t).innerJoin(u).on(t.fromUser().id
        .when(user.id).then( t.toUser().id).otherwise(t.fromUser().id).eq(u.id))
        .where(t.fromUser.eq(user).or(t.toUser.eq(user)))

// Get the number of users I've traded with
def totalCount = query.singleResult(u.id.countDistinct())

// Get the page of users / transactions
def projection = Projections.bean(ResultBean, u.id, u.username, u.name, u.id.count().as("tx"))
def rows = query.groupBy(u.id, u.username, u.name)
        .orderBy(u.id.count().desc())
        .offset(currentPage * pageSize)
        .limit(pageSize)
        .list(projection)

// Build the result
return [
    columns: [
        [header: "Login name", property: "username", width: "20%"],
        [header: "Full name", property: "name", align: "center"],
        [header: "Transactions", property: "tx", width: "20%", align: "right"]
    ],
    rows: rows,
    totalCount: totalCount
]

4.2.10.2. Possibilities for custom operations that return a result page

Custom operations that return a page of results are very versatile. For example, they can be printed as PDF or exported to CSV, or page results (if the script returns the total count).

Also, on the custom operation form it is possible to define an action to be executed when a row is clicked by the user. The possible actions are:

  • Navigate to an external URL: When clicking a row, the user is redirected to an external URL.
  • Navigate to a location in Cyclos: A list of common locations in Cyclos are presented.
  • Run an internal custom operation: Allows running a custom operation which has the scope = 'Internal'. This new operation will probably present some content to the user.

In all cases an action is set to a row, parameters can be passed to the next page. This is very important, as will provide context on which data was selected. For an internal custom operation to receive a parameter, first on the result page custom operation the field 'URL parameters' must be set, having a comma-separated value of object properties to be passed to the internal custom operation. This will pass all such properties from the clicked row to the internal custom operation. Then, the internal custom operation needs to have form fields defined with the matching internal name. The following is an example script for a custom operation which lists fictional external records. It needs to have as URL action the custom operation presented ahead to show an external record details, and pass the URL parameter 'recordId' (without quotes):

return [
    columns: [
        [header:"Name", property:"name"]
    ],
    rows: [
        [name: "Record 1", recordId: 1],
        [name: "Record 2", recordId: 2],
        [name: "Record 3", recordId: 3],
        [name: "Record 4", recordId: 4],
        [name: "Record 5", recordId: 5],
        [name: "Invalid Record", recordId: 99999],
    ]
]
Then another custom operation, which should be defined as internal, and have a form field which internal name 'recordId' (without quotes):
import org.cyclos.model.EntityNotFoundException

// Validate the id
def recordId = formParameters.recordId
def validIds = 1..50
if (!(recordId in validIds)) {
    throw new EntityNotFoundException([
        entityType: "External record",
        key: recordId as String])
}

return [
    title: "Details for record ${recordId}",
    content: "This is the description for record ${recordId}"
]

4.2.11. Custom web services

These scripts are invoked when a request is received in some path under <cyclos-root-url>[/network]/run/**. To actually run them, it is needed to create a custom web service definition in the "System - Tools - Custom web services" menu.

The custom web services have the following important properties:

  • The accepted HTTP methods: GET, POST or Both;
  • Whether the script will be executed as guest (optionally using a fixed HTTP username / password, with basic authorization) or as an authenticated user, like with other web services, using the same headers described in Section 3.4, “Other clients”;
  • An IP address whitelist, to control which hosts can call the custom web service;
  • The URL mappings, which is a list of paths (one per line) to be matched after the <cyclos-root-url>[/network]/run root path. It is possible to specify the following types of paths:
    • Simple paths. For example, 'users', matches '<cyclos-root-url>[/network]/run/users'
    • Nested paths. For example, 'users/list', matches '<cyclos-root-url>[/network]/run/users/list'
    • Wildcards. For example, 'users/*', matches '<cyclos-root-url>[/network]/run/users/a', but not '<cyclos-root-url>[/network]/run/users/a/b'
    • Nested wildcards. For example, 'users/**', matches '<cyclos-root-url>[/network]/run/users/a/b/c'
    • Path variables. For example, 'users/{groupId}/{userId}', matches '<cyclos-root-url>[/network]/run/users/123/78', and a map with {groupId:123,userId:78} is available to the script

Bound variables:

Return value: The script may return one of the following data:

  • A org.cyclos.model.utils.ResponseInfo, allowing to totally customize the response
  • Null. In this case, the response will have status code 200 and no body.
  • A string. In this case, the response will have status code 200, Content-type: text/plain, and the returned string as body
  • An arbitrary object / collection. this case, the response will have status code 200, Content-type: application/json, and the body will contain a JSON representation of the returned object

If the script captures an error and wants to customize the response, instead of silencing the exception in a catch clause and returning a org.cyclos.model.utils.ResponseInfo, which will cause the current transaction to commit, possibly leaving the database in an inconsistent state, the script should throw a org.cyclos.model.utils.ResponseException, which contains a ResponseInfo internally. This way the main transaction is rolled back. Other exceptions than ResponseExceptions are returned as HTTP status codes other than 200, and the details are returned as JSON, in the same way as Section 3.4, “Other clients”.

Sometimes it is useful to extend the Cyclos API to clients, like doing specific payments, or running a series of operations in a single request. However, it is important to use the same permissions as the user would normally have, to prevent security breaches. To do so, 3 steps are needed:

  • Make sure the script uses the security layer: Whenever using a service, use the security layer instead of the direct service implementation. For example, to use the UserService, instead of using the userService bound variable, use userServiceSecurity instead.
  • Ensure the custom web service has user authentication: On the custom web service details page, ensure it runs as user, not as guest.
  • On the script, make sure it runs with the user permisssions: On the details page of the script used by the custom web service, make sure the checkbox called Run with all permissions is unchecked. This guarantees the script will run with the exact permissions as the user

4.2.11.1. Examples

Perform a payment

This example allows a caller to quickly perform a payment between 2 users. It is assumed that the URL mapping is something like payment/{from}/{to}/{amount} and there is a single possible payment type between the 2 users.

import org.cyclos.model.banking.transactions.PerformPaymentDTO
import org.cyclos.model.users.users.UserLocatorVO

def pmt = new PerformPaymentDTO()
pmt.from = new UserLocatorVO(principal: pathVariables.from)
pmt.to = new UserLocatorVO(principal: pathVariables.to)
pmt.amount = pathVariables.getDecimal('amount')

// Perform the payment and return the complete PaymentVO
return paymentService.perform(pmt)

4.2.12. Custom scheduled tasks

These scripts are called periodically by custom scheduled tasks. See System – Scheduled tasks for more details.

The bound variables are:

Return value:

  • The script should return a string, which is logged as message, and can be viewed on the application

4.2.12.1. Examples

Periodically importing a file

This example imports a file with users, which is expected to be located at a given directory in the file system. For other import types, it is just a matter of using distinct org.cyclos.model.system.imports.ImportedFileDTO subclasses (some require setting some parameter, like in the example, the group for users). The scheduled task just triggers the import. From that point, the import is processed on the background, and the status can be monitored on System - Tools - Imports menu.

To use it, you will need the following content in the script parameters box (either in script itself or in the custom scheduled task's script parameters):

filename=/tmp/imports/users.csv
group=consumers

Then use the following code in the script box:

import org.cyclos.model.system.imports.UserImportedFileDTO
import org.cyclos.model.users.groups.GroupVO
import org.cyclos.model.utils.FileSizeUnit
import org.cyclos.server.utils.SerializableInputStream

// Resolve the users filename and the group
String filename = scriptParameters['filename']
String groupInternalName = scriptParameters['group']

// Download the file to a local temp file
File file = new File(filename)
if (!file.exists()) {
    return "The expected file, ${filename}, doesn't exist"
}
if (file.length() == 0) {
    return "The file ${filename} is empty"
}

// Caution! the SerializableInputStream automatically deletes the file
// when closed, except when calling, except when calling .file()
def stream = new SerializableInputStream(file)
stream.file()

// Import
UserImportedFileDTO dto = new UserImportedFileDTO()
dto.fileName = filename
// It is important to mark the file as automatic import,
// otherwise manual interaction would be required for processing
dto.processAutomatically = true
dto.group = new GroupVO([internalName: groupInternalName])
importService.upload(dto, stream)

// Build a result string
def fileSize = FileSizeUnit.nearestFileSize(file.length())
return "Started import of ${filename}. File size is ${fileSize}"

Periodically update a static HTML page

In this example, every time the scheduled task runs, a static HTML file is updated. In the file, it is written the total number of users and the balances of each system account.

import groovy.xml.MarkupBuilder

import org.cyclos.entities.users.QUser
import org.cyclos.model.banking.accounts.AccountWithStatusVO
import org.cyclos.model.banking.accounts.SystemAccountOwner
import org.cyclos.model.users.groups.BasicGroupNature
import org.cyclos.model.users.users.UserStatus

def now = new Date()

QUser u = QUser.user
int users = entityManagerHandler
        .from(u)
        .where(u.status.ne(UserStatus.REMOVED),
        u.group.nature.eq(BasicGroupNature.USER_GROUP))
        .count()
List<AccountWithStatusVO> accounts = accountService.
        getAccountsSummary(SystemAccountOwner.instance(), null)

File out = new File("/var/www/html/summary.html")

def sessionData = binding.sessionData
def formatter = binding.formatter
MarkupBuilder builder = new MarkupBuilder(new FileWriter(out))
builder.html {
    head {
        title "${sessionData.configuration.applicationName} summary"
        meta charset: "UTF-8"
    }
    body {
        p {
            b "Total users"
            span ": ${users}"
        }
        accounts.each { a ->
            p {
                b a.type.name
                span " balance: ${formatter.format(a.status.balance)}"
            }
        }
        br()
        br()
        br()
        p style: "font-size: small", "Last updated: ${formatter.format(now)}"
    }
}
return "File ${out.absolutePath} updated"

4.2.13. Custom SMS operations

These scripts are invoked when an user executes a custom sms operation, as configured in the sms channel in the configuration. The function should implement the logic for that operation.

Bound variables:

There are no expected return values for this script.

4.2.13.1. Examples

Pay taxi with an SMS message

In this example SMS operation, users can pay taxi drivers via SMS. It expects a single transfer type for the SMS operations channel to be enabled, and the user performing the operation needs to have permission to perform that payment. Besides, a custom profile field with internal name taxiId of type single line text, and marked as unique needs to be enabled for the product of taxi owners. Then, in the configuration details, in the channels tab, enable SMS operations and add an operation with alias taxi and the selected script. Then, customers can perform the payment by sending an sms in the format: taxi <taxi id> <amount>

import org.cyclos.model.ValidationException
import org.cyclos.model.banking.TransferException
import org.cyclos.model.banking.transactions.PerformPaymentDTO
import org.cyclos.model.banking.transactions.PerformPaymentData
import org.cyclos.model.messaging.sms.OutboundSmsType
import org.cyclos.model.system.fields.CustomFieldVO
import org.cyclos.model.users.fields.UserCustomFieldValueVO
import org.cyclos.model.users.users.UserLocatorVO

// Read the parameters
String taxiId = parameterProcessor.nextString("taxiId")
BigDecimal amount = parameterProcessor.nextDecimal("amount")

// Find the user by taxi id
def locator = new UserLocatorVO()
locator.fieldValue = new UserCustomFieldValueVO([
    field: new CustomFieldVO([internalName: "taxiId"]),
    stringValue: taxiId
])

// Find the payment type
PerformPaymentData data = transactionService.getPaymentData(
        phone.user, locator)
if (data.paymentTypes?.size == 0) {
    throw new ValidationException("No possible payment types")
}

// Perform the payment
def pmt = new PerformPaymentDTO()
pmt.amount = amount
pmt.from = data.from
pmt.to = data.to
pmt.type = data.paymentTypes[0]
try {
    vo = paymentService.perform(pmt)
    outboundSmsHandler.send(phone,
            "The payment was successful",
            OutboundSmsType.SMS_OPERATION_RESPONSE)
    // Also notify the taxi, for example, by connecting to the
    // taxi company system, which notifies the taxi driver...
} catch (TransferException e) {
    outboundSmsHandler.send(phone,
            "The payment couldn't be performed",
            OutboundSmsType.SMS_OPERATION_RESPONSE)
}

4.2.14. Inbound SMS handling

These scripts are invoked when a gateway sends SMS messages to Cyclos. There are two functions in this script: one to generate the gateway response and another one to resolve basic SMS data from an inbound HTTP request. Both functions are optional, defaulting to the normal behavior (when not using a script).

The common bound variables are:

The functions are:

  • Resolve basic SMS data: Function used to read an inbound sms request and return an object containing the phone number, the SMS message and the splitted SMS message into parts. Only the phone number and SMS message are required. If the message parts are empty, it will be assumed the message will be split by spaces.

  • Generate gateway response: Function used to determine the HTTP status code, headers and body to be returned to the SMS gateway. It can be called either when the bare minimum parameters – mobile phone number and sms message – were not sent by the gateway or when the gateway has sent a valid SMS. Keep in mind that if an operation has resulted in error, from a gateway perspective, the SMS was still delivered correctly, and the response should be a successful one. Maybe when the bare minimum parameters weren't send, the script could choose to return a different message. When no code is given, the default processing will be done, returning the HTTP status code 200 with "OK" in the body.

4.2.14.1. Examples

Receiving a SMS with a custom format

This example reads the phone number from a request header, and the message from the request body:

import org.apache.commons.io.IOUtils
import org.cyclos.impl.utils.sms.InboundSmsBasicData

// Read the phone from a header, and the message from the body
InboundSmsBasicData result = new InboundSmsBasicData()
result.phoneNumber = request.headers."phone-number"
result.message = IOUtils.toString(request.body, "UTF-8")
return result

4.2.15. Outbound SMS handling

These scripts are invoked to send SMS messages. By default, Cyclos connects to gateways via HTTP POST / GET, which can be set in the configuration. However, the sending can be customized (or totally replaced) via a script. As in most cases the custom sending just wants to customize some aspects of the sending, not all, it is possible that the script just creates a subclass of org.cyclos.impl.utils.sms.GatewaySmsSender, customizing some aspects of it (for example, by overridding the buildRequest method and adding some headers, or the resolveVariables method to have some additional variables which can be sent in the POST body).

Bound variables:

Return value:

4.2.15.1. Examples

Sending SMS requests as XML

This example posts the SMS message as XML to the gateway, and awaits the response before returning the status:

import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.*
import groovyx.net.http.HTTPBuilder

import java.util.concurrent.CountDownLatch

import org.cyclos.model.messaging.sms.OutboundSmsStatus

// Read the gateway URL from the configuration
def url = configuration.outboundSmsConfiguration.gatewayUrl

// Send the POST request
def http = new HTTPBuilder(url)
CountDownLatch latch = new CountDownLatch(1)
def error = false
http.request(POST, XML) {
    // Pass the body as a closure - parsed as XML
    body = {
        "sms-message" {
            "destination-phone" phoneNumber
            text message
        }
    }

    response.success = { resp, xml ->
        latch.countDown()
    }

    response.failure = { resp ->
        error = true
        latch.countDown()
    }
}

//Await for the response
latch.await()
return error ? OutboundSmsStatus.SUCCESS : OutboundSmsStatus.UNKNOWN_ERROR

4.2.16. Link generation

These scripts are used to generate the possible urls based on the underlying context.

The script code has the following variables bound (besides the default bindings ):

Related User:

Location:

  • location : The org.cyclos.model.utils.Location . The location can be used to determined the context of the link generation. For example, PAYMENT tells that this link leads to a payment details.

Entity id:

  • entityId : The related entity id Long value. For example, if the link is being generated for a payment details, this value represents the payment's id.

URL file part:

  • urlFilePart : In some circumstances the file part of the URL is provided in this attribute.

4.2.16.1. Examples

Link generation depending on the location

This examples returns distinct values according to location.

                    import org.cyclos.model.utils.Location

def root = 'https://mydomain.com' 

if(location == null){
	return null
}

switch(location){
	case Location.EXTERNAL_PAYMENT:
		return root + '/external-payment/' + entityId
	case Location.TRANSFER:
		return root + '/transfer/' + entityId
	default:
		return null
}