Examples of solutions that require a single script can be found directly in the specific script description page (links directly above). Solutions that need several scripts and configurations can be found in this section.
It is possible to integrate Cyclos with PayPal, allowing users to buy units with their PayPal account. This is done with a custom operation which allows users to confirm the payment in PayPal and then, once the payment is confirmed, a payment from a system account is performed to the corresponding user account, automating the process of buying units. However, keep in mind the rates charged by PayPal, which vary according to some conditions.
To do so, first you'll need a PayPal premium or business account (for testing – using PayPal sandbox – any account is enough). You'll need to go to the PayPal Developer page to create an application, and get the client id and secret.
Then several configurations are required in Cyclos. Scripts can only be created as global administrators switched to a network, so it is advised to use a global admin to perform the configuration. Carefully follow each of the following steps:
Make sure that the configuration for users use a correct root url. In System > System configuration > Configurations, select the configuration set for users and make sure the Main URL field points to the correct external URL. It will be used to generate the links which will be sent to PayPal redirect users back to Cyclos after confirming / canceling the operation.
This can be checked under System > Currencies select the currency used for this operation, mark the Enable transfer number option and fill in the required parameters.
Under System > System configuration > Record types, create a new system record type, with the following characteristics:
For this record type, create the following fields:
Under System > System configuration > Record types, create a new user record type, with the following characteristics:
For this record type, create the following fields:
Under System > Tools > Scripts, create a new library script, with the following characteristics:
# Settings for the access token record type auth.recordType = paypalAuth auth.clientId = clientId auth.clientSecret = clientSecret auth.token = token auth.tokenExpiration = tokenExpiration # Settings for the payment record type payment.recordType = paypalPayment payment.paymentId = paymentId payment.amount = amount payment.transaction = transaction # Settings for PayPal mode = sandbox currency = EUR paymentDescription = Buy Cyclos units # Settings for the Cyclos payment amountMultiplier = 1 accountType = debitUnits paymentType = paypalCredits # Messages error.invalidRequest = Invalid request error.transactionNotFound = Transaction not found error.transactionAlreadyApproved = The transaction was already approved error.payment = There was an error while processing the payment. Please, try again. error.notApproved = The payment was not approved message.canceled = You have cancelled the operation.\nFeel free to start again if needed. message.done = You have successfully completed the payment. Thank you.
import static groovyx.net.http.ContentType.*
import static groovyx.net.http.Method.*
import groovyx.net.http.HTTPBuilder
import java.util.concurrent.CountDownLatch
import org.apache.commons.codec.binary.Base64
import org.cyclos.entities.banking.PaymentTransferType
import org.cyclos.entities.banking.SystemAccountType
import org.cyclos.entities.users.RecordCustomField
import org.cyclos.entities.users.SystemRecord
import org.cyclos.entities.users.SystemRecordType
import org.cyclos.entities.users.User
import org.cyclos.entities.users.UserRecord
import org.cyclos.entities.users.UserRecordType
import org.cyclos.impl.banking.PaymentServiceLocal
import org.cyclos.impl.system.ScriptHelper
import org.cyclos.impl.users.RecordServiceLocal
import org.cyclos.impl.utils.IdMask
import org.cyclos.impl.utils.persistence.EntityManagerHandler
import org.cyclos.model.EntityNotFoundException
import org.cyclos.model.banking.accounts.SystemAccountOwner
import org.cyclos.model.banking.transactions.PaymentVO
import org.cyclos.model.banking.transactions.PerformPaymentDTO
import org.cyclos.model.banking.transfertypes.TransferTypeVO
import org.cyclos.model.users.records.RecordDataParams
import org.cyclos.model.users.records.UserRecordDTO
/**
* Class used to store / retrieve the authentication information for PayPal
* A system record type is used, with the following fields: client id (string),
* client secret (string), access token (string) and token expiration (date)
*/
class PayPalAuth {
String recordTypeName
String clientIdName
String clientSecretName
String tokenName
String tokenExpirationName
SystemRecordType recordType
SystemRecord record
Map<String, Object> wrapped
public PayPalAuth(Object binding) {
def params = binding.scriptParameters
recordTypeName = params.'auth.recordType' ?: 'paypalAuth'
clientIdName = params.'auth.clientId' ?: 'clientId'
clientSecretName = params.'auth.clientSecret' ?: 'clientSecret'
tokenName = params.'auth.token' ?: 'token'
tokenExpirationName = params.'auth.tokenExpiration' ?: 'tokenExpiration'
// Read the record type and the parameters for field internal names
recordType = binding.entityManagerHandler
.find(SystemRecordType, recordTypeName)
// Should return the existing instance, of a single form type.
// Otherwise it would be an error
record = binding.recordService.newEntity(
new RecordDataParams(recordTypeId: recordType.id))
if (!record.persistent) throw new IllegalStateException(
"No instance of system record ${recordType.name} was found")
wrapped = binding.scriptHelper.wrap(record, recordType.fields)
}
public String getClientId() {
wrapped[clientIdName]
}
public void setClientId(String clientId) {
wrapped[clientIdName] = clientId
}
public String getClientSecret() {
wrapped[clientSecretName]
}
public void setClientSecret(String clientSecret) {
wrapped[clientSecretName] = clientSecret
}
public String getToken() {
wrapped[tokenName]
}
public void setToken(String token) {
wrapped[tokenName] = token
}
public Date getTokenExpiration() {
wrapped[tokenExpirationName]
}
public void setTokenExpiration(Date tokenExpiration) {
wrapped[tokenExpirationName] = tokenExpiration
}
}
/**
* Class used to store / retrieve PayPal payments as user records in Cyclos
*/
class PayPalRecord {
String recordTypeName
String paymentIdName
String amountName
String transactionName
UserRecordType recordType
Map<String, RecordCustomField> fields
private EntityManagerHandler entityManagerHandler
private RecordServiceLocal recordService
private ScriptHelper scriptHelper
public PayPalRecord(Object binding) {
def params = binding.scriptParameters
recordTypeName = params.'payment.recordType' ?: 'paypalPayment'
paymentIdName = params.'payment.paymentId' ?: 'paymentId'
amountName = params.'payment.amount' ?: 'amount'
transactionName = params.'payment.transaction' ?: 'transaction'
entityManagerHandler = binding.entityManagerHandler
recordService = binding.recordService
scriptHelper = binding.scriptHelper
recordType = binding.entityManagerHandler.find(UserRecordType, recordTypeName)
fields = [:]
recordType.fields.each {f -> fields[f.internalName] = f}
}
/**
* Creates a payment record, for the given user and JSON,
* as returned from PayPal's create payment REST method
*/
public UserRecord create(User user, Number amount) {
RecordDataParams newParams = new RecordDataParams(
[userId: user.id, recordTypeId: recordType.id])
UserRecordDTO dto = recordService.getDataForNew(newParams).getDTO()
Map<String, Object> wrapped = scriptHelper.wrap(dto, recordType.fields)
wrapped[amountName] = amount
// Save the record DTO and return the entity
Long id = recordService.save(dto)
return entityManagerHandler.find(UserRecord, id)
}
/**
* Finds the record by id
*/
public UserRecord find(Long id) {
try {
UserRecord userRecord = entityManagerHandler.find(UserRecord, id)
if (userRecord.type != recordType) {
return null
}
return userRecord
} catch (EntityNotFoundException e) {
return null
}
}
/**
* Removes the given record, but only if it is of the
* expected type and hasn't been confirmed
*/
public void remove(UserRecord userRecord) {
if (userRecord.type != recordType) {
return
}
Map<String, Object> wrapped = scriptHelper
.wrap(userRecord, recordType.fields)
if (wrapped[transactionName] != null) return
entityManagerHandler.remove(userRecord)
}
}
/**
* Class used to interact with PayPal services
*/
class PayPalService {
String mode
String baseUrl
String currency
String paymentDescription
String accountTypeName
String paymentTypeName
double multiplier
SystemAccountType accountType
PaymentTransferType paymentType
private ScriptHelper scriptHelper
private PaymentServiceLocal paymentService
private IdMask idMask
private PayPalAuth auth
private PayPalRecord record
public PayPalService(
Object binding, PayPalAuth auth, PayPalRecord record) {
this.auth = auth
this.record = record
scriptHelper = binding.scriptHelper
paymentService = binding.paymentService
idMask = binding.applicationHandler.idMask
def params = binding.scriptParameters
mode = params.mode ?: 'sandbox'
if (mode != 'sandbox' && mode != 'live') {
throw new IllegalArgumentException("Invalid PayPal parameter " +
"'mode': ${mode}. Should be either sandbox or live")
}
baseUrl = mode == 'sandbox'
? 'https://api.sandbox.paypal.com' : 'https://api.paypal.com'
currency = params.currency
if (currency == null || currency.empty) {
throw new IllegalArgumentException(
"Missing PayPal parameter 'currency'")
}
EntityManagerHandler emh = binding.entityManagerHandler
accountTypeName = params.accountType
if (accountTypeName == null || accountTypeName.empty)
throw new IllegalArgumentException(
"Missing PayPal parameter 'accountType'")
paymentTypeName = params.paymentType
if (paymentTypeName == null || paymentTypeName.empty)
throw new IllegalArgumentException(
"Missing PayPal parameter 'paymentType'")
accountType = emh.find(SystemAccountType, accountTypeName)
if (!accountType.currency.transactionNumber?.used) {
throw new IllegalStateException("Currency " + accountType.currency
+ " doesn't have transaction number enabled")
}
paymentType = emh.find(
PaymentTransferType, paymentTypeName, accountType)
multiplier = Double.parseDouble(params.amountMultiplier ?: "1")
paymentDescription = params.paymentDescription ?: ""
}
/**
* Creates a payment in PayPal and the corresponding user record
*/
public Object createPayment(User user, Number amount, String callbackUrl) {
// Create the UserRecord for this payment
UserRecord userRecord = record.create(user, amount)
Long maskedId = idMask.apply(userRecord.id)
String returnUrl = "${callbackUrl}?succes=true&recordId=${maskedId}"
String cancelUrl = "${callbackUrl}?cancel=true&recordId=${maskedId}"
def jsonBody = [
intent: "sale",
redirect_urls: [
return_url: returnUrl,
cancel_url: cancelUrl
],
payer: [
payment_method: "paypal"
],
transactions: [
[
description: paymentDescription,
amount: [
total: amount,
currency: currency
]
]
]
]
// Create the payment in PayPal
Object json = postJson("${baseUrl}/v1/payments/payment", jsonBody)
// Update the payment id
def wrapped = scriptHelper.wrap(userRecord)
wrapped[record.paymentIdName] = json.id
return json
}
/**
* Executes a PayPal payment, and creates the payment in Cyclos
*/
public Object execute(String payerId, UserRecord userRecord) {
Object wrapped = scriptHelper.wrap(userRecord)
String paymentId = wrapped[record.paymentIdName]
BigDecimal amount = wrapped[record.amountName]
BigDecimal finalAmount = amount * multiplier
// Execute the payment in PayPal
Object json = postJson(
"${baseUrl}/v1/payments/payment/${paymentId}/execute",
[payer_id: payerId])
if (json.state == 'approved') {
// Perform the payment in Cyclos
PerformPaymentDTO dto = new PerformPaymentDTO()
dto.from = SystemAccountOwner.instance()
dto.to = userRecord.user
dto.amount = finalAmount
dto.type = new TransferTypeVO(paymentType.id)
PaymentVO vo = paymentService.perform(dto)
// Update the record, setting the linked transaction
wrapped[record.transactionName] = vo
userRecord.lastModifiedDate = new Date()
}
return json
}
/**
* Performs a synchronous request, posting and accepting JSON
*/
private postJson(url, jsonBody) {
def http = new HTTPBuilder(url)
CountDownLatch latch = new CountDownLatch(1)
def responseJson = null
def responseError = []
// Check if we need a new token
if (auth.token == null || auth.tokenExpiration < new Date()) {
refreshToken()
}
// Perform the request
http.request(POST, JSON) {
headers.'Authorization' = "Bearer ${auth.token}"
body = jsonBody
response.success = { resp, json ->
responseJson = json
latch.countDown()
}
response.failure = { resp ->
responseError << resp.statusLine.statusCode
responseError << resp.statusLine.reasonPhrase
latch.countDown()
}
}
latch.await()
if (!responseError.empty) {
throw new RuntimeException("Error making PayPal request to ${url}"
+ ", got error code ${responseError[0]}: ${responseError[1]}")
}
return responseJson
}
/**
* Refreshes the access token
*/
private void refreshToken() {
def http = new HTTPBuilder("${baseUrl}/v1/oauth2/token")
CountDownLatch latch = new CountDownLatch(1)
def responseJson = null
def responseError = []
http.request(POST, JSON) {
String auth = Base64.encodeBase64String((auth.clientId + ":"
+ auth.clientSecret).getBytes("UTF-8"))
headers.'Accept-Language' = 'en_US'
headers.'Authorization' = "Basic ${auth}"
send URLENC, [
grant_type: "client_credentials"
]
response.success = { resp, json ->
responseJson = json
latch.countDown()
}
response.failure = { resp ->
responseError << resp.statusLine.statusCode
responseError << resp.statusLine.reasonPhrase
latch.countDown()
}
}
latch.await()
if (!responseError.empty) {
throw new RuntimeException("Error getting PayPal token, " +
"got error code ${responseError[0]}: ${responseError[1]}")
}
// Update the authentication data
auth.token = responseJson.access_token
auth.tokenExpiration = new Date(System.currentTimeMillis() +
((responseJson.expires_in - 30) * 1000))
}
}
// Instantiate the objects
PayPalAuth auth = new PayPalAuth(binding)
PayPalRecord record = new PayPalRecord(binding)
PayPalService paypal = new PayPalService(binding, auth, record)
Under System > Tools > Scripts, create a new custom operation script, with the following characteristics:
def result = paypal.createPayment(user, formParameters.amount, returnUrl)
def link = result.links.find {it.rel == "approval_url"}
if (link) {
return link.href + "&useraction=commit"
} else {
throw new IllegalStateException("No approval url returned from PayPal")
}import org.cyclos.entities.users.UserRecord
def recordId = request.parameters.recordId as Long
def payerId = request.parameters.PayerID
// No record?
if (recordId == null) {
return "[ERROR] " +
(scriptParameters.'error.invalidRequest' ?: "Invalid request")
}
// Find the corresponding record
UserRecord userRecord = record.find(applicationHandler.idMask.remove(recordId))
if (userRecord == null) {
return "[ERROR] " +
(scriptParameters.'error.transactionNotFound' ?: "Transaction not found")
}
def wrapped = scriptHelper.wrap(userRecord)
if (request.parameters.cancel) {
// The operation has been canceled. Remove the record and send a message.
record.remove(userRecord)
return "[WARN]" + scriptParameters.'message.canceled'
?: "You have cancelled the operation.\nFeel free to start again if needed."
} else {
// Execute the payment
try {
def json = paypal.execute(payerId, userRecord)
if (json.state == 'approved') {
return scriptParameters.'message.done'
?: "You have successfully completed the payment. Thank you."
} else {
return "[ERROR] " + scriptParameters.'error.notApproved'
?: "The payment was not approved"
}
} catch (Exception e) {
return "[ERROR] " + scriptParameters.'error.payment'
?: "There was an error while processing the payment. Please, try again."
}
}
Under System > Tools > Custom operations, create a new one with the following characteristics:
For this custom operation create the following field:
Under System > Accounts configuration > Account types, choose the (normally unlimited) account from which payments will be performed to users. Then set its internal name to some meaningful name. The example configuration uses debitUnits as internal name, but it can be changed. Save the form.
Still in the details page for the account type, on the Transfer types tab, create a new Payment transfer type with the following characteristics:
Under System > User configuration > Groups, select the Network administrators group. Then, in the Permissions tab:
Click Reports & data > System records > Paypal authentication. If this menu entry is not showing up, refresh the browser page (by pressing F5) and try again. Update the Client ID and Client Secret fields exactly with the ones you got in the application you registered in the PayPal Developer page. Remember that PayPal has a sandbox, which can be used to test the application, and a live environment. For now, use the sandbox credentials. The other 2 fields can be left blank. Save the record.
Once the record is properly set, if you want to remove it from the menu, you can just remove the permission to view this system record in the adminitrator group page.
In System > User configuration > Products (permissions), select the member product for users which will run the operation. In the Custom operations field, make the Buy units with PayPal both enabled and allowed to run. From this moment, the operation will show up for users in the banking menu. Also on the Records enable the PayPal payment record.
In the PayPal library script, in parameters, there are several configurations which can be done. All those settings can be overridden in the custom operation's script parameters, allowing using distinct configurations for distinct operations. For example, it is possible to have distinct operations to perform payments in distinct currencies. In that case, the script parameters for each operation would define the currency again.
Here are some elements which can be configured:
Make sure the payment type is from an unlimited account, so payments in Cyclos won't fail because of funds. The way the example script is done, first the payment is executed in PayPal and, if authorized, a payment is made in Cyclos. If this payment fails, there could be an inconsistency between the Cyclos account an the PayPal payment. Improvements could be done to the script, to handle the case where the Cyclos payment failed. To do this, the ScriptHelper.addOnRollbackTransactional method can be used, for example, to notify some specific administrator or to refund the PayPal payment. But this handling is outside the scope of this example.
Loan features in Cyclos 4 can be implemented using scripting. As loans tend to be very specific for each project, having it implemented with scripts brings the possibility to tailor the behavior to each project.
The example provided works as follows:
In order to configure the loan script, follow carefully each of the following steps:
This can be checked under System > Currencies select the currency used for this operation, mark the Enable transfer number option and fill in the required parameters.
Under System > Accounts configuration > Transfer status flows, create a new one, with the following characteristics:
After saving, create the following statuses:
Under System > Accounts configuration > Payment fields, create a new one, with the following fields:
Under System > Accounts configuration > Account types, choose the (normally unlimited) account from which payments will be performed to users. Then set its internal name to some meaningful name. The example configuration uses debitUnits as internal name, but it can be changed later. Save the form.
Still in the system account type details page for the account type, on the Transfer types tab, create a new Payment transfer type with the following characteristics:
After saving, on the Payment fields tab, add the custom field named Repayment.
Under System > Accounts configuration > Account types, choose the user account which will receive payments. Then set its internal name to some meaningful name. The example configuration uses userUnits as internal name, but it can be changed later. Save the form.
Still in the user account type details page, on the Transfer types tab, create a new Payment transfer type with the following characteristics:
After saving, on the Payment fields tab, add the custom field named Loan.
Under System > Tools > Scripts, create a new library script, with the following characteristics:
# Loan configuration loan.account = debitUnits loan.type = loanGrant #loan.description = # Repayment configuration repayment.account = userUnits repayment.type = loanRepayment #repayment.description # Payment custom fields field.loan = loan field.repayment = repayment # Monthly compound interest rate (zero for none) monthlyInterestRate = 0 # Transfer status configuration status.flow = loan status.open = open status.closed = closed # Custom operation configuration operation.amount = amount operation.installments = installments operation.firstDueDate = firstDueDate # Messages message.invalidInstallments = The number of installments is invalid message.invalidLoanAmount = Invalid loan amount message.invalidFirstDueDate = The first due date cannot be lower than tomorrow message.loanGranted = The loan was successfully granted
import org.cyclos.entities.banking.Payment
import org.cyclos.entities.banking.PaymentTransferType
import org.cyclos.entities.banking.ScheduledPayment
import org.cyclos.entities.banking.SystemAccountType
import org.cyclos.entities.banking.TransactionCustomField
import org.cyclos.entities.banking.Transfer
import org.cyclos.entities.banking.TransferStatus
import org.cyclos.entities.banking.TransferStatusFlow
import org.cyclos.entities.banking.UserAccountType
import org.cyclos.entities.users.User
import org.cyclos.impl.banking.PaymentServiceLocal
import org.cyclos.impl.banking.ScheduledPaymentServiceLocal
import org.cyclos.impl.banking.TransferStatusServiceLocal
import org.cyclos.impl.system.ConfigurationAccessor
import org.cyclos.impl.system.ScriptHelper
import org.cyclos.impl.utils.persistence.EntityManagerHandler
import org.cyclos.model.ValidationException
import org.cyclos.model.banking.accounts.SystemAccountOwner
import org.cyclos.model.banking.transactions.PaymentVO
import org.cyclos.model.banking.transactions.PerformPaymentDTO
import org.cyclos.model.banking.transactions.PerformScheduledPaymentDTO
import org.cyclos.model.banking.transactions.ScheduledPaymentInstallmentDTO
import org.cyclos.model.banking.transactions.ScheduledPaymentVO
import org.cyclos.model.banking.transfers.TransferVO
import org.cyclos.model.banking.transferstatus.ChangeTransferStatusDTO
import org.cyclos.model.banking.transferstatus.TransferStatusVO
import org.cyclos.model.banking.transfertypes.TransferTypeVO
import org.cyclos.server.utils.DateHelper
import org.cyclos.utils.BigDecimalHelper
class Loan {
Map<String, Object> config
EntityManagerHandler entityManagerHandler
PaymentServiceLocal paymentService
ScheduledPaymentServiceLocal scheduledPaymentService
TransferStatusServiceLocal transferStatusService
ScriptHelper scriptHelper
ConfigurationAccessor configuration
double monthlyInterestRate
SystemAccountType systemAccount
UserAccountType userAccount
PaymentTransferType loanType
PaymentTransferType repaymentType
TransactionCustomField loanField
TransactionCustomField repaymentField
TransferStatusFlow flow
TransferStatus open
TransferStatus closed
Loan(binding) {
config = [:]
def params = binding.scriptParameters
[
'loan.account': 'systemAccount',
'loan.type': 'loanGrant',
'loan.description': null,
'repayment.account': 'userUnits',
'repayment.type': 'loanRepayment',
'repayment.description': null,
'field.loan': 'loan',
'field.repayment': 'repayment',
'monthlyInterestRate' : null,
'status.flow': 'loan',
'status.open': 'open',
'status.closed': 'closed',
'operation.amount': 'amount',
'operation.installments': 'installments',
'operation.firstDueDate': 'firstDueDate',
'message.invalidInstallments':
'The number of installments is invalid',
'message.invalidLoanAmount': 'Invalid loan amount',
'message.invalidFirstDueDate':
'The first due date cannot be lower than tomorrow',
'message.loanGranted':
'The loan was successfully granted to the user'
].each { k, v ->
def value = params[k] ?: v
config[k] = value
}
entityManagerHandler = binding.entityManagerHandler
paymentService = binding.paymentService
scheduledPaymentService = binding.scheduledPaymentService
transferStatusService = binding.transferStatusService
scriptHelper = binding.scriptHelper
configuration = binding.sessionData.configuration
systemAccount = entityManagerHandler.find(
SystemAccountType, config.'loan.account')
if (systemAccount.currency.transactionNumber == null
|| !systemAccount.currency.transactionNumber.used) {
throw new IllegalStateException(
"The currency ${systemAccount.currency.name} doesn't "
+ "have transaction number enabled")
}
loanType = entityManagerHandler.find(
PaymentTransferType, config.'loan.type', systemAccount)
userAccount = entityManagerHandler.find(
UserAccountType, config.'repayment.account')
repaymentType = entityManagerHandler.find(
PaymentTransferType, config.'repayment.type', userAccount)
if (!repaymentType.allowsScheduledPayments) {
throw new IllegalStateException("The repayment type " +
"${repaymentType.name} doesn't allows scheduled payment")
}
loanField = entityManagerHandler.find(
TransactionCustomField, config.'field.loan')
repaymentField = entityManagerHandler.find(
TransactionCustomField, config.'field.repayment')
if (!loanType.customFields.contains(repaymentField)) {
throw new IllegalStateException("The loan type ${loanType.name} "
+ "doesn't contain the custom field ${repaymentField.name}")
}
if (!repaymentType.customFields.contains(loanField)) {
throw new IllegalStateException("The repayment type "
+ "${repaymentType.name} doesn't contain the "
+ "custom field ${loanField.name}")
}
flow = entityManagerHandler.find(
TransferStatusFlow, config.'status.flow')
open = entityManagerHandler.find(
TransferStatus, config.'status.open', flow)
closed = entityManagerHandler.find(
TransferStatus, config.'status.closed', flow)
monthlyInterestRate = config.monthlyInterestRate?.toDouble() ?: 0
}
def BigDecimal calculateInstallmentAmount(BigDecimal amount,
int installments, Date grantDate, Date firstInstallmentDate) {
// Calculate the delay
Date shouldBeFirstExpiration = grantDate + 30
int delay = firstInstallmentDate - shouldBeFirstExpiration
if (delay < 0) {
delay = 0
}
double interest = monthlyInterestRate / 100.0
double numerator = ((1 + interest) **
(installments + delay / 30.0)) * interest
double denominator = ((1 + interest) ** installments) - 1
BigDecimal result = amount * numerator / denominator
return BigDecimalHelper.round(result, systemAccount.currency.precision)
}
def grant(User user, formParameters) {
BigDecimal loanAmount = formParameters[config.'operation.amount']
int installments = formParameters[config.'operation.installments']
Date firstDueDate = formParameters[config.'operation.firstDueDate']
Date minDate = DateHelper.shiftToNextDay(
new Date(), configuration.timeZone)
if (installments < 1 || installments > repaymentType.maxInstallments)
throw new ValidationException(config.'message.invalidInstallments')
if (loanAmount < 1)
throw new ValidationException(config.'message.invalidLoanAmount')
if (firstDueDate < minDate)
throw new ValidationException(config.'message.invalidFirstDueDate')
// Grant the loan
PaymentVO loanVO = paymentService.perform(new PerformPaymentDTO([
from: SystemAccountOwner.instance(),
to: user,
type: new TransferTypeVO(loanType.id),
amount: loanAmount,
description: config.'loan.description'
]))
Payment loan = entityManagerHandler.find(Payment, loanVO.id)
// Ensure the initial status is correct
Transfer loanTransfer = loan.transfer
if (loanTransfer == null) {
throw new IllegalStateException(
"The loan was not processed (probably pending authorization)")
}
TransferStatus currentStatus = loanTransfer.getStatus(flow)
if (currentStatus != open) {
throw new IllegalStateException(
"The initial status for flow ${flow.name} in ${loanType.name} "
+ "is not the expected one: ${open.name}, "
+ "but ${currentStatus} instead")
}
// Perform the repayment scheduled payment
PerformScheduledPaymentDTO dto = new PerformScheduledPaymentDTO()
def bean = scriptHelper.wrap(dto, [loanField])
bean.from = user
bean.to = SystemAccountOwner.instance()
bean.type = repaymentType
bean.amount = loanAmount
bean.description = config.'repayment.description'
bean.installmentsCount = installments
bean.firstInstallmentDate = firstDueDate
bean[loanField.internalName] = loan
// Interest
if (monthlyInterestRate > 0.00001) {
BigDecimal installmentAmount = calculateInstallmentAmount(
loanAmount, installments, new Date(), firstDueDate)
dto.installments = []
Date dueDate = firstDueDate
for (int i = 0; i < installments; i++) {
def installment = new ScheduledPaymentInstallmentDTO()
def instBean = scriptHelper.wrap(installment)
instBean.dueDate = dueDate
instBean.amount = installmentAmount
dto.installments << installment
dueDate += 30
}
bean.amount = installmentAmount * installments
}
ScheduledPaymentVO repaymentVO = scheduledPaymentService.perform(dto)
ScheduledPayment repayment = entityManagerHandler.find(
ScheduledPayment, repaymentVO.id)
// Update the loan with the repayment link
bean = scriptHelper.wrap(loan, [repaymentField])
bean[repaymentField.internalName] = repayment
}
def close(ScheduledPayment scheduledPayment) {
Payment loan = scriptHelper.wrap(scheduledPayment)
[loanField.internalName]
Transfer loanTransfer = loan.transfer
TransferStatus status = loanTransfer.getStatus(flow)
if (status != closed) {
// The loan was not closed: close it
transferStatusService.changeStatus(new ChangeTransferStatusDTO([
transfer: new TransferVO(loanTransfer.id),
newStatus: new TransferStatusVO(closed.id)
]))
}
}
}
Loan loan = new Loan(binding)Create a new script for the custom operation, with the following characteristics:
loan.grant(user, formParameters) return loan.config.'message.loanGranted'
Create a new script for the transaction extension point, with the following characteristics:
import org.cyclos.model.ValidationException
import org.cyclos.model.banking.transactions.ScheduledPaymentStatus
if (transaction.status == ScheduledPaymentStatus.CANCELED) {
// Should never cancel a loan scheduled payment
throw new ValidationException("Cannot cancel a loan")
} else if (transaction.status == ScheduledPaymentStatus.CLOSED) {
// Close the loan
loan.close(transaction)
}
Under System > Tools > Custom operations, create a new one, with the following characteristics:
After saving, create the following fields:
Under System > Tools > Extension points, create a new of type Transaction, with the following characteristics:
Under System > User configuration > Groups, select the Network administrators group. Then, in the Permissions tab:
In System > User configuration > Products (permissions), select the member product for users which will be able to receive loans. In the Custom operations field, make the Grant loan operation enabled. Leave the run checkbox unchecked (or users would be able to grant loans to themselves!).
You can permit users to to repay loan installments anticipated in Units. For this you have to check in the member product 'process installment' and the user need to have permissions to make a payment of the transaction type used for the loan repayments.