Initial commit

This commit is contained in:
Chris Smith
2025-02-19 14:51:16 +01:00
commit d82a6cad96
198 changed files with 13819 additions and 0 deletions

15
common/codeception.yml Normal file
View File

@@ -0,0 +1,15 @@
namespace: common\tests
actor_suffix: Tester
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
bootstrap: _bootstrap.php
settings:
colors: true
memory_limit: 1024M
modules:
config:
Yii2:
configFile: 'config/codeception-local.php'

View File

@@ -0,0 +1,19 @@
<?php
namespace common\components;
use Postmark\PostmarkClient;
use yii\base\Component;
class PostmarkComponent extends Component
{
/** @var PostmarkClient */
public PostmarkClient $client;
public mixed $serverToken;
public function init()
{
parent::init();
$this->client = new PostmarkClient($this->serverToken);
}
}

View File

@@ -0,0 +1,469 @@
<?php
namespace common\components;
use common\jobs\EmailJob;
use common\models\Account;
use common\models\InvoiceItem;
use common\models\SalesAgent;
use common\models\Service;
use Yii;
use yii\helpers\Url;
use yii\httpclient\Client;
use yii\httpclient\Request;
use yii\httpclient\RequestEvent;
class SonarApiComponent extends \yii\base\Component
{
public Client $client;
public string $baseUrl;
public string $bearerToken;
public function init()
{
parent::init();
$this->client = new Client([
'baseUrl' => $this->baseUrl,
'requestConfig' => [
'format' => Client::FORMAT_JSON
],
'responseConfig' => [
'format' => Client::FORMAT_JSON
],
]);
// Setup event for auth before each send
$this->client->on(Request::EVENT_BEFORE_SEND, function (RequestEvent $event) {
$event->request->addHeaders(['Authorization' => 'Bearer ' . $this->bearerToken]);
});
}
public function getAccounts(int $page = 1, int $limit = 100)
{
$data = [
'form_params' => [
'query' => 'query accountsWithSalesAgents(
$paginator: Paginator,
$search: [Search],
$sorter: [Sorter]
) {
accounts(
paginator: $paginator
search: $search
sorter: $sorter
reverse_relation_filters: {
relation: "custom_field_data",
search: {
integer_fields: {
attribute: "custom_field_id",
search_value: 12,
operator: EQ
}
}
}
general_search_mode: ROOT_PLUS_RELATIONS
account_status_id: 1
) {
entities {
id
name
account_status {
id
name
}
account_services {
entities {
id
quantity
name_override
price_override
price_override_reason
service {
id
name
amount
enabled
application
type
}
}
page_info {
page
records_per_page
total_count
total_pages
}
}
custom_field_data(custom_field_id:12) {
entities {
id
custom_field_id
value
}
}
}
page_info {
page
records_per_page
total_count
total_pages
}
}
}',
'variables' => [
'paginator' => [
'page' => $page,
'records_per_page' => $limit
],
'search' => [],
'sorter' => [
[
'attribute' => 'updated_at',
'direction' => 'ASC',
]
],
]
]
];
$response = $this->client->createRequest()
->setMethod('POST')
->setData($data)
->send();
$account = json_decode($response->getContent(), true);
return $account['form_params']['data'];
}
public function getInvoices(string $startDate, string $endDate)
{
$page = 1;
$limit = 100;
$invoices = [];
do {
$data = [
'form_params' => [
'query' => 'query accountInvoice($paginator: Paginator, $search: [Search], $sorter: [Sorter]) {
invoices(
paginator: $paginator
search: $search
sorter: $sorter
general_search_mode: ROOT_PLUS_RELATIONS
) {
entities {
id
account_id
total_debits
void
remaining_due
date
due_date
end_date
delinquent
debits {
entities {
id
quantity
service_id
service_name
amount
}
}
credits {
entities {
amount
}
}
}
page_info {
page
records_per_page
total_count
total_pages
}
}
}',
'variables' => [
'paginator' => [
'page' => $page,
'records_per_page' => $limit
],
'search' => [
[
'date_fields' => [
['attribute' => 'date', 'search_value' => $startDate, 'operator' => 'GTE'],
['attribute' => 'date', 'search_value' => $endDate, 'operator' => 'LTE'],
]
]
]
],
'sorter' => [
[
'attribute' => 'updated_at',
'direction' => 'ASC',
]
],
]
];
$response = $this->client->createRequest()
->setMethod('POST')
->setData($data)
->send();
$responseData = json_decode($response->getContent(), true);
$invoices = array_merge($invoices, $responseData['form_params']['data']['invoices']['entities']);
$page++;
} while ($page < ($responseData['form_params']['data']['invoices']['page_info']['total_pages'] + 1));
return $invoices;
}
public function getInvoice(int $invoiceId = 1)
{
$page = 1;
$limit = 100;
$data = [
'form_params' => [
'query' => 'query accountInvoice($paginator: Paginator, $search: [Search], $sorter: [Sorter]) {
invoices(
id: ' . $invoiceId . '
paginator: $paginator
search: $search
sorter: $sorter
general_search_mode: ROOT_PLUS_RELATIONS
) {
entities {
id
account_id
total_debits
void
remaining_due
date
due_date
end_date
delinquent
debits {
entities {
id
quantity
service_id
service_name
amount
}
}
credits {
entities {
amount
}
}
}
page_info {
page
records_per_page
total_count
total_pages
}
}
}',
'variables' => [
'paginator' => [
'page' => $page,
'records_per_page' => $limit
],
'search' => [],
'sorter' => [
[
'attribute' => 'updated_at',
'direction' => 'ASC',
]
],
]
]
];
$response = $this->client->createRequest()
->setMethod('POST')
->setData($data)
->send();
$invoice = json_decode($response->getContent(), true);
return $invoice['form_params']['data']['invoices']['entities'][0];
}
public function storeInvoices($invoices)
{
foreach ($invoices as $invoice) {
$this->storeInvoice($invoice);
}
}
public function storeInvoice($invoice)
{
\Yii::debug($invoice);
// $remainingDue is the Entire Invoice remaining to be paid amount, 0 = everything paid
$remainingDue = $invoice['remaining_due'];
// debits = charges on the account
// credits = payments on the account
foreach ($invoice['debits']['entities'] as $i => $rawItem) {
$invoiceItem = InvoiceItem::find()->where(['sonar_id' => (int)$rawItem['id']])->one();
if (null === $invoiceItem) { // create new invoice item
$account = Account::findOne(['sonar_id' => (int)$invoice['account_id']]);
$service = Service::findOne(['sonar_id' => (int)$rawItem['service_id']]);
\Yii::debug($rawItem);
if ($service && $account) {
\Yii::debug($invoice);
$payment = (isset($invoice['credits']['entities'][$i]['amount'])) ? $invoice['credits']['entities'][$i]['amount'] : 0;
// @todo check payment - i think it is wrong to assume we have the same credits and debits ^ CGS
$invoiceItem = new InvoiceItem([
'sonar_id' => (int)$rawItem['id'],
'account_id' => $account->id,
'service_id' => $service->id,
'name' => $rawItem['service_name'],
'status' => InvoiceItem::STATUS_OPEN,
'charge' => $rawItem['amount'],
'payment' => $payment,
'is_commissionable' => $service->hasCommission(),
]);
$invoiceItem->save();
}
}
// is the invoice item paid?
if ($invoiceItem && $remainingDue == 0) {
$invoiceItem->status = InvoiceItem::STATUS_PAYMENT_RECEIVED;
$invoiceItem->save();
}
}
}
private function mapAccounts($accounts)
{
$mapped = [];
$i = 0;
$db = \Yii::$app->db;
foreach ($accounts as $account) {
$mapped[$i]['sonar_id'] = (int)$account['id'];
$mapped[$i]['name'] = $account['name'];
/**
* [
* 'id' => '132'
* 'quantity' => 1
* 'name_override' => 'Bradley'
* 'price_override' => 0
* 'price_override_reason' => 'testing'
* 'service' => [
* 'id' => '10'
* 'name' => 'Business Giga Speed Internet'
* 'amount' => 18000
* 'enabled' => true
* 'application' => 'DEBIT'
* 'type' => 'DATA'
* ]
* ]
*/
$mapped[$i]['services'] = []; // init empty array
foreach ($account['account_services']['entities'] as $key => $account_service) {
$mapped[$i]['services'][$key]['sonar_id'] = (int)$account_service['service']['id'];
$mapped[$i]['services'][$key]['name'] = (!empty($account_service['name_override'])) ? $account_service['name_override'] : $account_service['service']['name'];
$mapped[$i]['services'][$key]['price'] = (!empty($account_service['price_override'])) ? $account_service['price_override'] : $account_service['service']['amount'];
if ($account_service['service']['application'] === 'CREDIT') {
$mapped[$i]['services'][$key]['price'] = -1 * $mapped[$i]['services'][$key]['price'];// store as a negative
}
// set to 0 if null after credit
if (null === $mapped[$i]['services'][$key]['price']) {
$mapped[$i]['services'][$key]['price'] = 0;
}
}
$name = $account['custom_field_data']['entities'][0]['value'];
$salesAgent = SalesAgent::findOne(['name' => $name]);
if (null === $salesAgent) {
$salesAgent = new SalesAgent(['name' => $name]);
$salesAgent->save();
}
$mapped[$i]['sales_agent_id'] = $salesAgent->id;
$i++;
}
return $mapped;
}
public function storeAccounts()
{
$page = 1;
do {
$batch = $this->getAccounts($page, 100);
$accounts = $this->mapAccounts($batch['accounts']['entities']);
foreach ($accounts as $account) {
$accountModel = Account::findOne(['sonar_id' => $account['sonar_id']]);
if (null === $accountModel) {
$accountModel = new Account([
'sonar_id' => $account['sonar_id'],
'name' => $account['name'],
'sales_agent_id' => $account['sales_agent_id'],
]);
$accountModel->save();
} else {
//$accountModel->sonar_id = $account['sonar_id'];
$accountModel->name = $account['name'];
$accountModel->sales_agent_id = $account['sales_agent_id'];
$accountModel->save();
}
foreach ($account['services'] as $rawServiceData) {
$serviceModel = Service::findOne(['sonar_id' => (int)$rawServiceData['sonar_id']]);
if (null === $serviceModel) {
$serviceModel = new Service([
'sonar_id' => $rawServiceData['sonar_id'],
'name' => $rawServiceData['name'],
'price' => $rawServiceData['price'],
'account_id' => $accountModel->id,
'active' => 1, // @todo pull active state in from sonar api
]);
$serviceModel->save();
} else {
$serviceModel->commission = $serviceModel->getFormattedDollar($serviceModel->commission, false);
$serviceModel->sonar_id = $rawServiceData['sonar_id'];
$serviceModel->name = $rawServiceData['name'];
$serviceModel->price = $rawServiceData['price'];
$serviceModel->account_id = $accountModel->id;
if (!empty($serviceModel->dirtyAttributes)) {
if (isset($serviceModel->dirtyAttributes['price'])) {
//Yii::$app->queue->push(new EmailJob([
// 'templateAlias' => EmailJob::PRICE_CHANGE,
// "email" => Yii::$app->params['adminEmail'],
// 'templateModel' => [
// "action_edit_url" => Yii::$app->urlManager->createAbsoluteUrl(
// ['service/update', 'id' => $serviceModel->id]
// ),
// ]
//]));
}
}
$serviceModel->save();
}
}
}
$page++;
} while ($page < ($batch['accounts']['page_info']['total_pages'] + 1));
}
public function processInvoices(int $invoiceId)
{
dump($this->getInvoice($invoiceId));
}
}

