diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f69c0b1..7a8a2f3 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -46,6 +46,3 @@ jobs:
- name: Execute tests
run: composer test
-
- # - name: Execute tests with hashids turned on
- # run: composer test-hashids
diff --git a/README.md b/README.md
index bdcdf6b..3789b5b 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,33 @@
## What's going on here?
This is a reward system package for [Lunar](https://github.com/lunarphp/lunar)
-which allows your users to earn points for their purchases and redeem them for discounts.
+which allows you to add or subtract reward points from models (eg. `User`s).
+You can give points to users for various actions like buying products, writing reviews, etc.
+Your users can then spend these points on discounts, free products, etc.
+
+Point balances are managed by the awesome [Laravel Wallet](https://github.com/021-projects/laravel-wallet) package.
+
+### Example use cases
+
+#### Getting rewards
+
+```diff
++ Give points to a user for every paid order (can be calculated from order value, or fixed amount)
++ Give points to a user for writing a review
++ Give points to a user for referring a friend
++ Give points to a user for signing up
++ Give points to a user for completing a profile
+```
+
+#### Spending rewards
+
+```diff
+- Donating points to a charity (transfer points from user to a dedicated charity account)
+- Redeeming points for a discount (applied on cart)
+- Redeeming points for a coupon code with the value of points
+- Moving users to a different customer group based on points
+- Giving a free product for a certain amount of points
+```
## Getting started guide
@@ -27,12 +53,15 @@ composer require dystcz/lunar-rewards
Publish config files
-> You will probably need them pretty bad
-
```bash
-php artisan vendor:publish --provider="Dystcz\LunarRewards\LunarRewardsServiceProvider" --tag="lunar-rewards"
+php artisan vendor:publish --provider="Dystcz\LunarRewards\LunarRewardsServiceProvider" --tag="lunar-rewards.config"
```
+This will publish two configuration files:
+
+1. `config/lunar-rewards/rewards.php` - contains the rewards configuration
+2. `config/wallet.php` - contains the wallet configuration
+
Publish migrations
> Only in case you want to customize the database schema
@@ -41,6 +70,140 @@ Publish migrations
php artisan vendor:publish --provider="Dystcz\LunarRewards\LunarRewardsServiceProvider" --tag="lunar-rewards.migrations"
```
+### Configuration
+
+If you want to dig deeper into the underlaying [laravel-wallet](https://github.com/021-projects/laravel-wallet) package configuration
+please visit their [documentation](021-projects.github.io/laravel-wallet).
+You might want to [configure the database table names](https://021-projects.github.io/laravel-wallet/8.x/configuration.html#table-names).
+
+### Usage
+
+#### Preparing your models
+
+1. Implement the `Rewardable` interface in your model.
+2. Add the `HasRewardPointsBalance` trait to your model.
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Contracts\Rewardable;
+use Dystcz\LunarRewards\Domain\Rewards\Traits\HasRewardPointsBalance;
+
+class Model
+class Model implements Rewardable
+{
+ use HasRewardPointsBalance;
+}
+```
+
+#### Depositing / Giving points to a model
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Actions\DepositPoints;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
+use Dystcz\LunarRewards\Facades\LunarRewards;
+
+(new DepositPoints)->handle(to: $model, points: new Reward(100));
+
+// or by calling the facade
+LunarRewards::deposit(to: $model, points: new Reward(1000));
+```
+
+#### Charging points from a model
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Actions\ChargePoints;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
+use Dystcz\LunarRewards\Facades\LunarRewards;
+
+(new ChargePoints)->handle(from: $model, points: new Reward(100));
+
+// or by calling the facade
+LunarRewards::charge(from: $model, points: new Reward(1000));
+```
+
+#### Transferring points
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Actions\TransferPoints;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
+use Dystcz\LunarRewards\Facades\LunarRewards;
+
+(new TransferPoints)->handle(from: $model, to: $model2, points: new Reward(100));
+
+// or by calling the facade
+LunarRewards::transfer(from: $model, to: $model2, points: new Reward(1000));
+```
+
+#### Getting model points balance
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Managers\PointBalanceManager;
+use Dystcz\LunarRewards\Facades\LunarRewards;
+
+$balance = PointBalanceManager::of($model);
+
+// Points Balance
+$balance->getValue(); // int
+$balance->getReward(); // Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward
+
+// Get balance by calling the facade
+LunarRewards::balance($model); // int
+
+// All Sent Points
+$balance->getSent(); // int
+$balance->getSentReward(); // Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward
+
+// All Received Points
+$balance->getReceived(); // int
+$balance->getReceivedReward(); // Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward
+```
+
+#### Getting model points transactions
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Managers\PointBalanceManager;
+
+$balance = PointBalanceManager::of($model);
+
+// All Received Points
+$balance->getTransactions(); // Illuminate\Support\Collection<\Dystcz\LunarRewards\Domain\Rewards\Models\Transaction>
+$balance->getTransactionsQuery(); // Illuminate\Database\Eloquent\Builder
+
+// Or simply by calling the facade
+```
+
+#### Validating balances
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Managers\PointBalanceManager;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
+
+$balance = PointBalanceManager::of($model);
+
+// Check if model has enough points
+$balance->hasEnoughPoints(new Reward(1000)); // bool
+```
+
+#### Creating coupons from balance
+
+```php
+use Dystcz\LunarRewards\Domain\Rewards\Actions\CreateCouponFromBalance;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
+
+$currency = $order->currency; // Lunar\Models\Currency
+
+// Create a coupon with the value from the whole balance
+$coupon = App::make(CreateCouponFromBalance::class)->handle(model: $model, currency: $currency);
+
+// Create a coupon only for provided points
+$coupon = App::make(CreateCouponFromBalance::class)->handle(
+ model: $model,
+ currency: $currency,
+ points: new Reward(1000)
+);
+```
+
+### Lunar API endpoints
+
### Testing
```bash
@@ -67,7 +230,7 @@ If you discover any security related issues, please email dev@dy.st instead of u
- [All Contributors](../../contributors)
- [Lunar](https://github.com/lunarphp/lunar) for providing awesome e-commerce package
-- [Laravel JSON:API](https://github.com/laravel-json-api/laravel)
+- [Laravel Wallet](https://github.com/021-projects/laravel-wallet) for the points transaction engine
which is a brilliant JSON:API layer for Laravel applications
## License
diff --git a/composer.json b/composer.json
index 0ec19e1..b9f5790 100644
--- a/composer.json
+++ b/composer.json
@@ -23,6 +23,7 @@
],
"require": {
"php": "^8.2",
+ "021/laravel-wallet": "^8.2",
"illuminate/support": "^10.0",
"lunarphp/lunar": "^0.8"
},
@@ -31,14 +32,15 @@
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^2.13",
+ "driftingly/rector-laravel": "^0.17.0",
+ "dystcz/lunar-api": "^0.8",
"laravel-json-api/testing": "^2.1",
+ "laravel/facade-documenter": "dev-main",
"laravel/pint": "^1.7",
- "dystcz/lunar-api": "^0.8",
"orchestra/testbench": "^8.0",
"pestphp/pest": "^2.0",
"pestphp/pest-plugin-laravel": "^2.0",
"rector/rector": "^0.15.23",
- "driftingly/rector-laravel": "^0.17.0",
"spatie/laravel-ray": "^1.32"
},
"autoload": {
@@ -56,7 +58,6 @@
"clear": "@php vendor/bin/testbench package:purge --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"test": "vendor/bin/pest",
- "test-hashids": "vendor/bin/pest -c phpunit.hashids.xml",
"test-coverage": "vendor/bin/pest --coverage",
"analyse": "vendor/bin/phpstan analyse",
"format": "vendor/bin/pint"
@@ -78,5 +79,11 @@
}
},
"minimum-stability": "dev",
- "prefer-stable": true
+ "prefer-stable": true,
+ "repositories": {
+ "facade-documenter": {
+ "type": "vcs",
+ "url": "git@github.com:laravel/facade-documenter.git"
+ }
+ }
}
diff --git a/composer.lock b/composer.lock
index 9e93be9..b5826b7 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,8 +4,124 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "721071caf4082d4d694da455432f4771",
+ "content-hash": "6c17a8a017dae78b9733e98af130b806",
"packages": [
+ {
+ "name": "021/laravel-wallet",
+ "version": "v8.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/021-projects/laravel-wallet.git",
+ "reference": "c66401a1682fce12a2ca0577eead7d7562b73585"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/021-projects/laravel-wallet/zipball/c66401a1682fce12a2ca0577eead7d7562b73585",
+ "reference": "c66401a1682fce12a2ca0577eead7d7562b73585",
+ "shasum": ""
+ },
+ "require": {
+ "021/safely-transaction": "^1.0.1",
+ "ext-bcmath": "*",
+ "laravel/framework": "^10.0|^11.0",
+ "php": "^8.1|^8.2|^8.3"
+ },
+ "require-dev": {
+ "larastan/larastan": "^2.0",
+ "laravel/pint": "^1.13",
+ "orchestra/testbench": "^8.21|^9.0",
+ "phpunit/phpunit": "^10.5"
+ },
+ "type": "package",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "O21\\LaravelWallet\\ServiceProvider"
+ ],
+ "dont-discover": []
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "O21\\LaravelWallet\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "021",
+ "email": "devsellio@gmail.com",
+ "homepage": "https://021-projects.github.io/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Reliable and flexible wallet system for Laravel",
+ "keywords": [
+ "balance",
+ "deposit",
+ "finance",
+ "laravel",
+ "payment",
+ "payout",
+ "transactions",
+ "transfer",
+ "wallet",
+ "withdrawal"
+ ],
+ "support": {
+ "issues": "https://github.com/021-projects/laravel-wallet/issues",
+ "source": "https://github.com/021-projects/laravel-wallet/tree/v8.2.6"
+ },
+ "time": "2024-03-17T08:41:53+00:00"
+ },
+ {
+ "name": "021/safely-transaction",
+ "version": "v1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/021-projects/laravel-safely-transaction.git",
+ "reference": "3036089425805bc29f6da9b88dc16896ff67c400"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/021-projects/laravel-safely-transaction/zipball/3036089425805bc29f6da9b88dc16896ff67c400",
+ "reference": "3036089425805bc29f6da9b88dc16896ff67c400",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/database": "^7.0|^8.0|^9.0|^10.0|^11.0",
+ "php": "^8.0|^8.1|^8.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5.10"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "O21\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Helper class for locking record in database",
+ "keywords": [
+ "framework",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/021-projects/laravel-safely-transaction/issues",
+ "source": "https://github.com/021-projects/laravel-safely-transaction/tree/v1.0.3"
+ },
+ "time": "2024-02-22T19:29:46+00:00"
+ },
{
"name": "barryvdh/laravel-dompdf",
"version": "v2.1.1",
@@ -9506,6 +9622,37 @@
},
"time": "2023-02-14T19:18:31+00:00"
},
+ {
+ "name": "laravel/facade-documenter",
+ "version": "dev-main",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/facade-documenter.git",
+ "reference": "57c6f78e684351902d5b8d0166cc13e33f4d73e6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/facade-documenter/zipball/57c6f78e684351902d5b8d0166cc13e33f4d73e6",
+ "reference": "57c6f78e684351902d5b8d0166cc13e33f4d73e6",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/support": "^9.51|^10.0|^11.0|^12.0",
+ "phpstan/phpdoc-parser": "^1.16"
+ },
+ "default-branch": true,
+ "bin": [
+ "facade.php"
+ ],
+ "type": "library",
+ "license": [
+ "MIT"
+ ],
+ "support": {
+ "source": "https://github.com/laravel/facade-documenter/tree/main"
+ },
+ "time": "2024-03-12T17:06:24+00:00"
+ },
{
"name": "laravel/pint",
"version": "v1.15.1",
@@ -13424,7 +13571,9 @@
],
"aliases": [],
"minimum-stability": "dev",
- "stability-flags": [],
+ "stability-flags": {
+ "laravel/facade-documenter": 20
+ },
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
diff --git a/config/rewards.php b/config/rewards.php
index dc0e829..7f268a9 100644
--- a/config/rewards.php
+++ b/config/rewards.php
@@ -17,8 +17,41 @@
|--------------------------------------------------------------------------
|
| Specify the coefficient to use for calculating reward points.
- | The coefficient multiplies the order total to calculate the reward points.
+ | The coefficient multiplies the value to calculate the reward points.
+ | Default: sub total * reward point coefficient (10)
|
*/
- 'reward_point_coefficient' => 1,
+ 'reward_coefficient' => 10,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Reward Value Coefficient
+ |--------------------------------------------------------------------------
+ |
+ | Specify the coefficient to use for calculating value from reward points.
+ | The coefficient divides the reward points to calculate the value.
+ | Default: points / (reward point coefficient * value coefficient) (10 * 10 = 100)
+ |
+ */
+ 'value_coefficient' => 10,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Coupon Code Generator
+ |--------------------------------------------------------------------------
+ |
+ | Specify which class to for generating discount codes.
+ |
+ */
+ 'coupon_code_generator' => \Dystcz\LunarRewards\Domain\Discounts\Generators\CouponCodeGenerator::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Coupon Name Prefix
+ |--------------------------------------------------------------------------
+ |
+ | Set the prefix for the coupon code name.
+ |
+ */
+ 'coupon_name_prefix' => 'Reward Points Coupon',
];
diff --git a/config/wallet.php b/config/wallet.php
new file mode 100644
index 0000000..d74eefc
--- /dev/null
+++ b/config/wallet.php
@@ -0,0 +1,48 @@
+ 'RP',
+
+ 'balance' => [
+ 'accounting_statuses' => [
+ \O21\LaravelWallet\Enums\TransactionStatus::SUCCESS,
+ \O21\LaravelWallet\Enums\TransactionStatus::ON_HOLD,
+ ],
+ 'extra_values' => [
+ // enable value_pending calculation
+ 'pending' => false,
+ // enable value_on_hold calculation
+ 'on_hold' => false,
+ ],
+ 'max_scale' => 8,
+ 'log_states' => false,
+ ],
+
+ 'models' => [
+ 'balance' => \Dystcz\LunarRewards\Domain\Rewards\Models\Balance::class,
+ 'balance_state' => \Dystcz\LunarRewards\Domain\Rewards\Models\BalanceState::class,
+ 'transaction' => \Dystcz\LunarRewards\Domain\Rewards\Models\Transaction::class,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Table Names
+ |--------------------------------------------------------------------------
+ |
+ | Specify the table names to use for the rewards system.
+ | Note that the table names are prefixed with the default Lunar table prefix.
+ | Eg. 'balances' will be 'lunar_balances' or wharever your table prefix is.
+ |
+ */
+ 'table_names' => [
+ 'balances' => 'wallet_balances',
+ 'balance_states' => 'wallet_balance_states',
+ 'transactions' => 'wallet_transactions',
+ ],
+
+ 'processors' => [
+ 'deposit' => \O21\LaravelWallet\Transaction\Processors\DepositProcessor::class,
+ 'charge' => \O21\LaravelWallet\Transaction\Processors\ChargeProcessor::class,
+ 'transfer' => \O21\LaravelWallet\Transaction\Processors\TransferProcessor::class,
+ ],
+];
diff --git a/lang/cs/validations.php b/lang/cs/validations.php
index 483a4fd..3ac44ad 100644
--- a/lang/cs/validations.php
+++ b/lang/cs/validations.php
@@ -1,209 +1,5 @@
[
- 'company_in' => [
- 'string' => 'IČO společnosti musí být řetězec.',
- ],
- 'company_tin' => [
- 'string' => 'Pole DIČ společnosti musí být řetězec.',
- ],
- 'line_one' => [
- 'required' => 'Pole první řádek je povinné.',
- 'string' => 'Pole první řádek musí být řetězec.',
- ],
- 'line_two' => [
- 'string' => 'Pole druhý řádek musí být řetězec.',
- ],
- 'line_three' => [
- 'string' => 'Pole třetí řádek musí být řetězec.',
- ],
- 'city' => [
- 'required' => 'Pole město je povinné.',
- 'string' => 'Pole město musí být řetězec.',
- ],
- 'state' => [
- 'string' => 'Pole stát musí být řetězec.',
- ],
- 'postcode' => [
- 'required' => 'Pole PSČ je povinné.',
- 'string' => 'Pole PSČ musí být řetězec.',
- ],
- 'delivery_instructions' => [
- 'string' => 'Pole instrukce pro doručení musí být řetězec.',
- ],
- 'contact_email' => [
- 'string' => 'Pole kontaktní e-mail musí být řetězec.',
- ],
- 'contact_phone' => [
- 'string' => 'Pole kontaktní telefon musí být řetězec.',
- ],
- 'shipping_default' => [
- 'boolean' => 'Pole výchozí doručení musí být logická hodnota.',
- ],
- 'billing_default' => [
- 'boolean' => 'Pole výchozí fakturace musí být logická hodnota.',
- ],
- 'meta' => [
- 'array' => 'Pole meta musí být pole.',
- ],
- ],
-
- 'carts' => [
- 'create_user' => [
- 'boolean' => 'Pole vytvořit uživatele musí být logická hodnota.',
- ],
- 'meta' => [
- 'array' => 'Pole meta musí být pole.',
- ],
- 'coupon_code' => [
- 'required' => 'Pole kód kupónu je povinné.',
- 'string' => 'Pole kód kupónu musí být řetězec.',
- 'invalid' => 'Kupón není platný nebo byl použit příliš mnohokrát.',
- ],
- 'agree' => [
- 'accepted' => 'Musíte souhlasit s obchodními podmínkami.',
- ],
- 'shipping_option' => [
- 'required' => 'Prosím vyberte možnost doručení.',
- ],
- ],
-
- 'cart_addresses' => [
- 'title' => [
- 'string' => 'Pole titul musí být řetězec.',
- ],
- 'first_name' => [
- 'required' => 'Pole jméno je povinné.',
- 'string' => 'Pole jméno musí být řetězec.',
- ],
- 'last_name' => [
- 'required' => 'Pole příjmení je povinné.',
- 'string' => 'Pole příjmení musí být řetězec.',
- ],
- 'company_name' => [
- 'string' => 'Pole název společnosti musí být řetězec.',
- ],
- 'company_in' => [
- 'string' => 'Pole IČO společnosti musí být řetězec.',
- ],
- 'company_tin' => [
- 'string' => 'Pole DIČ společnosti musí být řetězec.',
- ],
- 'line_one' => [
- 'required' => 'Pole první řádek je povinné.',
- 'string' => 'Pole první řádek musí být řetězec.',
- ],
- 'line_two' => [
- 'string' => 'Pole druhý řádek musí být řetězec.',
- ],
- 'line_three' => [
- 'string' => 'Pole třetí řádek musí být řetězec.',
- ],
- 'city' => [
- 'required' => 'Pole město je povinné.',
- 'string' => 'Pole město musí být řetězec.',
- ],
- 'state' => [
- 'string' => 'Pole stát musí být řetězec.',
- ],
- 'postcode' => [
- 'required' => 'Pole PSČ je povinné.',
- 'string' => 'Pole PSČ musí být řetězec.',
- ],
- 'delivery_instructions' => [
- 'string' => 'Pole instrukce pro doručení musí být řetězec.',
- ],
- 'contact_email' => [
- 'string' => 'Pole kontaktní e-mail musí být řetězec.',
- ],
- 'contact_phone' => [
- 'string' => 'Pole kontaktní telefon musí být řetězec.',
- ],
- 'shipping_option' => [
- 'string' => 'Pole možnost doručení musí být řetězec.',
- ],
- 'address_type' => [
- 'required' => 'Pole typ adresy je povinné.',
- 'string' => 'Pole typ adresy musí být řetězec.',
- 'in' => 'Pole typ adresy musí být jedno z: :values.',
- ],
- ],
-
- 'cart_lines' => [
- 'quantity' => [
- 'integer' => 'Pole množství musí být celé číslo.',
- ],
- 'purchasable_id' => [
- 'required' => 'Pole ID položky je povinné.',
- 'integer' => 'Pole ID položky musí být celé číslo.',
- ],
- 'purchasable_type' => [
- 'required' => 'Pole typ položky je povinné.',
- 'string' => 'Pole typ položky musí být řetězec.',
- ],
- 'meta' => [
- 'array' => 'Pole meta musí být pole.',
- ],
- ],
-
- 'customers' => [
- 'title' => [
- 'string' => 'Pole titul musí být řetězec.',
- ],
- 'first_name' => [
- 'string' => 'Pole jméno musí být řetězec.',
- ],
- 'last_name' => [
- 'string' => 'Pole příjmení musí být řetězec.',
- ],
- 'company_name' => [
- 'string' => 'Pole název společnosti musí být řetězec.',
- ],
- 'vat_no' => [
- 'string' => 'DIČ společnosti musí být řetězec.',
- ],
- 'account_ref' => [
- 'string' => 'IČO společnosti musí být řetězec.',
- ],
- ],
-
- 'orders' => [
- 'notes' => [
- 'string' => 'Pole poznámky musí být řetězec.',
- ],
- ],
-
- 'payments' => [
- 'payment_method' => [
- 'required' => 'Pole způsob platby je povinné.',
- 'string' => 'Pole způsob platby musí být řetězec.',
- 'in' => 'Pole způsob platby musí být jedno z: :types.',
- ],
- 'amount' => [
- 'numeric' => 'Pole částka musí být číslo.',
- ],
- 'meta' => [
- 'array' => 'Pole meta musí být pole.',
- ],
- ],
-
- 'shipping' => [
- 'set_shipping_option' => [
- 'shipping_option' => [
- 'required' => 'Prosím vyberte možnost doručení.',
- 'string' => 'Pole možnost doručení musí být řetězec.',
- ],
- ],
- ],
-
- 'payments' => [
- 'set_payment_option' => [
- 'payment_option' => [
- 'required' => 'Prosím vyberte platební metodu.',
- 'string' => 'Pole platební metoda musí být řetězec.',
- ],
- ],
- ],
+ //
];
diff --git a/lang/en/validations.php b/lang/en/validations.php
index 505c591..3ac44ad 100644
--- a/lang/en/validations.php
+++ b/lang/en/validations.php
@@ -1,227 +1,5 @@
[
- 'email' => [
- 'required' => 'Please enter your email address.',
- 'email' => 'Please enter a valid email address.',
- 'unique' => 'This email address is already in use.',
- 'max' => 'Your email address must be less than :max characters long.',
- ],
-
- 'password' => [
- 'required' => 'Please enter a password.',
- 'min' => 'Your password must be at least :min characters long.',
- 'confirmed' => 'Please confirm your password.',
- ],
- ],
-
- 'addresses' => [
- 'company_in' => [
- 'string' => 'Company ID must be a string.',
- ],
- 'company_tin' => [
- 'string' => 'Company tax ID field must be a string.',
- ],
- 'line_one' => [
- 'required' => 'Line one field is required.',
- 'string' => 'Line one field must be a string.',
- ],
- 'line_two' => [
- 'string' => 'Line two field must be a string.',
- ],
- 'line_three' => [
- 'string' => 'Line three field must be a string.',
- ],
- 'city' => [
- 'required' => 'City field is required.',
- 'string' => 'City field must be a string.',
- ],
- 'state' => [
- 'string' => 'State field must be a string.',
- ],
- 'postcode' => [
- 'required' => 'Postcode field is required.',
- 'string' => 'Postcode field must be a string.',
- ],
- 'delivery_instructions' => [
- 'string' => 'Delivery instructions field must be a string.',
- ],
- 'contact_email' => [
- 'string' => 'Contact email field must be a string.',
- ],
- 'contact_phone' => [
- 'string' => 'Contact phone field must be a string.',
- ],
- 'shipping_default' => [
- 'boolean' => 'Shipping default field must be a boolean.',
- ],
- 'billing_default' => [
- 'boolean' => 'Billing default field must be a boolean.',
- ],
- 'meta' => [
- 'array' => 'Meta field must be an array.',
- ],
- ],
-
- 'carts' => [
- 'create_user' => [
- 'boolean' => 'Create user field must be a boolean.',
- ],
- 'meta' => [
- 'array' => 'Meta field must be an array.',
- ],
- 'coupon_code' => [
- 'required' => 'Coupon code field is required.',
- 'string' => 'Coupon code field must be a string.',
- 'invalid' => 'The coupon is not valid or has been used too many times',
- ],
- 'agree' => [
- 'accepted' => 'You must agree to the terms and conditions.',
- ],
- 'shipping_option' => [
- 'required' => 'Please select a shipping option.',
- ],
- 'payment_option' => [
- 'required' => 'Please select a payment option.',
- ],
- ],
-
- 'cart_addresses' => [
- 'title' => [
- 'string' => 'Title field must be a string.',
- ],
- 'first_name' => [
- 'required' => 'First name field is required.',
- 'string' => 'First name field must be a string.',
- ],
- 'last_name' => [
- 'required' => 'Last name field is required.',
- 'string' => 'Last name field must be a string.',
- ],
- 'company_name' => [
- 'string' => 'Company name field must be a string.',
- ],
- 'company_in' => [
- 'string' => 'Company in field must be a string.',
- ],
- 'company_tin' => [
- 'string' => 'Company tin field must be a string.',
- ],
- 'line_one' => [
- 'required' => 'Line one field is required.',
- 'string' => 'Line one field must be a string.',
- ],
- 'line_two' => [
- 'string' => 'Line two field must be a string.',
- ],
- 'line_three' => [
- 'string' => 'Line three field must be a string.',
- ],
- 'city' => [
- 'required' => 'City field is required.',
- 'string' => 'City field must be a string.',
- ],
- 'state' => [
- 'string' => 'State field must be a string.',
- ],
- 'postcode' => [
- 'required' => 'Postcode field is required.',
- 'string' => 'Postcode field must be a string.',
- ],
- 'delivery_instructions' => [
- 'string' => 'Delivery instructions field must be a string.',
- ],
- 'contact_email' => [
- 'string' => 'Contact email field must be a string.',
- ],
- 'contact_phone' => [
- 'string' => 'Contact phone field must be a string.',
- ],
- 'shipping_option' => [
- 'string' => 'Shipping option field must be a string.',
- ],
- 'address_type' => [
- 'required' => 'Address type field is required.',
- 'string' => 'Address type field must be a string.',
- 'in' => 'Address type field must be one of: :values.',
- ],
- ],
-
- 'cart_lines' => [
- 'quantity' => [
- 'integer' => 'Quantity field must be an integer.',
- ],
- 'purchasable_id' => [
- 'required' => 'Purchasable id field is required.',
- 'integer' => 'Purchasable id field must be an integer.',
- ],
- 'purchasable_type' => [
- 'required' => 'Purchasable type field is required.',
- 'string' => 'Purchasable type field must be a string.',
- ],
- 'meta' => [
- 'array' => 'Meta field must be an array.',
- ],
- ],
-
- 'customers' => [
- 'title' => [
- 'string' => 'Title field must be a string.',
- ],
- 'first_name' => [
- 'string' => 'First name field must be a string.',
- ],
- 'last_name' => [
- 'string' => 'Last name field must be a string.',
- ],
- 'company_name' => [
- 'string' => 'Company name field must be a string.',
- ],
- 'vat_no' => [
- 'string' => 'Company tax ID must be a string.',
- ],
- 'account_ref' => [
- 'string' => 'Company ID must be a string.',
- ],
- ],
-
- 'orders' => [
- 'notes' => [
- 'string' => 'Notes field must be a string.',
- ],
- ],
-
- 'payments' => [
- 'payment_method' => [
- 'required' => 'Payment method field is required.',
- 'string' => 'Payment method field must be a string.',
- 'in' => 'Payment method field must be one of: :types.',
- ],
- 'amount' => [
- 'numeric' => 'Amount field must be numeric.',
- ],
- 'meta' => [
- 'array' => 'Meta field must be an array.',
- ],
- ],
-
- 'shipping' => [
- 'set_shipping_option' => [
- 'shipping_option' => [
- 'required' => 'Please select a shipping option.',
- 'string' => 'Shipping option field must be a string.',
- ],
- ],
- ],
-
- 'payments' => [
- 'set_payment_option' => [
- 'payment_option' => [
- 'required' => 'Please select a payment method.',
- 'string' => 'Payment method field must be a string.',
- ],
- ],
- ],
+ //
];
diff --git a/phpunit.hashids.xml b/phpunit.hashids.xml
deleted file mode 100644
index f52fc73..0000000
--- a/phpunit.hashids.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
- ./tests/Unit
-
-
- ./tests/Feature
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Domain/Discounts/Contracts/CouponCodeGenerator.php b/src/Domain/Discounts/Contracts/CouponCodeGenerator.php
new file mode 100644
index 0000000..c18f503
--- /dev/null
+++ b/src/Domain/Discounts/Contracts/CouponCodeGenerator.php
@@ -0,0 +1,11 @@
+generateCode();
+
+ $exists = Discount::query()
+ ->where('coupon', $code)
+ ->exists();
+ }
+
+ return $code;
+ }
+
+ /**
+ * Generate code.
+ */
+ protected function generateCode(): string
+ {
+ return Str::upper(Str::random(static::LENGTH));
+ }
+
+ /**
+ * Static constructor.
+ */
+ public static function of(Model $model): self
+ {
+ return new static($model);
+ }
+
+ /**
+ * Get model.
+ */
+ public function getModel(): ?Model
+ {
+ return $this->model;
+ }
+}
diff --git a/src/Domain/Rewards/Actions/ChargePoints.php b/src/Domain/Rewards/Actions/ChargePoints.php
new file mode 100644
index 0000000..cfbde6f
--- /dev/null
+++ b/src/Domain/Rewards/Actions/ChargePoints.php
@@ -0,0 +1,25 @@
+transactionCreator
+ ->amount($points->value)
+ ->currency($points->currency->code)
+ ->processor('charge')
+ ->from($from)
+ ->commit();
+
+ return $transaction;
+ }
+}
diff --git a/src/Domain/Rewards/Actions/CreateCouponFromBalance.php b/src/Domain/Rewards/Actions/CreateCouponFromBalance.php
new file mode 100644
index 0000000..40f341e
--- /dev/null
+++ b/src/Domain/Rewards/Actions/CreateCouponFromBalance.php
@@ -0,0 +1,85 @@
+defaultCurrency = $this->getDefaultCurrency();
+ }
+
+ /**
+ * Create coupon from reward points.
+ */
+ public function handle(Rewardable $model, Currency $currency, ?Reward $points = null): Discount
+ {
+ $points = $points ?? LunarRewards::balanceManager($model)->getReward();
+
+ $price = $this->calculateCouponValue($points, $currency);
+
+ $coupon = $this->createCoupon($model, $price);
+
+ return $coupon;
+ }
+
+ /**
+ * Calculate coupon value.
+ */
+ protected function calculateCouponValue(Reward $points, Currency $currency): Price
+ {
+ return RewardValueCalculator::for($points, $currency)
+ ->calculate();
+ }
+
+ /**
+ * Get default currency.
+ */
+ protected function getDefaultCurrency(): Currency
+ {
+ return Currency::getDefault();
+ }
+
+ /**
+ * Create coupon.
+ */
+ protected function createCoupon(Rewardable $model, Price $price): Discount
+ {
+ $prefix = trim(Config::get('lunar-rewards.rewards.coupon_name_prefix', 'Reward Points'));
+ $suffix = "for {$model->getMorphClass()}::{$model->getKey()}";
+ $name = implode(' ', [$prefix, $suffix]);
+ $code = $this->generator->generate();
+
+ $discount = Discount::create([
+ 'type' => AmountOff::class,
+ 'name' => $name,
+ 'max_uses' => 1,
+ 'handle' => $code,
+ 'coupon' => $code,
+ 'data' => [
+ 'fixed_value' => true,
+ 'fixed_values' => [
+ $price->currency->code => $price->decimal,
+ ],
+ ],
+ 'starts_at' => Carbon::now(),
+ ]);
+
+ return $discount;
+ }
+}
diff --git a/src/Domain/Rewards/Actions/DepositPoints.php b/src/Domain/Rewards/Actions/DepositPoints.php
new file mode 100644
index 0000000..40885d5
--- /dev/null
+++ b/src/Domain/Rewards/Actions/DepositPoints.php
@@ -0,0 +1,26 @@
+transactionCreator
+ ->amount($points->value)
+ ->currency($points->currency->code)
+ ->processor('deposit')
+ ->to($to)
+ ->overcharge()
+ ->commit();
+
+ return $transaction;
+ }
+}
diff --git a/src/Domain/Rewards/Actions/PointsAction.php b/src/Domain/Rewards/Actions/PointsAction.php
new file mode 100644
index 0000000..518eaf9
--- /dev/null
+++ b/src/Domain/Rewards/Actions/PointsAction.php
@@ -0,0 +1,18 @@
+transactionCreator = App::make(TransactionCreator::class);
+ }
+}
diff --git a/src/Domain/Rewards/Actions/TransferPoints.php b/src/Domain/Rewards/Actions/TransferPoints.php
new file mode 100644
index 0000000..489783b
--- /dev/null
+++ b/src/Domain/Rewards/Actions/TransferPoints.php
@@ -0,0 +1,26 @@
+transactionCreator
+ ->amount($points->value)
+ ->currency($points->currency->code)
+ ->processor('transfer')
+ ->from($from)
+ ->to($to)
+ ->commit();
+
+ return $transaction;
+ }
+}
diff --git a/src/Domain/Rewards/Calculators/RewardPointsCalculator.php b/src/Domain/Rewards/Calculators/RewardPointsCalculator.php
index b7449c5..73c556b 100644
--- a/src/Domain/Rewards/Calculators/RewardPointsCalculator.php
+++ b/src/Domain/Rewards/Calculators/RewardPointsCalculator.php
@@ -3,13 +3,14 @@
namespace Dystcz\LunarRewards\Domain\Rewards\Calculators;
use Dystcz\LunarRewards\Domain\Rewards\Contracts\RewardPointsCalculator as RewardPointsCalculatorContract;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
use Illuminate\Support\Facades\Config;
use Lunar\Models\Cart;
use Lunar\Models\Order;
class RewardPointsCalculator implements RewardPointsCalculatorContract
{
- protected int|float $coefficient;
+ protected float $rewardCoefficient;
protected Order|Cart $model;
@@ -20,13 +21,13 @@ public function __construct(Order|Cart $model)
{
$this->model = $model;
- $this->setCoefficient(Config::get('lunar-rewards.reward_point_coefficient', 1));
+ $this->setRewardCoefficient(Config::get('lunar-rewards.rewards.reward_coefficient', 1));
}
/**
* {@inheritDoc}
*/
- public function calculate(): int
+ public function calculate(): Reward
{
if (! $this->getModel()) {
throw new \Exception('No model set for calculating reward points.');
@@ -36,11 +37,11 @@ public function calculate(): int
$exchangeRate = $currency->exchange_rate;
$subTotal = $this->getModel()->sub_total ?? $this->getModel()->subTotal ?? null;
$subTotalValue = $subTotal ? $subTotal->value : 0;
- $coefficient = $this->getCoefficient();
+ $rewardCoefficient = $this->getRewardCoefficient();
- $points = (int) round($subTotalValue * $exchangeRate * $coefficient / 100);
+ $points = (int) round($subTotalValue * $exchangeRate * $rewardCoefficient / 100);
- return $points;
+ return new Reward($points);
}
/**
@@ -62,9 +63,9 @@ public function getModel(): Order|Cart|null
/**
* Set the coefficient for calculating reward points.
*/
- public function setCoefficient(int|float $coefficient): self
+ public function setRewardCoefficient(int|float $rewardCoefficient): self
{
- $this->coefficient = (float) $coefficient;
+ $this->rewardCoefficient = (float) $rewardCoefficient;
return $this;
}
@@ -72,8 +73,8 @@ public function setCoefficient(int|float $coefficient): self
/**
* Get the coefficient used for calculating reward points.
*/
- public function getCoefficient(): float
+ public function getRewardCoefficient(): float
{
- return $this->coefficient;
+ return $this->rewardCoefficient;
}
}
diff --git a/src/Domain/Rewards/Calculators/RewardValueCalculator.php b/src/Domain/Rewards/Calculators/RewardValueCalculator.php
new file mode 100644
index 0000000..229befd
--- /dev/null
+++ b/src/Domain/Rewards/Calculators/RewardValueCalculator.php
@@ -0,0 +1,115 @@
+reward = $reward;
+
+ $this->currency = $currency;
+
+ $this->setRewardCoefficient(Config::get('lunar-rewards.rewards.reward_coefficient', 1));
+ $this->setValueCoefficient(Config::get('lunar-rewards.rewards.value_coefficient', 1));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function calculate(): Price
+ {
+ if (! $this->getReward()) {
+ throw new \Exception('No reward set for calculating the value.');
+ }
+
+ if (! $this->getCurrency()) {
+ throw new \Exception('No currency set for calculating the value.');
+ }
+
+ $rewardCoefficient = $this->getRewardCoefficient();
+ $valueCoefficient = $this->getValueCoefficient();
+ $exchangeRate = $this->getCurrency()->exchange_rate;
+
+ $value = (int) round($this->getReward()->value * $exchangeRate / ($rewardCoefficient * $valueCoefficient) * 100);
+
+ return new Price($value, $this->getCurrency());
+ }
+
+ /**
+ * Static constructor.
+ */
+ public static function for(Reward $reward, Currency $currency): self
+ {
+ return new static($reward, $currency);
+ }
+
+ /**
+ * Get the reward.
+ */
+ public function getReward(): Reward
+ {
+ return $this->reward;
+ }
+
+ /**
+ * Get the currency.
+ */
+ public function getCurrency(): Currency
+ {
+ return $this->currency;
+ }
+
+ /**
+ * Set the value coefficient.
+ */
+ public function setValueCoefficient(int|float $valueCoefficient): self
+ {
+ $this->valueCoefficient = (float) $valueCoefficient;
+
+ return $this;
+ }
+
+ /**
+ * Get the value coefficient.
+ */
+ public function getValueCoefficient(): float
+ {
+ return $this->valueCoefficient;
+ }
+
+ /**
+ * Set the reward points coefficient.
+ */
+ public function setRewardCoefficient(int|float $rewardCoefficient): self
+ {
+ $this->rewardCoefficient = (float) $rewardCoefficient;
+
+ return $this;
+ }
+
+ /**
+ * Get the reward points coefficient.
+ */
+ public function getRewardCoefficient(): float
+ {
+ return $this->rewardCoefficient;
+ }
+}
diff --git a/src/Domain/Rewards/Contracts/RewardPointsCalculator.php b/src/Domain/Rewards/Contracts/RewardPointsCalculator.php
index fcb111d..b564381 100644
--- a/src/Domain/Rewards/Contracts/RewardPointsCalculator.php
+++ b/src/Domain/Rewards/Contracts/RewardPointsCalculator.php
@@ -2,6 +2,7 @@
namespace Dystcz\LunarRewards\Domain\Rewards\Contracts;
+use Dystcz\LunarRewards\Domain\Rewards\DataTypes\Reward;
use Lunar\Models\Cart;
use Lunar\Models\Order;
@@ -15,5 +16,5 @@ public function __construct(Order|Cart $model);
/**
* Calculate the reward points for the given order.
*/
- public function calculate(): int;
+ public function calculate(): Reward;
}
diff --git a/src/Domain/Rewards/Contracts/RewardValueCalculator.php b/src/Domain/Rewards/Contracts/RewardValueCalculator.php
new file mode 100644
index 0000000..e80aaa7
--- /dev/null
+++ b/src/Domain/Rewards/Contracts/RewardValueCalculator.php
@@ -0,0 +1,20 @@
+currency = App::make('lunar-rewards-currency');
+
+ parent::__construct($value, $this->currency, $unitQty);
+ }
+}
diff --git a/src/Domain/Rewards/Managers/PointBalanceManager.php b/src/Domain/Rewards/Managers/PointBalanceManager.php
new file mode 100644
index 0000000..8ea5407
--- /dev/null
+++ b/src/Domain/Rewards/Managers/PointBalanceManager.php
@@ -0,0 +1,198 @@
+balance = $model->balance();
+ }
+
+ /**
+ * Create a new instance of the action.
+ */
+ public static function of(Rewardable $model): self
+ {
+ return new static($model);
+ }
+
+ /**
+ * Get the model.
+ */
+ public function model(): Rewardable
+ {
+ return $this->model;
+ }
+
+ /**
+ * Get balance model.
+ */
+ public function balance(): Balance
+ {
+ return $this->balance;
+ }
+
+ /**
+ * Get transactions query.
+ */
+ public function getTransactionsQuery(): Builder
+ {
+ return $this
+ ->balance()
+ ->transactions();
+ }
+
+ /**
+ * Get transactions.
+ *
+ * @return Collection
+ */
+ public function getTransactions(): Collection
+ {
+ return $this
+ ->getTransactionsQuery()
+ ->get();
+ }
+
+ /**
+ * Determine if the model has enough points.
+ */
+ public function hasEnoughPoints(Reward $points): bool
+ {
+ return $this
+ ->model()
+ ->isEnoughFunds((string) $points, $points->currency->code);
+ }
+
+ /**
+ * Get balance.
+ */
+ public function get(): Numeric
+ {
+ return $this->balance->value;
+ }
+
+ /**
+ * Get balance reward.
+ */
+ public function getReward(): Reward
+ {
+ return new Reward((int) $this->get()->get());
+ }
+
+ /**
+ * Get balance value.
+ */
+ public function getValue(): int
+ {
+ return $this->getReward()->value;
+ }
+
+ /**
+ * Get pending.
+ */
+ public function getPending(): Numeric
+ {
+ return new Numeric($this->balance->value_pending);
+ }
+
+ /**
+ * Get pending reward.
+ */
+ public function getPendingReward(): Reward
+ {
+ return new Reward((int) $this->getPending()->get());
+ }
+
+ /**
+ * Get pending value.
+ */
+ public function getPendingValue(): int
+ {
+ return $this->getPendingReward()->value;
+ }
+
+ /**
+ * Get on hold value.
+ */
+ public function getOnHold(): Numeric
+ {
+ return new Numeric($this->balance->value_on_hold);
+ }
+
+ /**
+ * Get on hold reward.
+ */
+ public function getOnHoldReward(): Reward
+ {
+ return new Reward((int) $this->getOnHold()->get());
+ }
+
+ /**
+ * Get on hold value.
+ */
+ public function getOnHoldValue(): int
+ {
+ return $this->getOnHoldReward()->value;
+ }
+
+ /**
+ * Get sent.
+ */
+ public function getSent(): Numeric
+ {
+ return $this->balance->sent;
+ }
+
+ /**
+ * Get sent reward.
+ */
+ public function getSentReward(): Reward
+ {
+ return new Reward((int) $this->getSent()->get());
+ }
+
+ /**
+ * Get sent value.
+ */
+ public function getSentValue(): int
+ {
+ return $this->getSentReward()->value;
+ }
+
+ /**
+ * Get received.
+ */
+ public function getReceived(): Numeric
+ {
+ return $this->balance->received;
+ }
+
+ /**
+ * Get received reward.
+ */
+ public function getReceivedReward(): Reward
+ {
+ return new Reward((int) $this->getReceived()->get());
+ }
+
+ /**
+ * Get received value.
+ */
+ public function getReceivedValue(): int
+ {
+ return $this->getReceivedReward()->value;
+ }
+}
diff --git a/src/Domain/Rewards/Models/Balance.php b/src/Domain/Rewards/Models/Balance.php
new file mode 100644
index 0000000..fe43186
--- /dev/null
+++ b/src/Domain/Rewards/Models/Balance.php
@@ -0,0 +1,19 @@
+setTable("{$tablePrefix}{$tableName}");
+ }
+}
diff --git a/src/Domain/Rewards/Models/BalanceState.php b/src/Domain/Rewards/Models/BalanceState.php
new file mode 100644
index 0000000..cc6c2b1
--- /dev/null
+++ b/src/Domain/Rewards/Models/BalanceState.php
@@ -0,0 +1,19 @@
+setTable("{$tablePrefix}{$tableName}");
+ }
+}
diff --git a/src/Domain/Rewards/Models/Transaction.php b/src/Domain/Rewards/Models/Transaction.php
new file mode 100644
index 0000000..d86f0f0
--- /dev/null
+++ b/src/Domain/Rewards/Models/Transaction.php
@@ -0,0 +1,19 @@
+setTable("{$tablePrefix}{$tableName}");
+ }
+}
diff --git a/src/Domain/Rewards/Traits/HasRewardPointsBalance.php b/src/Domain/Rewards/Traits/HasRewardPointsBalance.php
new file mode 100644
index 0000000..d2d04d4
--- /dev/null
+++ b/src/Domain/Rewards/Traits/HasRewardPointsBalance.php
@@ -0,0 +1,10 @@
+deposit = App::make(DepositPoints::class);
+ $this->charge = App::make(ChargePoints::class);
+ $this->transfer = App::make(TransferPoints::class);
+ }
+
+ /**
+ * Deposit points to the model.
+ */
+ public function deposit(Rewardable $to, Reward $points): Transaction
+ {
+ return $this->deposit->handle($to, $points);
+ }
+
+ /**
+ * Charge points from the model.
+ */
+ public function charge(Rewardable $from, Reward $points): Transaction
+ {
+ return $this->charge->handle($from, $points);
+ }
+
+ /**
+ * Transfer points from one model to another.
+ */
+ public function transfer(Rewardable $from, Rewardable $to, Reward $points): Transaction
+ {
+ return $this->transfer->handle($from, $to, $points);
+ }
+
+ /**
+ * Get list of transactions.
+ *
+ * @return Collection
+ */
+ public function transactions(Rewardable $model): Collection
+ {
+ return $this->balanceManager($model)->getTransactions();
+ }
+
+ /**
+ * Get the balance of the model.
+ */
+ public function balance(Rewardable $model): int
+ {
+ return $this->balanceManager($model)->getValue();
+ }
+
+ /**
+ * Get the balance manager of the model.
+ */
+ public function balanceManager(Rewardable $model): PointBalanceManager
+ {
+ return PointBalanceManager::of($model);
+ }
}
diff --git a/src/LunarRewardsServiceProvider.php b/src/LunarRewardsServiceProvider.php
index 1234566..b8d52aa 100644
--- a/src/LunarRewardsServiceProvider.php
+++ b/src/LunarRewardsServiceProvider.php
@@ -6,6 +6,7 @@
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
+use Lunar\Models\Currency;
class LunarRewardsServiceProvider extends ServiceProvider
{
@@ -37,13 +38,36 @@ public function register(): void
fn () => new LunarRewards,
);
+ // Register the reward points currency.
+ $this->app->singleton(
+ 'lunar-rewards-currency',
+ fn () => new Currency([
+ 'code' => Config::get('wallet.default_currency', 'RP'),
+ 'name' => 'Reward Points',
+ 'exchange_rate' => 1,
+ 'decimal_places' => 0,
+ 'enabled' => true,
+ ]),
+ );
+
+ // Register the reward points calculator.
$this->app->singleton(
\Dystcz\LunarRewards\Domain\Rewards\Contracts\RewardPointsCalculator::class,
fn () => new (Config::get(
- 'lunar-rewards.reward_point_calculator',
+ 'lunar-rewards.rewards.reward_point_calculator',
\Dystcz\LunarRewards\Domain\Rewards\Calculators\RewardPointsCalculator::class,
)),
);
+
+ // Register coupon code generator.
+ $this->app->singleton(
+ \Dystcz\LunarRewards\Domain\Discounts\Contracts\CouponCodeGenerator::class,
+ fn () => new (Config::get(
+ 'lunar-rewards.rewards.coupon_code_generator',
+ \Dystcz\LunarRewards\Domain\Discounts\Generators\CouponCodeGenerator::class,
+ )),
+ );
+
}
/**
@@ -74,8 +98,12 @@ protected function publishConfig(): void
foreach ($this->configFiles as $configFile) {
$this->publishes([
"{$this->root}/config/{$configFile}.php" => config_path("lunar-rewards/{$configFile}.php"),
- ], 'lunar-rewards');
+ ], 'lunar-rewards.config');
}
+
+ $this->publishes([
+ "{$this->root}/config/wallet.php" => config_path('wallet.php'),
+ ], 'lunar-rewards.config');
}
/**
@@ -99,6 +127,11 @@ protected function registerConfig(): void
"lunar-rewards.{$configFile}",
);
}
+
+ $this->mergeConfigFrom(
+ "{$this->root}/config/wallet.php",
+ 'wallet',
+ );
}
/**
diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php
new file mode 100644
index 0000000..1e73b07
--- /dev/null
+++ b/tests/Feature/ExampleTest.php
@@ -0,0 +1,10 @@
+assertTrue(true);
+});
diff --git a/tests/Stubs/Carts/Cart.php b/tests/Stubs/Carts/Cart.php
new file mode 100644
index 0000000..2382e0a
--- /dev/null
+++ b/tests/Stubs/Carts/Cart.php
@@ -0,0 +1,12 @@
+setUpDatabase();
+
Config::set('auth.providers.users', [
'driver' => 'eloquent',
'model' => User::class,
@@ -35,9 +36,7 @@ protected function setUp(): void
Config::set('lunar.urls.generator', TestUrlGenerator::class);
Config::set('lunar.taxes.driver', 'test');
- Taxes::extend('test', function ($app) {
- return $app->make(TestTaxDriver::class);
- });
+ Taxes::extend('test', fn (Application $app) => $app->make(TestTaxDriver::class));
activity()->disableLogging();
}
@@ -78,6 +77,9 @@ protected function getPackageProviders($app): array
// Lunar Hub
\Lunar\Hub\AdminHubServiceProvider::class,
+ // Laravel Wallet
+ \O21\LaravelWallet\ServiceProvider::class,
+
// Lunar Rewards
\Dystcz\LunarRewards\LunarRewardsServiceProvider::class,
];
@@ -108,6 +110,16 @@ public function getEnvironmentSetUp($app): void
'prefix' => '',
]);
+ /**
+ * Wallet configuration.
+ */
+ Config::set('wallet.default_currency', 'RP');
+ Config::set('wallet.table_names', [
+ 'balances' => 'lunar_rewards_balances',
+ 'balance_states' => 'lunar_rewards_balance_states',
+ 'transactions' => 'lunar_rewards_transactions',
+ ]);
+
Config::set('database.connections.mysql', [
'driver' => 'mysql',
'host' => 'mysql',
@@ -129,17 +141,24 @@ public function getEnvironmentSetUp($app): void
protected function defineDatabaseMigrations(): void
{
$this->loadLaravelMigrations();
+ }
+
+ /**
+ * Set up the database.
+ */
+ protected function setUpDatabase(): void
+ {
+ $walletMigrations = [
+ 'database/migrations/create_balances_table.php.stub',
+ 'database/migrations/create_transactions_table.php.stub',
+ 'database/migrations/create_balance_states_table.php.stub',
+ ];
+
+ foreach ($walletMigrations as $migration) {
+ $migration = include __DIR__."/../vendor/021/laravel-wallet/{$migration}";
- // NOTE MySQL migrations do not play nice with Lunar testing for some reason
- // // artisan($this, 'lunar:install');
- // // artisan($this, 'vendor:publish', ['--tag' => 'lunar']);
- // // artisan($this, 'vendor:publish', ['--tag' => 'lunar.migrations']);
- //
- // // artisan($this, 'migrate', ['--database' => 'mysql']);
- //
- // $this->beforeApplicationDestroyed(
- // fn () => artisan($this, 'migrate:rollback', ['--database' => 'mysql'])
- // );
+ $migration->up();
+ }
}
/**
diff --git a/tests/Traits/CreatesTestingModels.php b/tests/Traits/CreatesTestingModels.php
index d4636ab..96a79c7 100644
--- a/tests/Traits/CreatesTestingModels.php
+++ b/tests/Traits/CreatesTestingModels.php
@@ -3,6 +3,7 @@
namespace Dystcz\LunarRewards\Tests\Traits;
use Dystcz\LunarApi\Domain\Carts\Models\Cart;
+use Dystcz\LunarRewards\Tests\Stubs\Users\User;
use Illuminate\Database\Eloquent\Model;
use Lunar\DataTypes\Price as PriceDataType;
use Lunar\DataTypes\ShippingOption;
@@ -19,6 +20,14 @@
trait CreatesTestingModels
{
+ /**
+ * @param array $models
+ */
+ public function createUser(...$models): User
+ {
+ return User::factory()->create();
+ }
+
/**
* @param array $models
*/
diff --git a/tests/Unit/Domain/Coupons/Generators/CouponCodeGeneratorTest.php b/tests/Unit/Domain/Coupons/Generators/CouponCodeGeneratorTest.php
new file mode 100644
index 0000000..35dd698
--- /dev/null
+++ b/tests/Unit/Domain/Coupons/Generators/CouponCodeGeneratorTest.php
@@ -0,0 +1,63 @@
+generate();
+
+ $this->assertIsString($code);
+ $this->assertEquals(CouponCodeGenerator::LENGTH, strlen($code));
+
+})->group('rewards', 'discounts', 'generators');
+
+it('can recursively generate unique coupon code', function () {
+ /** @var TestCase $this */
+ App::singleton(CouponCodeGeneratorContract::class, fn () => new TestCouponCodeGenerator);
+
+ /** @var CouponCodeGenerator $generator */
+ $generator = App::make(CouponCodeGeneratorContract::class);
+
+ Discount::factory()->create(['coupon' => '10']);
+ Discount::factory()->create(['coupon' => '11']);
+ Discount::factory()->create(['coupon' => '13']);
+ Discount::factory()->create(['coupon' => '14']);
+ Discount::factory()->create(['coupon' => '15']);
+ Discount::factory()->create(['coupon' => '17']);
+ Discount::factory()->create(['coupon' => '18']);
+ Discount::factory()->create(['coupon' => '19']);
+ Discount::factory()->create(['coupon' => '20']);
+
+ $ranTimes = 0;
+ $code = null;
+
+ while (true) {
+ $ranTimes = Cache::get('coupon-generator-test');
+ if ($ranTimes > 1) {
+ break;
+ } else {
+ Cache::forget('coupon-generator-test');
+ }
+
+ $code = $generator->generate();
+ }
+
+ $this->assertGreaterThan(1, $ranTimes);
+ $this->assertTrue(in_array($code, ['12', '16']));
+ $this->assertIsString($code);
+ $this->assertEquals(TestCouponCodeGenerator::LENGTH, strlen($code));
+
+})->group('rewards', 'discounts', 'generators');
diff --git a/tests/Unit/Domain/Rewards/Actions/ChargePointsTest.php b/tests/Unit/Domain/Rewards/Actions/ChargePointsTest.php
new file mode 100644
index 0000000..12bad05
--- /dev/null
+++ b/tests/Unit/Domain/Rewards/Actions/ChargePointsTest.php
@@ -0,0 +1,42 @@
+createUser();
+
+ $balance = PointBalanceManager::of($user);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+ (new ChargePoints)->handle(from: $user, points: new Reward(200));
+ (new ChargePoints)->handle(from: $user, points: new Reward(300));
+ (new ChargePoints)->handle(from: $user, points: new Reward(50));
+
+ $this->assertEquals(450, $balance->getValue());
+})->group('rewards', 'balance', 'charge');
+
+it('it throws an exception if there are not enough funds to charge', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+
+ $balance = PointBalanceManager::of($user);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+
+ $this->assertThrows(
+ fn () => (new ChargePoints)->handle(from: $user, points: new Reward(1200)),
+ InsufficientFundsException::class,
+ );
+
+})->group('rewards', 'balance', 'charge');
diff --git a/tests/Unit/Domain/Rewards/Actions/CreateCouponFromBalanceTest.php b/tests/Unit/Domain/Rewards/Actions/CreateCouponFromBalanceTest.php
new file mode 100644
index 0000000..47ef447
--- /dev/null
+++ b/tests/Unit/Domain/Rewards/Actions/CreateCouponFromBalanceTest.php
@@ -0,0 +1,37 @@
+create([
+ 'decimal_places' => 2,
+ 'exchange_rate' => 1.0,
+ ]);
+
+ $user = $this->createUser();
+
+ $balance = PointBalanceManager::of($user);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+
+ $coupon = App::make(CreateCouponFromBalance::class)->handle($user, $currency);
+
+ $this->assertEquals($coupon->data, [
+ 'fixed_value' => true,
+ 'fixed_values' => [
+ $currency->code => 10,
+ ],
+ ]);
+
+})->group('rewards', 'coupons');
diff --git a/tests/Unit/Domain/Rewards/Actions/DepositPointsTest.php b/tests/Unit/Domain/Rewards/Actions/DepositPointsTest.php
new file mode 100644
index 0000000..6f4270f
--- /dev/null
+++ b/tests/Unit/Domain/Rewards/Actions/DepositPointsTest.php
@@ -0,0 +1,21 @@
+createUser();
+
+ $balance = PointBalanceManager::of($user);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(100));
+
+ $this->assertEquals(100, $balance->getValue());
+})->group('rewards', 'balance', 'deposit');
diff --git a/tests/Unit/Domain/Rewards/Actions/TransferPointsTest.php b/tests/Unit/Domain/Rewards/Actions/TransferPointsTest.php
new file mode 100644
index 0000000..d0937d9
--- /dev/null
+++ b/tests/Unit/Domain/Rewards/Actions/TransferPointsTest.php
@@ -0,0 +1,56 @@
+createUser();
+ $user2 = $this->createUser();
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+ (new DepositPoints)->handle(to: $user2, points: new Reward(200));
+
+ $balance = PointBalanceManager::of($user);
+ $balance2 = PointBalanceManager::of($user2);
+
+ (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(200)); // 800, 400
+ (new TransferPoints)->handle(from: $user2, to: $user, points: new Reward(100)); // 900, 300
+ (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(150)); // 750, 450
+ (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(50)); // 700, 500
+ (new TransferPoints)->handle(from: $user2, to: $user, points: new Reward(20)); // 720, 480
+
+ // User
+ $this->assertEquals(720, $balance->getValue());
+ $this->assertEquals(400, $balance->getSentValue());
+ $this->assertEquals(1000 + 120, $balance->getReceivedValue());
+
+ // User 2
+ $this->assertEquals(480, $balance2->getValue());
+ $this->assertEquals(120, $balance2->getSentValue());
+ $this->assertEquals(200 + 400, $balance2->getReceivedValue());
+
+})->group('rewards', 'balance', 'transfer');
+
+it('it throws an exception if there are not enough funds to charge', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+ $user2 = $this->createUser();
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+
+ $this->assertThrows(
+ fn () => (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(1200)),
+ InsufficientFundsException::class,
+ );
+
+})->group('rewards', 'balance', 'charge');
diff --git a/tests/Unit/Domain/Rewards/Actions/RewardPointsCalculatorTest.php b/tests/Unit/Domain/Rewards/Calculators/RewardPointsCalculatorTest.php
similarity index 77%
rename from tests/Unit/Domain/Rewards/Actions/RewardPointsCalculatorTest.php
rename to tests/Unit/Domain/Rewards/Calculators/RewardPointsCalculatorTest.php
index debfdb2..87a1289 100644
--- a/tests/Unit/Domain/Rewards/Actions/RewardPointsCalculatorTest.php
+++ b/tests/Unit/Domain/Rewards/Calculators/RewardPointsCalculatorTest.php
@@ -8,7 +8,7 @@
uses(TestCase::class, RefreshDatabase::class);
-it('can calculate points from order correctly', function () {
+it('can calculate points from order', function () {
/** @var TestCase $this */
$currency = Currency::factory()->create([
@@ -21,11 +21,11 @@
currency: $currency,
);
- $points = RewardPointsCalculator::for($order)
- ->setCoefficient(2)
+ $reward = RewardPointsCalculator::for($order)
+ ->setRewardCoefficient(2)
->calculate();
- $this->assertEquals(20, $points);
+ $this->assertEquals(20, $reward->value);
})->group('rewards', 'point-calculator');
@@ -42,11 +42,11 @@
currency: $currency,
);
- $points = RewardPointsCalculator::for($order)
- ->setCoefficient(10)
+ $reward = RewardPointsCalculator::for($order)
+ ->setRewardCoefficient(10)
->calculate();
- $this->assertEquals(150, $points);
+ $this->assertEquals(150, $reward->value);
})->group('rewards', 'point-calculator');
@@ -65,10 +65,10 @@
currency: $currency,
);
- $points = RewardPointsCalculator::for($order)
+ $reward = RewardPointsCalculator::for($order)
->calculate();
- $this->assertEquals(100, $points);
+ $this->assertEquals(100, $reward->value);
})->group('rewards', 'point-calculator');
@@ -85,10 +85,10 @@
currency: $currency,
);
- $points = RewardPointsCalculator::for($cart)
- ->setCoefficient(3)
+ $reward = RewardPointsCalculator::for($cart)
+ ->setRewardCoefficient(3)
->calculate();
- $this->assertEquals(60, $points);
+ $this->assertEquals(60, $reward->value);
})->group('rewards', 'point-calculator');
diff --git a/tests/Unit/Domain/Rewards/Calculators/RewardValueCalculatorTest.php b/tests/Unit/Domain/Rewards/Calculators/RewardValueCalculatorTest.php
new file mode 100644
index 0000000..ffd489f
--- /dev/null
+++ b/tests/Unit/Domain/Rewards/Calculators/RewardValueCalculatorTest.php
@@ -0,0 +1,24 @@
+create([
+ 'decimal_places' => 2,
+ 'exchange_rate' => 1.0,
+ ]);
+
+ $calculator = RewardValueCalculator::for($reward, $currency);
+
+ $this->assertEquals(1000, $calculator->calculate()->value);
+
+})->group('rewards', 'value-calculator');
diff --git a/tests/Unit/Domain/Rewards/Managers/PointBalanceManagerTest.php b/tests/Unit/Domain/Rewards/Managers/PointBalanceManagerTest.php
new file mode 100644
index 0000000..b5bc2ee
--- /dev/null
+++ b/tests/Unit/Domain/Rewards/Managers/PointBalanceManagerTest.php
@@ -0,0 +1,135 @@
+createUser();
+ $user2 = $this->createUser();
+
+ $balance = PointBalanceManager::of($user);
+ $balance2 = PointBalanceManager::of($user2);
+
+ $this->assertEquals(0, $balance->getValue());
+ $this->assertEquals(0, $balance2->getValue());
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+ $this->assertEquals(1000, $balance->getValue());
+ $this->assertEquals(1000, $balance->getReceivedValue());
+ $this->assertEquals(0, $balance->getSentValue());
+
+ (new ChargePoints)->handle(from: $user, points: new Reward(500));
+ $this->assertEquals(500, $balance->getValue());
+ $this->assertEquals(1000, $balance->getReceivedValue());
+ $this->assertEquals(500, $balance->getSentValue());
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(200));
+ $this->assertEquals(700, $balance->getValue());
+ $this->assertEquals(1200, $balance->getReceivedValue());
+ $this->assertEquals(500, $balance->getSentValue());
+
+ (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(450));
+ $this->assertEquals(250, $balance->getValue());
+ $this->assertEquals(1200, $balance->getReceivedValue());
+ $this->assertEquals(950, $balance->getSentValue());
+ $this->assertEquals(450, $balance2->getValue());
+ $this->assertEquals(450, $balance2->getReceivedValue());
+ $this->assertEquals(0, $balance2->getSentValue());
+
+})->group('rewards', 'balance', 'managers', 'balance-manager');
+
+it('can get correct values in Numeric, Reward and int', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+ $user2 = $this->createUser();
+
+ $balance = PointBalanceManager::of($user);
+ $balance2 = PointBalanceManager::of($user2);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+ (new ChargePoints)->handle(from: $user, points: new Reward(500));
+ (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(450));
+
+ // Balance
+ $this->assertEquals(new Numeric(50), $balance->get());
+ $this->assertEquals(new Reward(50), $balance->getReward());
+ $this->assertEquals(50, $balance->getValue());
+
+ // Pending
+ $this->assertEquals(new Numeric(0), $balance->getPending());
+ $this->assertEquals(new Reward(0), $balance->getPendingReward());
+ $this->assertEquals(0, $balance->getPendingValue());
+
+ // On hold
+ $this->assertEquals(new Numeric(0), $balance->getOnHold());
+ $this->assertEquals(new Reward(0), $balance->getOnHoldReward());
+ $this->assertEquals(0, $balance->getOnHoldValue());
+
+ // Sent
+ $this->assertEquals(new Numeric(950), $balance->getSent());
+ $this->assertEquals(new Reward(950), $balance->getSentReward());
+ $this->assertEquals(950, $balance->getSentValue());
+
+ // Received
+ $this->assertEquals(new Numeric(1000), $balance->getReceived());
+ $this->assertEquals(new Reward(1000), $balance->getReceivedReward());
+ $this->assertEquals(1000, $balance->getReceivedValue());
+
+})->group('rewards', 'balance', 'managers', 'balance-manager');
+
+it('can get model transactions', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+ $user2 = $this->createUser();
+
+ $balance = PointBalanceManager::of($user);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(1000));
+ (new ChargePoints)->handle(from: $user, points: new Reward(500));
+ (new TransferPoints)->handle(from: $user, to: $user2, points: new Reward(450));
+
+ $transactions = $balance->getTransactions();
+
+ $this->assertInstanceOf(Collection::class, $transactions);
+ $this->assertEquals(3, $transactions->count());
+
+ $this->assertInstanceOf(Builder::class, $balance->getTransactionsQuery());
+
+ $transactions = $balance->getTransactionsQuery()->limit(1)->get();
+
+ $this->assertInstanceOf(Collection::class, $transactions);
+ $this->assertEquals(1, $transactions->count());
+
+})->group('rewards', 'balance', 'managers', 'balance-manager');
+
+it('can check if model has enough points to perform an operation', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+
+ $balance = PointBalanceManager::of($user);
+
+ (new DepositPoints)->handle(to: $user, points: new Reward(200));
+
+ $this->assertEquals(200, $balance->getValue());
+
+ $this->assertTrue($balance->hasEnoughPoints(new Reward(100)));
+ $this->assertFalse($balance->hasEnoughPoints(new Reward(300)));
+
+})->group('rewards', 'balance', 'managers', 'balance-manager');
diff --git a/tests/Unit/Facades/LunarRewardsTest.php b/tests/Unit/Facades/LunarRewardsTest.php
new file mode 100644
index 0000000..06227ba
--- /dev/null
+++ b/tests/Unit/Facades/LunarRewardsTest.php
@@ -0,0 +1,91 @@
+createUser();
+
+ (new DepositPoints)->handle($user, new Reward(1000));
+
+ $balance = LunarRewards::balanceManager($user);
+
+ $this->assertEquals(1000, $balance->getValue());
+ $this->assertEquals(1000, LunarRewards::balance($user));
+
+})->group('rewards', 'facade');
+
+it('can deposit points by calling a facade', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+
+ $balance = LunarRewards::balanceManager($user);
+
+ LunarRewards::deposit($user, new Reward(1000));
+
+ $this->assertEquals(1000, $balance->getValue());
+ $this->assertEquals(1000, LunarRewards::balance($user));
+
+})->group('rewards', 'facade');
+
+it('can charge points by calling a facade', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+
+ LunarRewards::deposit($user, new Reward(500));
+
+ $balance = LunarRewards::balanceManager($user);
+
+ LunarRewards::charge($user, new Reward(200));
+
+ $this->assertEquals(300, $balance->getValue());
+ $this->assertEquals(300, LunarRewards::balance($user));
+
+})->group('rewards', 'facade');
+
+it('can transfer points by calling a facade', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+ $user2 = $this->createUser();
+
+ LunarRewards::deposit($user, new Reward(600));
+
+ $balance = LunarRewards::balanceManager($user);
+ $balance2 = LunarRewards::balanceManager($user2);
+
+ LunarRewards::transfer($user, $user2, new Reward(400));
+
+ $this->assertEquals(200, $balance->getValue());
+ $this->assertEquals(200, LunarRewards::balance($user));
+
+ $this->assertEquals(400, $balance2->getValue());
+ $this->assertEquals(400, LunarRewards::balance($user2));
+
+})->group('rewards', 'facade');
+
+it('can get list of transactions by calling a facade', function () {
+
+ /** @var TestCase $this */
+ $user = $this->createUser();
+ $user2 = $this->createUser();
+
+ LunarRewards::deposit($user, new Reward(600));
+ LunarRewards::charge($user, new Reward(200));
+ LunarRewards::transfer($user, $user2, new Reward(100));
+
+ $this->assertCount(3, LunarRewards::transactions($user));
+ $this->assertCount(1, LunarRewards::transactions($user2));
+ $this->assertInstanceOf(Collection::class, LunarRewards::transactions($user));
+
+})->group('rewards', 'facade');