src/models/Process.php
<?php
namespace luya\payment\models;
use luya\admin\aws\DetailViewActiveWindow;
use luya\admin\ngrest\base\NgRestModel;
use luya\behaviors\JsonBehavior;
use luya\payment\PaymentException;
use Yii;
use yii\helpers\VarDumper;
/**
* Process.
*
* File has been created with `crud/create` command.
*
* @property integer $id
* @property string $salt
* @property string $hash
* @property string $random_key
* @property integer $amount
* @property string $currency
* @property string $order_id
* @property string $success_link
* @property string $error_link
* @property string $abort_link
* @property integer $close_state
* @property tinyint $is_closed
* @property integer $create_timestamp
* @property integer $close_timestamp
* @property integer $state_create
* @property integer $state_back
* @property integer $state_fail
* @property integer $state_abort
* @property integer $state_notify
* @property array $provider_data An optional json array which can store data about payment process. Data can merged at any step of the process.
*
* @author Basil Suter <basil@nadar.io>
* @since 1.0.0
*/
class Process extends NgRestModel
{
public const STATE_PENDING = 0;
public const STATE_SUCCESS = 1;
public const STATE_ERROR = 2;
public const STATE_ABORT = 3;
public $auth_token;
/**
* @inheritdoc
*/
public static function tableName()
{
return 'payment_process';
}
/**
* @inheritdoc
*/
public static function ngRestApiEndpoint()
{
return 'api-payment-process';
}
public function behaviors()
{
return [
[
'class' => JsonBehavior::class,
'attributes' => ['provider_data'],
],
];
}
/**
* @inheritdoc
*/
public function init()
{
// As the process is mainly used in frontend and can be used without admin module we detach the log behavior.
$this->detachBehavior('LogBehavior');
parent::init();
$this->on(self::EVENT_BEFORE_VALIDATE, function ($event) {
// ensure order_id is a string value even when its a number ohter whise validation would fail.
$this->order_id = (string) $this->order_id;
if ($this->isNewRecord) {
$this->create_timestamp = time();
$this->createTokens();
}
});
$this->on(self::EVENT_AFTER_INSERT, [$this, 'saveItems']);
}
private $_items;
public function setItems(array $items)
{
$this->_items = $items;
}
public function saveItems()
{
foreach ($this->_items as $item) {
$itemModel = new ProcessItem();
$itemModel->process_id = $this->id;
$itemModel->qty = $item['qty'];
$itemModel->amount = $item['amount'];
$itemModel->name = $item['name'];
$itemModel->total_amount = $item['total_amount'];
$itemModel->is_shipping = $item['is_shipping'];
$itemModel->is_tax = $item['is_tax'];
if (!$itemModel->save()) {
throw new PaymentException("Unable to store the process item due to validation errors.");
}
}
}
/**
* @inheritdoc
*/
public function attributeLabels()
{
return [
'id' => Yii::t('app', 'ID'),
'salt' => Yii::t('app', 'Salt'),
'hash' => Yii::t('app', 'Hash'),
'random_key' => Yii::t('app', 'Random Key'),
'amount' => Yii::t('app', 'Amount'),
'currency' => Yii::t('app', 'Currency'),
'order_id' => Yii::t('app', 'Order ID'),
'success_link' => Yii::t('app', 'Success Link'),
'error_link' => Yii::t('app', 'Error Link'),
'abort_link' => Yii::t('app', 'Abort Link'),
'close_state' => Yii::t('app', 'Close State'),
'is_closed' => Yii::t('app', 'Is Closed'),
'create_timestamp' => Yii::t('app', 'Created at'),
'close_timestamp' => Yii::t('app', 'Closed Timestamp'),
];
}
/**
* @inheritdoc
*/
public function rules()
{
return [
[['salt', 'hash', 'random_key', 'amount', 'currency', 'order_id', 'success_link', 'error_link', 'abort_link'], 'required'],
[['amount', 'close_state', 'is_closed', 'create_timestamp', 'close_timestamp', 'state_notify', 'state_abort', 'state_fail', 'state_back', 'state_create'], 'integer'],
[['salt', 'hash'], 'string', 'max' => 120],
[['random_key'], 'string', 'max' => 32],
[['currency'], 'string', 'max' => 10],
[['order_id'], 'string', 'max' => 50],
[['success_link', 'error_link', 'abort_link'], 'string', 'max' => 255],
[['hash'], 'unique'],
[['random_key'], 'unique'],
[['items'], 'safe'],
[['provider_data'], 'each', 'rule' => ['safe']],
];
}
/**
* @inheritdoc
*/
public function ngRestAttributeTypes()
{
return [
'salt' => 'text',
'hash' => 'text',
'random_key' => 'text',
'amount' => 'number',
'currency' => 'text',
'order_id' => 'text',
'success_link' => 'text',
'error_link' => 'text',
'abort_link' => 'text',
'close_state' => ['selectArray', 'emptyListValue' => false, 'data' => [self::STATE_PENDING => 'Pending', self::STATE_SUCCESS => 'Success', self::STATE_ABORT => 'Aborted', self::STATE_ERROR => 'Error']],
'is_closed' => ['toggleStatus', 'interactive' => false],
'create_timestamp' => 'datetime',
'provider_data' => 'raw',
];
}
public function fields()
{
$fields = parent::fields();
$fields['auth_token'] = 'auth_token';
return $fields;
}
public function getFormatedAmount()
{
return Yii::$app->formatter->asCurrency($this->amount / 100, $this->currency);
}
public function ngRestExtraAttributeTypes()
{
return [
'formatedAmount' => 'text',
];
}
/**
* @inheritdoc
*/
public function ngRestScopes()
{
return [
['list', ['order_id', 'create_timestamp', 'formatedAmount', 'close_state']],
];
}
public function ngRestFilters()
{
return [
'Successful' => self::ngRestFind()->andWhere(['close_state' => self::STATE_SUCCESS]),
'Pending' => self::ngRestFind()->andWhere(['close_state' => self::STATE_PENDING]),
'Error' => self::ngRestFind()->andWhere(['close_state' => self::STATE_ERROR]),
'Abort' => self::ngRestFind()->andWhere(['close_state' => self::STATE_ABORT]),
];
}
/**
* @inheritdoc
*/
public function ngRestActiveWindows()
{
return [
[
'class' => DetailViewActiveWindow::class,
'attributes' => [
[
'attribute' => 'amount',
'value' => function ($model) {
return $model->getFormatedAmount();
}
],
'currency',
'order_id',
'success_link',
'error_link',
'abort_link',
'close_state',
'is_closed:boolean',
'create_timestamp:datetime',
[
'attribute' => 'provider_data',
'value' => function ($model) {
return VarDumper::dumpAsString($model->provider_data);
},
'contentOptions' => ['encode' => false, 'encoding' => false]
],
]
],
];
}
public function ngRestRelations()
{
return [
['label' => 'Articles', 'targetModel' => ProcessItem::class,'apiEndpoint' => ProcessItem::ngRestApiEndpoint(), 'dataProvider' => $this->getItems()],
['label' => 'Log', 'targetModel' => ProcessTrace::class,'apiEndpoint' => ProcessTrace::ngRestApiEndpoint(), 'dataProvider' => $this->getTraces()],
];
}
/**
* Get related items
*
* @return ProcessItem[]
*/
public function getItems()
{
return $this->hasMany(ProcessItem::class, ['process_id' => 'id']);
}
/**
* Get related trace events
*
* @return ProcessTrace[]
*/
public function getTraces()
{
return $this->hasMany(ProcessTrace::class, ['process_id' => 'id']);
}
/**
* Create variables based on the input key.
*
* 1. Generate a random string
* 2. generate a password hash based on random string and input key stored in $auth_token
* 3. Generate a salt random string
* 4. generate a password hash from salt and auth token
* 5. Base 64 encode the auth token
* 6. Generate randon key and md5
*
* Restore and ensure in application:
*
* 1. Get the model from with the random key
* 2. Validate the auth token against this model
* a. decode the auth token
* b. validate the auth token against the hash from the model.
*
* Creates and assignes values to:
*
* + auth_token
* + salt
* + hash
* + random_key
*
* @param [type] $inputKey
* @return void
*/
public function createTokens()
{
$inputKey = $this->order_id;
$security = Yii::$app->security;
// random string
$random = $security->generateRandomString(32);
// generate the auth token based from the random string and the inputKey
$this->auth_token = $security->generatePasswordHash($random . $inputKey);
// random salt string
$this->salt = $security->generateRandomString(32);
// generate a hash to compare the auth token from the salt and auth token
$this->hash = $security->generatePasswordHash($this->salt . $this->auth_token);
// encode the token with base 64 in order to remove conflicting http url signs
$this->auth_token = base64_encode($this->auth_token);
// generate a random key to add for for the transaction itself.
$this->random_key = md5($security->generaterandomKey());
}
/**
* Validate the auth token against model hash
*
* @return void
*/
public function validateAuthToken()
{
$token = base64_decode($this->auth_token);
return Yii::$app->security->validatePassword($this->salt.$token, $this->hash);
}
/**
* Payment trace short hand.
*
* @param string $eventType
* @param string $message
* @return boolean Whether saving was successfull or not.
*/
public function addPaymentTraceEvent($eventType, $message = null)
{
$model = new ProcessTrace();
$model->process_id = $this->id;
$model->event = $eventType;
$model->message = $message;
return $model->save();
}
}