4
common/config/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
codeception-local.php
main-local.php
params-local.php
test-local.php

View File

@@ -0,0 +1,37 @@
<?php
/**
* This class only exists here for IDE (PHPStorm/Netbeans/...) autocompletion.
* This file is never included anywhere.
* Adjust this file to match classes configured in your application config, to enable IDE autocompletion for custom components.
* Example: A property phpdoc can be added in `__Application` class as `@property \vendor\package\Rollbar|__Rollbar $rollbar` and adding a class in this file
* ```php
* // @property of \vendor\package\Rollbar goes here
* class __Rollbar {
* }
* ```
*/
class Yii {
/**
* @var \yii\web\Application|\yii\console\Application|__Application
*/
public static $app;
}
/**
* @property yii\rbac\DbManager $authManager
* @property \Da\User\Model\User $user
* @property \common\components\SonarApiComponent $sonar
* @property \common\components\HubspotApiComponent $hubspot
* @property \common\components\PostmarkComponent $postmark
* @property \yii\queue\db\Queue $queue
*
*/
class __Application {
}
/**
* @property app\models\User $identity
*/
class __WebUser {
}

View File

@@ -0,0 +1,5 @@
<?php
Yii::setAlias('@common', dirname(__DIR__));
Yii::setAlias('@frontend', dirname(dirname(__DIR__)) . '/frontend');
Yii::setAlias('@backend', dirname(dirname(__DIR__)) . '/api');
Yii::setAlias('@console', dirname(dirname(__DIR__)) . '/console');

44
common/config/main.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
use common\components\PostmarkComponent;
use common\components\SonarApiComponent;
use common\components\HubspotApiComponent;
use yii\caching\FileCache;
use yii\queue\db\Queue;
$params = array_merge(
require __DIR__ . '/params.php',
require __DIR__ . '/params-local.php'
);
return [
'name' => $params['company_name'] . ' - ' . $params['product_name'],
'aliases' => [
'@bower' => '@vendor/bower-asset',
'@npm' => '@vendor/npm-asset',
],
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'components' => [
'cache' => [
'class' => FileCache::class,
],
'sonar' => [
'class' => SonarApiComponent::class,
'baseUrl' => $params['sonar.url'] . '/api/graphql',
'bearerToken' => $params['sonar.bearerToken'],
],
'postmark' => [
'class' => PostmarkComponent::class,
'serverToken' => $params['postmark.serverToken'],
],
'authManager' => [
'class' => 'yii\rbac\DbManager',
],
'urlManager' => [
'enablePrettyUrl' => true,
'showScriptName' => false,
'rules' => [
],
]
],
];

14
common/config/params.php Normal file
View File

@@ -0,0 +1,14 @@
<?php
return [
'adminEmail' => 'admin@example.com',
'supportEmail' => 'support@example.com',
'senderEmail' => 'noreply@example.com',
'senderName' => 'Example.com mailer',
'user.passwordResetTokenExpire' => 3600,
'user.passwordMinLength' => 8,
'sonar.url' => 'https://yourname.sonar.software',
'sonar.bearerToken' => '',
'postmark.serverToken' => 'postmark-server-key',
'postmark.messageStream' => 'outbound',
'sentry.dsn' => 'https://asdf@o4507934844780544.ingest.us.sentry.io/4508006893158400',
];

11
common/config/test.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
return [
'id' => 'app-common-tests',
'basePath' => dirname(__DIR__),
'components' => [
'user' => [
'class' => \yii\web\User::class,
'identityClass' => 'common\models\User',
],
],
];

View File

@@ -0,0 +1,11 @@
<?php
namespace common\fixtures;
use yii\test\ActiveFixture;
class AuthAssignmentFixture extends ActiveFixture
{
public $tableName = '{{%auth_assignment}}';
public $depends = [UserFixture::class];
}

View File

@@ -0,0 +1,11 @@
<?php
namespace common\fixtures;
use yii\test\ActiveFixture;
class UserFixture extends ActiveFixture
{
public $modelClass = 'common\models\User';
public $depends = [];
}

View File

@@ -0,0 +1,12 @@
<?php
return [
[
'item_name' => 'admin',
'user_id' => 1,
],
[
'item_name' => 'user',
'user_id' => 2,
],
];

View File

@@ -0,0 +1,22 @@
<?php
return [
[
//password
'password_hash' => '$2y$13$k2hQ8aV/z6V0Y7pRbq1ufOUSaJI7EhNvvTUIoj2s/rxAmgyY95KPa',
'email' => 'admin@example.com',
'auth_key' => '1',
'status' => '1',
'created_at' => '1402312317',
'updated_at' => '1402312317',
],
[
//password
'password_hash' => '$2y$13$k2hQ8aV/z6V0Y7pRbq1ufOUSaJI7EhNvvTUIoj2s/rxAmgyY95KPa',
'email' => 'user@example.com',
'auth_key' => '1',
'status' => '1',
'created_at' => '1402312317',
'updated_at' => '1402312317',
],
];

65
common/jobs/EmailJob.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
namespace common\jobs;
use Yii;
use yii\base\BaseObject;
use yii\queue\RetryableJobInterface;
class EmailJob extends BaseObject implements RetryableJobInterface
{
const MAX_ATTEMPTS = 3;
const TTR = 600; // in seconds
public const PASSWORD_RESET = 'password-reset';
public const PASSWORD_HAS_BEEN_RESET = 'password-has-been-reset';
public const WELCOME_EMAIL = 'welcome-email';
public const VERIFY_EMAIL = 'verify-email';
public const ADMIN_NOTIFY = 'admin-new-user';
public const PRICE_CHANGE = 'price-change';
public const PAYOUT_NOTIFY = 'payout-notify';
public array $templateModel;
public string $email;
public string $templateAlias;
/**
* @inheritDoc
*/
public function execute($queue)
{
// Merge these values which are always on emails
$this->templateModel = array_merge($this->templateModel, [
"product" => Yii::$app->params['product_name'],
"product_name" => Yii::$app->params['product_name'],
"support_url" => Yii::$app->params['support_url'],
"product_url" => Yii::$app->urlManager->createAbsoluteUrl(['site/index']),
"company_name" => Yii::$app->params['company_name'],
"company_address" => Yii::$app->params['company_address'],
]);
Yii::$app->postmark->client->sendEmailWithTemplate(
Yii::$app->params['supportEmail'],
$this->email,
$this->templateAlias,
$this->templateModel,
messageStream: Yii::$app->params['postmark.messageStream']
);
}
/**
* @inheritDoc
*/
public function getTtr()
{
return self::TTR;
}
/**
* @inheritDoc
*/
public function canRetry($attempt, $error)
{
return ($attempt < self::MAX_ATTEMPTS);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace common\models;
use yii\db\ActiveRecord;
class AuthAssignment extends ActiveRecord
{
public static function tableName()
{
return '{{%auth_assignment}}';
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace common\models;
use Yii;
use yii\base\Model;
/**
* Login form
*/
class LoginForm extends Model
{
public $email;
public $password;
public $rememberMe = true;
private $_user;
/**
* {@inheritdoc}
*/
public function rules()
{
return [
// username and password are both required
[['email', 'password'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* @param string $attribute the attribute currently being validated
* @param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect email or password.');
}
}
}
/**
* Logs in a user using the provided email and password.
*
* @return bool whether the user is logged in successfully
*/
public function login()
{
if ($this->validate()) {
return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600 * 24 * 30 : 0);
}
return false;
}
/**
* Finds user by [[email]]
*
* @return User|null
*/
protected function getUser()
{
if ($this->_user === null) {
$this->_user = User::findByEmail($this->email);
}
return $this->_user;
}
}

61
common/models/Meal.php Normal file
View File

@@ -0,0 +1,61 @@
<?php
namespace common\models;
/**
* This is the model class for table "meal".
*
* @property int $id
* @property string $file_name
* @property int $calories
* @property int $protein
* @property int $fat
* @property int $carbohydrates
* @property int $fiber
* @property int $meal
* @property int $created_at
* @property int $updated_at
*/
class Meal extends \yii\db\ActiveRecord
{
/**
* {@inheritdoc}
*/
public static function tableName()
{
return 'meal';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['file_name', 'calories', 'protein', 'fat', 'carbohydrates', 'fiber', 'meal', 'created_at', 'updated_at'], 'required'],
[['calories', 'protein', 'fat', 'carbohydrates', 'fiber', 'meal', 'created_at', 'updated_at'], 'integer'],
[['file_name'], 'string', 'max' => 255],
];
}
/**
* {@inheritdoc}
*/
public function attributeLabels()
{
return [
'id' => 'ID',
'file_name' => 'File Name',
'calories' => 'Calories',
'protein' => 'Protein',
'fat' => 'Fat',
'carbohydrates' => 'Carbohydrates',
'fiber' => 'Fiber',
'meal' => 'Meal',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
];
}
}

332
common/models/User.php Normal file
View File

@@ -0,0 +1,332 @@
<?php
namespace common\models;
use common\jobs\EmailJob;
use Yii;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\AfterSaveEvent;
use yii\web\IdentityInterface;
/**
* User model
*
* @property integer $id
* @property string $password_hash
* @property string $password_reset_token
* @property string $verification_token
* @property string $email
* @property string $first_name
* @property string $name
* @property string $auth_key
* @property integer $status
* @property integer $sales_agent_id
* @property integer $created_at
* @property integer $updated_at
* @property string $password write-only password
*/
class User extends ActiveRecord implements IdentityInterface
{
// User statuses
public const STATUS_UNVERIFIED = 10;
public const STATUS_INACTIVE = 0;
public const STATUS_ACTIVE = 1;
public const STATUS_VERIFIED = 2;
public const PAYOUT_INTERVAL_MONTHLY = 0;
public array $userStatusArray;
public string $role = '';
public bool $welcomeEmailSent = false;
public string $firstName;
public function init()
{
parent::init();
$this->userStatusArray = [
self::STATUS_UNVERIFIED => Yii::t('app', 'Unverified'),
self::STATUS_INACTIVE => Yii::t('app', 'Inactive'),
self::STATUS_ACTIVE => Yii::t('app', 'Active'),
self::STATUS_VERIFIED => Yii::t('app', 'Verified (not active)'),
];
// register event
$this->on(self::EVENT_AFTER_INSERT, [$this, 'emailTrigger']);
$this->on(self::EVENT_AFTER_UPDATE, [$this, 'emailTrigger']);
}
public function emailTrigger(AfterSaveEvent $event)
{
if ($event->sender->status == self::STATUS_ACTIVE && !$event->sender->welcome_email_sent) {
Yii::$app->queue->push(new EmailJob([
'templateAlias' => EmailJob::WELCOME_EMAIL,
'email' => $event->sender->email,
'templateModel' => [
"name" => $event->sender->first_name,
"action_url" => Yii::$app->urlManager->createAbsoluteUrl(['site/login']),
]
]));
$event->sender->welcome_email_sent = true;
$event->sender->save(false);
}
}
/**
* {@inheritdoc}
*/
public static function tableName()
{
return '{{%user}}';
}
/**
* {@inheritdoc}
*/
public function rules()
{
return [
['status', 'default', 'value' => self::STATUS_UNVERIFIED],
[['email'], 'email'],
[['email'], 'unique'],
[['sales_agent_id', 'created_at', 'updated_at'], 'integer'],
[
'sales_agent_id',
'required',
'when' => function ($model) {
return $model->role === 'sales-agent';
},
'whenClient' => "function (attribute, value) {
return $('#role').val() == 'sales-agent';
}"
],
[
'status',
'in',
'range' => [
self::STATUS_ACTIVE,
self::STATUS_UNVERIFIED,
self::STATUS_VERIFIED,
self::STATUS_INACTIVE,
]
],
[['role'], 'string'],
];
}
public function afterSave($insert, $changedAttributes)
{
$auth = Yii::$app->authManager;
// delete exiting roles if set
$auth->revokeAll($this->id);
// assign new role
if (!empty($this->role)) {
$auth->assign($auth->getRole($this->role), $this->id);
}
parent::afterSave($insert, $changedAttributes);
}
public function afterFind()
{
$rolesAssignedToUser = Yii::$app->authManager->getRolesByUser($this->id);
// we only use one role for the user
if (!empty($rolesAssignedToUser)) {
$this->role = array_key_first($rolesAssignedToUser);
}
parent::afterFind();
}
/**
* {@inheritdoc}
*/
public function behaviors()
{
return [
TimestampBehavior::class,
];
}
/**
* Get names for dropdown lists
*
* @param $dropdown
* @return array|mixed
*/
public function getStatusName($dropdown = false)
{
return $dropdown ? $this->userStatusArray : $this->userStatusArray[$this->status];
}
public function getAuthAssignment()
{
return $this->hasOne(\common\models\AuthAssignment::class, ['user_id' => 'id']);
}
/**
* {@inheritdoc}
*/
public static function findIdentity($id)
{
return static::findOne(['id' => $id, 'status' => self::STATUS_ACTIVE]);
}
/**
* Finds user by email
*
* @param string $email
* @return ActiveRecord|array|null
*/
public static function findByEmail($email)
{
return static::findOne(['email' => $email, 'status' => self::STATUS_ACTIVE]);
}
/**
* {@inheritdoc}
*/
public static function findIdentityByAccessToken($token, $type = null)
{
throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
}
/**
* Finds user by password reset token
*
* @param string $token password reset token
* @return ActiveRecord|array|null
*/
public static function findByPasswordResetToken($token)
{
if (!static::isPasswordResetTokenValid($token)) {
return null;
}
return static::findOne(['password_reset_token' => $token, 'status' => self::STATUS_ACTIVE]);
}
/**
* Finds user by verification email token
*
* @param string $token verify email token
* @return static|null
*/
public static function findByVerificationToken($token)
{
return static::findOne([
'verification_token' => $token,
'status' => self::STATUS_UNVERIFIED
]);
}
/**
* Finds out if password reset token is valid
*
* @param string $token password reset token
* @return bool
*/
public static function isPasswordResetTokenValid($token)
{
if (empty($token)) {
return false;
}
$timestamp = (int)substr($token, strrpos($token, '_') + 1);
$expire = Yii::$app->params['user.passwordResetTokenExpire'];
return $timestamp + $expire >= time();
}
/**
* {@inheritdoc}
*/
public function getId()
{
return $this->getPrimaryKey();
}
/**
* {@inheritdoc}
*/
public function getAuthKey()
{
return $this->auth_key;
}
/**
* {@inheritdoc}
*/
public function validateAuthKey($authKey)
{
return $this->getAuthKey() === $authKey;
}
/**
* Validates password
*
* @param string $password password to validate
* @return bool if password provided is valid for current user
*/
public function validatePassword($password)
{
return Yii::$app->security->validatePassword($password, $this->password_hash);
}
/**
* Generates password hash from password and sets it to the model
*
* @param string $password
*/
public function setPassword($password)
{
$this->password_hash = Yii::$app->security->generatePasswordHash($password);
}
/**
* Generates "remember me" authentication key
*/
public function generateAuthKey()
{
$this->auth_key = Yii::$app->security->generateRandomString();
}
/**
* Generates new password reset token
*/
public function generatePasswordResetToken()
{
$this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time();
}
/**
* Generates new token for email verification
*/
public function generateEmailVerificationToken()
{
$this->verification_token = Yii::$app->security->generateRandomString() . '_' . time();
}
/**
* Removes password reset token
*/
public function removePasswordResetToken()
{
$this->password_reset_token = null;
}
/**
* Gets query for [[SalesAgent]].
*
* @return \yii\db\ActiveQuery|yii\db\ActiveQuery
*/
public function getSalesAgent()
{
return $this->hasOne(SalesAgent::class, ['id' => 'sales_agent_id']);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace common\models\search;
use common\models\User as UserModel;
use Yii;
use yii\base\Model;
use yii\data\ActiveDataProvider;
use common\models\SalesAgent;
class User extends UserModel
{
public string $role = '';
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'status'], 'integer'],
[['email', 'first_name', 'role'], 'safe'],
];
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = UserModel::find()
->joinWith('authAssignment', 'salesAgent');
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->sort->attributes['salesAgentName'] = [
'asc' => ['sales_agent_id' => SORT_ASC],
'desc' => ['sales_agent_id' => SORT_DESC],
];
$dataProvider->sort->attributes['role'] = [
'asc' => ['auth_assignment.item_name' => SORT_ASC],
'desc' => ['auth_assignment.item_name' => SORT_DESC],
];
$this->load($params);
if (!$this->validate()) {
return $dataProvider;
}
if (!empty($this->salesAgentName)) {
$salesAgent = SalesAgent::find()->where(['name' => $this->salesAgentName])->one();
$this->sales_agent_id = $salesAgent ? $salesAgent->id : null;
}
$query->andFilterWhere([
'id' => $this->id,
'status' => $this->status,
'sales_agent_id' => $this->sales_agent_id,
'auth_assignment.item_name' => $this->role
]);
$query->andFilterWhere(['like', 'email', $this->email]);
return $dataProvider;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace common\rbac;
class SalesAgentRule extends \yii\rbac\Rule
{
public $name = 'assignedToSalesAgent';
/**
* @inheritDoc
*/
public function execute($user, $item, $params)
{
return isset($params['post']) && $params['post']->sales_agent_id == $user;
}
}

View File

@@ -0,0 +1,9 @@
<?php
defined('YII_DEBUG') or define('YII_DEBUG', true);
defined('YII_ENV') or define('YII_ENV', 'test');
defined('YII_APP_BASE_PATH') or define('YII_APP_BASE_PATH', __DIR__.'/../../');
require_once __DIR__ . '/../../vendor/autoload.php';
require_once __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
require __DIR__ . '/../config/bootstrap.php';

View File

@@ -0,0 +1,23 @@
<?php
return [
[
'auth_key' => 'HP187Mvq7Mmm3CTU80dLkGmni_FUH_lR',
//password_0
'password_hash' => '$2y$13$.NVUBcghbQPiEcBh4LmI7.ctT3FDofwpgrKIAfwWvsr.wBM0l6Y7u',
'password_reset_token' => 'ExzkCOaYc1L8IOBs4wdTGGbgNiG3Wz1I_1402312317',
'created_at' => '1402312317',
'updated_at' => '1402312317',
'email' => 'nicole.paucek@schultz.info',
],
[
//password
'password_hash' => '$2y$13$k2hQ8aV/z6V0Y7pRbq1ufOUSaJI7EhNvvTUIoj2s/rxAmgyY95KPa',
'email' => 'admin@example.com',
'auth_key' => '1',
'status' => '1',
'sales_agent_id' => '0',
'created_at' => '1402312317',
'updated_at' => '1402312317',
],
];

2
common/tests/_output/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

1
common/tests/_support/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
_generated

View File

@@ -0,0 +1,26 @@
<?php
namespace common\tests;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void verify($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
*
* @SuppressWarnings(PHPMD)
*/
class UnitTester extends \Codeception\Actor
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

View File

@@ -0,0 +1,7 @@
suite_namespace: common\tests\unit
actor: UnitTester
bootstrap: false
modules:
enabled:
- Yii2:
part: fixtures

View File

@@ -0,0 +1,67 @@
<?php
namespace unit\models;
use Yii;
use common\models\LoginForm;
use common\fixtures\UserFixture;
/**
* Login form test
*/
class LoginFormTest extends \Codeception\Test\Unit
{
/**
* @var \common\tests\UnitTester
*/
protected $tester;
/**
* @return array
*/
public function _fixtures()
{
return [
'user' => [
'class' => UserFixture::class,
'dataFile' => codecept_data_dir() . 'user.php'
]
];
}
public function testLoginNoUser()
{
$model = new LoginForm([
'email' => 'not_existing_username',
'password' => 'not_existing_password',
]);
verify($model->login())->false();
verify(Yii::$app->user->isGuest)->true();
}
public function testLoginWrongPassword()
{
$model = new LoginForm([
'email' => 'nicole.paucek@schultz.info',
'password' => 'wrong_password',
]);
verify($model->login())->false();
verify( $model->errors)->arrayHasKey('password');
verify(Yii::$app->user->isGuest)->true();
}
public function testLoginCorrect()
{
$model = new LoginForm([
'email' => 'admin@example.com',
'password' => 'password',
]);
verify($model->login())->true();
verify($model->errors)->arrayHasNotKey('password');
verify(Yii::$app->user->isGuest)->false();
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace unit\traits;
use common\traits\FormattedDollarTrait;
use Yii;
use function PHPUnit\Framework\assertEquals;
/**
* Formatted dollar trait test
*/
class FormattedDollarTraitTest extends \Codeception\Test\Unit
{
/**
* @dataProvider floatDataProvider
* @return void
*/
public function testConvertToCents($test, $expected)
{
$mock = $this->getMockForTrait(FormattedDollarTrait::class);
$this->assertEquals($expected, $mock->convertToCents($test));
}
public function floatDataProvider()
{
return [
[12.445, 1244],
[-13.678901234, -1367],
["-10.4", -1040],
["-10", -1000],
["11.445", 1144],
["533.3.3533.11,445", 533335331144],
["1,40032,0030.445", 140032003044],
[124.99, 12499],
[-1.4, -140],
[14, 1400],
[.99, 99],
[2.3, 230],
[-30, -3000],
];
}
}

76
common/widgets/Alert.php Normal file
View File

@@ -0,0 +1,76 @@
<?php
namespace common\widgets;
use Yii;
/**
* Alert widget renders a message from session flash. All flash messages are displayed
* in the sequence they were assigned using setFlash. You can set message as following:
*
* ```php
* Yii::$app->session->setFlash('error', 'This is the message');
* Yii::$app->session->setFlash('success', 'This is the message');
* Yii::$app->session->setFlash('info', 'This is the message');
* ```
*
* Multiple messages could be set as follows:
*
* ```php
* Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']);
* ```
*
* @author Kartik Visweswaran <kartikv2@gmail.com>
* @author Alexander Makarov <sam@rmcreative.ru>
*/
class Alert extends \yii\bootstrap5\Widget
{
/**
* @var array the alert types configuration for the flash messages.
* This array is setup as $key => $value, where:
* - key: the name of the session flash variable
* - value: the bootstrap alert type (i.e. danger, success, info, warning)
*/
public $alertTypes = [
'error' => 'alert-danger',
'danger' => 'alert-danger',
'success' => 'alert-success',
'info' => 'alert-info',
'warning' => 'alert-warning'
];
/**
* @var array the options for rendering the close button tag.
* Array will be passed to [[\yii\bootstrap\Alert::closeButton]].
*/
public $closeButton = [];
/**
* {@inheritdoc}
*/
public function run()
{
$session = Yii::$app->session;
$flashes = $session->getAllFlashes();
$appendClass = isset($this->options['class']) ? ' ' . $this->options['class'] : '';
foreach ($flashes as $type => $flash) {
if (!isset($this->alertTypes[$type])) {
continue;
}
foreach ((array) $flash as $i => $message) {
echo \yii\bootstrap5\Alert::widget([
'body' => $message,
'closeButton' => $this->closeButton,
'options' => array_merge($this->options, [
'id' => $this->getId() . '-' . $type . '-' . $i,
'class' => $this->alertTypes[$type] . $appendClass,
]),
]);
}
$session->removeFlash($type);
}
}
}