diff --git a/.phpstorm.meta.php b/.phpstorm.meta.php index af06931ac..4890e9f4a 100644 --- a/.phpstorm.meta.php +++ b/.phpstorm.meta.php @@ -1,97 +1,7 @@ AvailabilityDtoAssemblerInterface::class, - TransactionDtoAssemblerInterface::class => TransactionDtoAssemblerInterface::class, - TransferDtoAssemblerInterface::class => TransferDtoAssemblerInterface::class, - TransferLazyDtoAssemblerInterface::class => TransferLazyDtoAssemblerInterface::class, - // internal.query in assembler - TransactionQueryAssemblerInterface::class => TransactionQueryAssemblerInterface::class, - TransferQueryAssemblerInterface::class => TransferQueryAssemblerInterface::class, - - // internal.repositories - TransactionRepositoryInterface::class => TransactionRepositoryInterface::class, - TransferRepositoryInterface::class => TransferRepositoryInterface::class, - - // internal.service - DatabaseServiceInterface::class => DatabaseServiceInterface::class, - JsonServiceInterface::class => JsonServiceInterface::class, - LockServiceInterface::class => LockServiceInterface::class, - MathServiceInterface::class => MathServiceInterface::class, - StorageServiceInterface::class => StorageServiceInterface::class, - TranslatorServiceInterface::class => TranslatorServiceInterface::class, - UuidFactoryServiceInterface::class => UuidFactoryServiceInterface::class, - - // internal.transform - TransactionDtoTransformerInterface::class => TransactionDtoTransformerInterface::class, - TransferDtoTransformerInterface::class => TransferDtoTransformerInterface::class, - - // legacy.models - Wallet::class => Wallet::class, - Transfer::class => Transfer::class, - Transaction::class => Transaction::class, - - // legacy.objects - Cart::class => Cart::class, - - // services - AssistantServiceInterface::class => AssistantServiceInterface::class, - AtmServiceInterface::class => AtmServiceInterface::class, - AtomicServiceInterface::class => AtomicServiceInterface::class, - BasketServiceInterface::class => BasketServiceInterface::class, - BookkeeperServiceInterface::class => BookkeeperServiceInterface::class, - CastServiceInterface::class => CastServiceInterface::class, - ConsistencyServiceInterface::class => ConsistencyServiceInterface::class, - DiscountServiceInterface::class => DiscountServiceInterface::class, - ExchangeServiceInterface::class => ExchangeServiceInterface::class, - PrepareServiceInterface::class => PrepareServiceInterface::class, - PurchaseServiceInterface::class => PurchaseServiceInterface::class, - TaxServiceInterface::class => TaxServiceInterface::class, - - // lagacy.services - CommonServiceLegacy::class => CommonServiceLegacy::class, - MetaServiceLegacy::class => MetaServiceLegacy::class, + '' => '@', ])); - } diff --git a/README.md b/README.md index 12b4e0c2d..c5981a8b8 100644 --- a/README.md +++ b/README.md @@ -25,16 +25,25 @@ laravel-wallet - Easy work with virtual wallet. * **Laravel Version**: `9.x` * **[Composer](https://getcomposer.org/):** `composer require bavix/laravel-wallet` + +### Support Policy + +| Version | Laravel | PHP | End of improvements | End of support | +|------------|----------------|-------------|---------------------|----------------| +| 7.x | ^6.0,^7.0,^8.0 | 7.4,8.0,8.1 | 1 Mar 2022 | 6 Sep 2022 | +| 8.x | ^9.0 | 8.0,8.1 | 1 May 2022 | 1 Jun 2022 | +| 9.x [LTS] | ^9.0 | 8.0,8.1 | 1 Feb 2023 | 6 Nov 2023 | + ### Upgrade Guide To perform the migration, you will be [helped by the instruction](https://bavix.github.io/laravel-wallet/#/upgrade-guide). ### Extensions -| Extension | Description | -| ----- | ----- | -| [Swap](https://github.com/bavix/laravel-wallet-swap) | Addition to the laravel-wallet library for quick setting of exchange rates | -| [Warm Up](https://github.com/bavix/laravel-wallet-warmup) | Addition to the laravel-wallet library for refresh balance wallets | +| Extension | Description | +|-----------------------------------------------------------|----------------------------------------------------------------------------| +| [Swap](https://github.com/bavix/laravel-wallet-swap) | Addition to the laravel-wallet library for quick setting of exchange rates | +| [Warm Up](https://github.com/bavix/laravel-wallet-warmup) | Addition to the laravel-wallet library for refresh balance wallets | ### Usage Add the `HasWallet` trait and `Wallet` interface to model. @@ -78,19 +87,52 @@ class User extends Model implements Customer } ``` -Add the `HasWallet` trait and `Product` interface to `Item` model. +Add the `HasWallet` trait and interface to `Item` model. + +Starting from version 9.x there are two product interfaces: +- For an unlimited number of products (`ProductLimitedInterface`); +- For a limited number of products (`ProductInterface`); + +An example with an unlimited number of products: ```php use Bavix\Wallet\Traits\HasWallet; -use Bavix\Wallet\Interfaces\Product; use Bavix\Wallet\Interfaces\Customer; +use Bavix\Wallet\Interfaces\ProductInterface; -class Item extends Model implements Product +class Item extends Model implements ProductInterface +{ + use HasWallet; + + public function getAmountProduct(Customer $customer) + { + return 100; + } + + public function getMetaProduct(): ?array + { + return [ + 'title' => $this->title, + 'description' => 'Purchase of Product #' . $this->id, + ]; + } +} +``` + +Example with a limited number of products: +```php +use Bavix\Wallet\Traits\HasWallet; +use Bavix\Wallet\Interfaces\Customer; +use Bavix\Wallet\Interfaces\ProductLimitedInterface; + +class Item extends Model implements ProductLimitedInterface { use HasWallet; public function canBuy(Customer $customer, int $quantity = 1, bool $force = false): bool { /** + * This is where you implement the constraint logic. + * * If the service can be purchased once, then * return !$customer->paid($this); */ @@ -112,6 +154,10 @@ class Item extends Model implements Product } ``` +I do not recommend using the limited interface when working with a shopping cart. +If you are working with a shopping cart, then you should override the `PurchaseServiceInterface` interface. +With it, you can check the availability of all products with one request, there will be no N-queries in the database. + Proceed to purchase. ```php @@ -123,7 +169,7 @@ $user->pay($item); // If you do not have enough money, throw an exception var_dump($user->balance); // 0 if ($user->safePay($item)) { - // try to buy again ) + // try to buy again } var_dump((bool)$user->paid($item)); // bool(true) @@ -135,7 +181,11 @@ var_dump((bool)$user->paid($item)); // bool(false) ### Eager Loading ```php +// When working with one wallet User::with('wallet'); + +// When using the multi-wallet functionality +User::with('wallets'); ``` ### How to work with fractional numbers? diff --git a/changelog.md b/changelog.md index d07d424e1..4f985ba33 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.0.0] - 2022-05-02 #481 +### Added +- ExtraDtoInterface #479 +- Product custom price #485 + +### Changed +- Changing the logic of funds transfers #483 +- Split Product interface #474 +- PHP 8+ Union types #482 +- Eager loading #480 + +### Removed +- method `Cart::addItems` +- method `Cart::addItem` +- method `Cart::setMeta` + +### Updated +- Performance just got a little better +- Public contracts have become stricter +- Inside is now strongly typed + +### Deprecated +- interface `Product` +- method `CartPay::paid` + ## [8.4.1] - 2022-04-26 ### Deprecated - Cart::getUniqueItems @@ -843,7 +868,8 @@ The operation is now executed in the transaction and updates the new `refund` fi - Exceptions: AmountInvalid, BalanceIsEmpty. - Models: Transfer, Transaction. -[Unreleased]: https://github.com/bavix/laravel-wallet/compare/8.4.1...develop +[Unreleased]: https://github.com/bavix/laravel-wallet/compare/9.0.0...develop +[9.0.0]: https://github.com/bavix/laravel-wallet/compare/8.4.1...9.0.0 [8.4.1]: https://github.com/bavix/laravel-wallet/compare/8.4.0...8.4.1 [8.4.0]: https://github.com/bavix/laravel-wallet/compare/8.3.0...8.4.0 [8.3.0]: https://github.com/bavix/laravel-wallet/compare/8.2.1...8.3.0 diff --git a/composer.json b/composer.json index 4693270aa..f68a5e488 100644 --- a/composer.json +++ b/composer.json @@ -38,11 +38,11 @@ "infection/infection": "~0.26", "laravel/cashier": "^13.8", "nunomaduro/collision": "^6.2", - "orchestra/testbench": "^7.3", - "phpstan/phpstan": "^1.5", + "orchestra/testbench": "^7.4", + "phpstan/phpstan": "^1.6", "phpunit/phpunit": "^9.5", "rector/rector": "^0.12", - "symplify/easy-coding-standard": "^10.1", + "symplify/easy-coding-standard": "^10.2", "vimeo/psalm": "^4.22" }, "suggest": { diff --git a/config/config.php b/config/config.php index 27aadf324..8ed1effac 100644 --- a/config/config.php +++ b/config/config.php @@ -4,6 +4,8 @@ use Bavix\Wallet\Internal\Assembler\AvailabilityDtoAssembler; use Bavix\Wallet\Internal\Assembler\BalanceUpdatedEventAssembler; +use Bavix\Wallet\Internal\Assembler\ExtraDtoAssembler; +use Bavix\Wallet\Internal\Assembler\OptionDtoAssembler; use Bavix\Wallet\Internal\Assembler\TransactionDtoAssembler; use Bavix\Wallet\Internal\Assembler\TransactionQueryAssembler; use Bavix\Wallet\Internal\Assembler\TransferDtoAssembler; @@ -36,11 +38,14 @@ use Bavix\Wallet\Services\CastService; use Bavix\Wallet\Services\ConsistencyService; use Bavix\Wallet\Services\DiscountService; +use Bavix\Wallet\Services\EagerLoaderService; use Bavix\Wallet\Services\ExchangeService; use Bavix\Wallet\Services\PrepareService; use Bavix\Wallet\Services\PurchaseService; use Bavix\Wallet\Services\RegulatorService; use Bavix\Wallet\Services\TaxService; +use Bavix\Wallet\Services\TransactionService; +use Bavix\Wallet\Services\TransferService; use Bavix\Wallet\Services\WalletService; return [ @@ -96,10 +101,13 @@ 'cast' => CastService::class, 'consistency' => ConsistencyService::class, 'discount' => DiscountService::class, + 'eager_loader' => EagerLoaderService::class, 'exchange' => ExchangeService::class, 'prepare' => PrepareService::class, 'purchase' => PurchaseService::class, 'tax' => TaxService::class, + 'transaction' => TransactionService::class, + 'transfer' => TransferService::class, 'wallet' => WalletService::class, ], @@ -126,6 +134,8 @@ 'assemblers' => [ 'availability' => AvailabilityDtoAssembler::class, 'balance_updated_event' => BalanceUpdatedEventAssembler::class, + 'extra' => ExtraDtoAssembler::class, + 'option' => OptionDtoAssembler::class, 'transaction' => TransactionDtoAssembler::class, 'transfer_lazy' => TransferLazyDtoAssembler::class, 'transfer' => TransferDtoAssembler::class, diff --git a/database/2018_11_06_222923_create_transactions_table.php b/database/2018_11_06_222923_create_transactions_table.php index 3bef22cee..d24ecc0a9 100644 --- a/database/2018_11_06_222923_create_transactions_table.php +++ b/database/2018_11_06_222923_create_transactions_table.php @@ -7,8 +7,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateTransactionsTable extends Migration -{ +return new class() extends Migration { public function up(): void { Schema::create($this->table(), function (Blueprint $table) { @@ -42,4 +41,4 @@ private function table(): string { return (new Transaction())->getTable(); } -} +}; diff --git a/database/2018_11_07_192923_create_transfers_table.php b/database/2018_11_07_192923_create_transfers_table.php index e0861bb2c..f003d941c 100644 --- a/database/2018_11_07_192923_create_transfers_table.php +++ b/database/2018_11_07_192923_create_transfers_table.php @@ -8,8 +8,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateTransfersTable extends Migration -{ +return new class() extends Migration { public function up(): void { Schema::create($this->table(), function (Blueprint $table) { @@ -70,4 +69,4 @@ private function transactionTable(): string { return (new Transaction())->getTable(); } -} +}; diff --git a/database/2018_11_15_124230_create_wallets_table.php b/database/2018_11_15_124230_create_wallets_table.php index 2799b24f4..268ee8ae2 100644 --- a/database/2018_11_15_124230_create_wallets_table.php +++ b/database/2018_11_15_124230_create_wallets_table.php @@ -8,8 +8,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -class CreateWalletsTable extends Migration -{ +return new class() extends Migration { public function up(): void { Schema::create($this->table(), function (Blueprint $table) { @@ -63,4 +62,4 @@ private function transactionTable(): string { return (new Transaction())->getTable(); } -} +}; diff --git a/database/2021_11_02_202021_update_wallets_uuid_table.php b/database/2021_11_02_202021_update_wallets_uuid_table.php index 5025bd988..754b99926 100644 --- a/database/2021_11_02_202021_update_wallets_uuid_table.php +++ b/database/2021_11_02_202021_update_wallets_uuid_table.php @@ -9,8 +9,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; -class UpdateWalletsUuidTable extends Migration -{ +return new class() extends Migration { public function up(): void { if (Schema::hasColumn($this->table(), 'uuid')) { @@ -18,7 +17,7 @@ public function up(): void } // upgrade from 6.x - Schema::table($this->table(), function (Blueprint $table) { + Schema::table($this->table(), static function (Blueprint $table) { $table->uuid('uuid') ->after('slug') ->nullable() @@ -49,4 +48,4 @@ protected function table(): string { return (new Wallet())->getTable(); } -} +}; diff --git a/depfile.yaml b/depfile.yaml deleted file mode 100644 index 797b6bc80..000000000 --- a/depfile.yaml +++ /dev/null @@ -1,249 +0,0 @@ -paths: - - ./src - -layers: - - name: Legacy - collectors: - - type: className - regex: ^Bavix\\.*Legacy$ - - type: className - regex: ^Bavix\\.*\\Objects\\Cart$ - - - name: Contract - collectors: - - type: className - regex: ^Bavix\\Wallet\\Interfaces\\.* - - - name: UI - collectors: - - type: className - regex: ^Bavix\\.*\\Traits\\.* - - - name: UIException - collectors: - - type: className - regex: ^Illuminate\\Database\\Eloquent\\ModelNotFoundException$ - - type: className - regex: ^Bavix\\Wallet\\Exceptions\\.* - - # internal - - name: InternalException - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Exceptions\\.* - - - name: Event - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Events\\.*Event$ - - - name: EventInterface - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Events\\.*EventInterface$ - - - name: Dto - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Dto\\.*Dto$ - - - name: DtoInterface - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Dto\\.*DtoInterface$ - - - name: AssemblerDto - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Assembler\\.*DtoAssembler$ - - - name: AssemblerDtoInterface - collectors: - - type: className - regex: ^Bavix\\.*\\Internal\\Assembler\\.*DtoAssemblerInterface$ - - - name: QueryInterface - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Query\\.*QueryInterface$ - - - name: Query - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Query\\.*Query$ - - - name: RepositoryInterface - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Repository\\.*RepositoryInterface$ - - - name: Repository - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Repository\\.*Repository$ - - - name: Transform - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Transform\\.*DtoTransformer$ - - - name: TransformInterface - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Transform\\.*DtoTransformerInterface$ - - - name: Infra - collectors: - - type: className - regex: ^Bavix\\.*Internal\\Service\\.*Service$ - - # contracts - - name: Model - collectors: - - type: className - regex: ^Bavix\\.*Models\\Transaction$ - - type: className - regex: ^Bavix\\.*Models\\Transfer$ - - type: className - regex: ^Bavix\\.*Models\\Wallet$ - - - name: Service - collectors: - - type: className - regex: ^Bavix\\.*Services\\.*Service$ - - - name: ServiceInterface - collectors: - - type: className - regex: ^Bavix\\.*Service\\.*ServiceInterface$ - - type: className - regex: ^Bavix\\.*Services\\.*ServiceInterface$ - - # framework - - name: EloquentModel - collectors: - - type: className - regex: ^Illuminate\\Database\\Eloquent\\Model$ - - - name: Config - collectors: - - type: className - regex: ^Illuminate\\Config\\Repository$ - - - name: Cache - collectors: - - type: className - regex: ^Illuminate\\Contracts\\Cache\\Repository$ - -ruleset: - Contract: - - InternalException - - DtoInterface - - UIException - - Model - - UI: - - AssemblerDtoInterface # delete as soon as possible - - InternalException - - ServiceInterface - - UIException - - Contract - - Model - - Legacy # delete as soon as possible - - UIException: - - InternalException - - Infra: - - InternalException - - ServiceInterface - - EventInterface - - Config - - Cache - - EventInterface: - Event: - - EventInterface - - DtoInterface: - - Contract - Dto: - - DtoInterface - - Contract - - Model: - - AssemblerDtoInterface - - InternalException - - ServiceInterface - - EloquentModel - - UIException - - Contract - - Legacy - - UI - - TransformInterface: - - DtoInterface - Transform: - - TransformInterface - - DtoInterface - - QueryInterface: - Query: - - QueryInterface - - RepositoryInterface: - - InternalException - - QueryInterface - - DtoInterface - - Model - Repository: - - RepositoryInterface - - TransformInterface - - InternalException - - ServiceInterface # json service only - - QueryInterface - - DtoInterface - - UIException - - Model - - ServiceInterface: - - InternalException - - EventInterface - - EloquentModel - - DtoInterface - - UIException - - Contract - - Model - Service: - - AssemblerDtoInterface - - RepositoryInterface - - InternalException - - ServiceInterface - - EloquentModel - - DtoInterface - - UIException - - Contract - - Model - - AssemblerDtoInterface: - - EloquentModel - - DtoInterface - - Contract - AssemblerDto: - - AssemblerDtoInterface - - QueryInterface - - ServiceInterface # UUID - - EloquentModel - - DtoInterface - - Contract - - Dto - - Legacy: - - AssemblerDtoInterface - - InternalException - - ServiceInterface - - DtoInterface - - UIException - - Contract - - Model - - Dto # Cart from objects diff --git a/deptrac.yaml b/deptrac.yaml new file mode 100644 index 000000000..e549ce5f0 --- /dev/null +++ b/deptrac.yaml @@ -0,0 +1,247 @@ +parameters: + paths: + - ./src + + layers: + - name: Legacy + collectors: + - type: className + regex: ^Bavix\\.*\\Objects\\Cart$ + + - name: Contract + collectors: + - type: className + regex: ^Bavix\\Wallet\\Interfaces\\.* + - type: className + regex: ^Bavix\\Wallet\\External\\.* + + - name: UI + collectors: + - type: className + regex: ^Bavix\\.*\\Traits\\.* + + - name: UIException + collectors: + - type: className + regex: ^Illuminate\\Database\\Eloquent\\ModelNotFoundException$ + - type: className + regex: ^Bavix\\Wallet\\Exceptions\\.* + + # internal + - name: InternalException + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Exceptions\\.* + + - name: Event + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Events\\.*Event$ + + - name: EventInterface + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Events\\.*EventInterface$ + + - name: Dto + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Dto\\.*Dto$ + + - name: DtoInterface + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Dto\\.*DtoInterface$ + + - name: AssemblerDto + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Assembler\\.*DtoAssembler$ + + - name: AssemblerDtoInterface + collectors: + - type: className + regex: ^Bavix\\.*\\Internal\\Assembler\\.*DtoAssemblerInterface$ + + - name: QueryInterface + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Query\\.*QueryInterface$ + + - name: Query + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Query\\.*Query$ + + - name: RepositoryInterface + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Repository\\.*RepositoryInterface$ + + - name: Repository + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Repository\\.*Repository$ + + - name: Transform + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Transform\\.*DtoTransformer$ + + - name: TransformInterface + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Transform\\.*DtoTransformerInterface$ + + - name: Infra + collectors: + - type: className + regex: ^Bavix\\.*Internal\\Service\\.*Service$ + + # contracts + - name: Model + collectors: + - type: className + regex: ^Bavix\\.*Models\\Transaction$ + - type: className + regex: ^Bavix\\.*Models\\Transfer$ + - type: className + regex: ^Bavix\\.*Models\\Wallet$ + + - name: Service + collectors: + - type: className + regex: ^Bavix\\.*Services\\.*Service$ + + - name: ServiceInterface + collectors: + - type: className + regex: ^Bavix\\.*Service\\.*ServiceInterface$ + - type: className + regex: ^Bavix\\.*Services\\.*ServiceInterface$ + + # framework + - name: EloquentModel + collectors: + - type: className + regex: ^Illuminate\\Database\\Eloquent\\Model$ + + - name: Config + collectors: + - type: className + regex: ^Illuminate\\Config\\Repository$ + + - name: Cache + collectors: + - type: className + regex: ^Illuminate\\Contracts\\Cache\\Repository$ + + ruleset: + Contract: + - InternalException + - DtoInterface + - UIException + - Model + + UI: + - AssemblerDtoInterface # delete as soon as possible + - InternalException + - ServiceInterface + - UIException + - Contract + - Model + - Legacy # delete as soon as possible + + UIException: + - InternalException + + Infra: + - InternalException + - ServiceInterface + - EventInterface + - Config + - Cache + + EventInterface: + Event: + - EventInterface + + DtoInterface: + - Contract + Dto: + - DtoInterface + - Contract + + Model: + - AssemblerDtoInterface + - InternalException + - ServiceInterface + - EloquentModel + - UIException + - Contract + - Legacy + - UI + + TransformInterface: + - DtoInterface + Transform: + - TransformInterface + - DtoInterface + + QueryInterface: + Query: + - QueryInterface + + RepositoryInterface: + - InternalException + - QueryInterface + - DtoInterface + - Model + Repository: + - RepositoryInterface + - TransformInterface + - InternalException + - ServiceInterface # json service only + - QueryInterface + - DtoInterface + - UIException + - Model + + ServiceInterface: + - InternalException + - EventInterface + - EloquentModel + - DtoInterface + - UIException + - Contract + - Model + Service: + - AssemblerDtoInterface + - RepositoryInterface + - InternalException + - ServiceInterface + - EloquentModel + - DtoInterface + - UIException + - Contract + - Model + + AssemblerDtoInterface: + - EloquentModel + - DtoInterface + - Contract + AssemblerDto: + - AssemblerDtoInterface + - QueryInterface + - ServiceInterface # UUID + - EloquentModel + - DtoInterface + - Contract + - Dto + + Legacy: + - InternalException + - ServiceInterface + - DtoInterface + - Contract + - Dto # Cart from objects diff --git a/docs/basic-usage.md b/docs/basic-usage.md index 8917f8fd9..0ee3d96f7 100644 --- a/docs/basic-usage.md +++ b/docs/basic-usage.md @@ -40,26 +40,59 @@ class User extends Model implements Customer } ``` -Add the `HasWallet` trait and `Product` interface to `Item` model. +Add the `HasWallet` trait and interface to `Item` model. + +Starting from version 9.x there are two product interfaces: +- For an unlimited number of products (`ProductLimitedInterface`); +- For a limited number of products (`ProductInterface`); + +An example with an unlimited number of products: +```php +use Bavix\Wallet\Traits\HasWallet; +use Bavix\Wallet\Interfaces\Customer; +use Bavix\Wallet\Interfaces\ProductInterface; + +class Item extends Model implements ProductInterface +{ + use HasWallet; + + public function getAmountProduct(Customer $customer): int|string + { + return 100; + } + + public function getMetaProduct(): ?array + { + return [ + 'title' => $this->title, + 'description' => 'Purchase of Product #' . $this->id, + ]; + } +} +``` + +Example with a limited number of products: ```php use Bavix\Wallet\Traits\HasWallet; -use Bavix\Wallet\Interfaces\Product; use Bavix\Wallet\Interfaces\Customer; +use Bavix\Wallet\Interfaces\ProductLimitedInterface; -class Item extends Model implements Product +class Item extends Model implements ProductLimitedInterface { use HasWallet; public function canBuy(Customer $customer, int $quantity = 1, bool $force = false): bool { /** + * This is where you implement the constraint logic. + * * If the service can be purchased once, then * return !$customer->paid($this); */ return true; } - public function getAmountProduct(Customer $customer) + public function getAmountProduct(Customer $customer): int|string { return 100; } @@ -74,6 +107,10 @@ class Item extends Model implements Product } ``` +I do not recommend using the limited interface when working with a shopping cart. +If you are working with a shopping cart, then you should override the `PurchaseServiceInterface` interface. +With it, you can check the availability of all products with one request, there will be no N-queries in the database. + Proceed to purchase. ```php diff --git a/docs/cart.md b/docs/cart.md index 4f51263c6..8a783c96a 100644 --- a/docs/cart.md +++ b/docs/cart.md @@ -16,27 +16,59 @@ class User extends Model implements Customer ## Item Model -Add the `HasWallet` trait and `Product` interface to Item model. +Add the `HasWallet` trait and interface to `Item` model. +Starting from version 9.x there are two product interfaces: +- For an unlimited number of products (`ProductLimitedInterface`); +- For a limited number of products (`ProductInterface`); + +An example with an unlimited number of products: +```php +use Bavix\Wallet\Traits\HasWallet; +use Bavix\Wallet\Interfaces\Customer; +use Bavix\Wallet\Interfaces\ProductInterface; + +class Item extends Model implements ProductInterface +{ + use HasWallet; + + public function getAmountProduct(Customer $customer): int|string + { + return round($this->price * 100); + } + + public function getMetaProduct(): ?array + { + return [ + 'title' => $this->title, + 'description' => 'Purchase of Product #' . $this->id, + ]; + } +} +``` + +Example with a limited number of products: ```php use Bavix\Wallet\Traits\HasWallet; -use Bavix\Wallet\Interfaces\Product; use Bavix\Wallet\Interfaces\Customer; +use Bavix\Wallet\Interfaces\ProductLimitedInterface; -class Item extends Model implements Product +class Item extends Model implements ProductLimitedInterface { use HasWallet; public function canBuy(Customer $customer, int $quantity = 1, bool $force = false): bool { /** + * This is where you implement the constraint logic. + * * If the service can be purchased once, then * return !$customer->paid($this); */ return true; } - - public function getAmountProduct(Customer $customer) + + public function getAmountProduct(Customer $customer): int|string { return round($this->price * 100); } @@ -45,12 +77,16 @@ class Item extends Model implements Product { return [ 'title' => $this->title, - 'description' => 'Purchase of Product #' . $this->getKey(), + 'description' => 'Purchase of Product #' . $this->id, ]; } } ``` +I do not recommend using the limited interface when working with a shopping cart. +If you are working with a shopping cart, then you should override the `PurchaseServiceInterface` interface. +With it, you can check the availability of all products with one request, there will be no N-queries in the database. + ## Fill the cart Find the user and check the balance. @@ -76,14 +112,19 @@ $products = Item::query() $cart = app(Cart::class); foreach ($products as $product) { - // add product's - $cart->addItem($product, $list[$product->slug]); + $cart = $cart->withItem($product, quantity: $list[$product->slug]); } -$user->deposit($cart->getTotal()); +$cartTotal = $cart->getTotal($user); // 15127 +$user->deposit($cartTotal); $user->balanceInt; // 15127 $user->balanceFloat; // 151.27 +$cart = $cart->withItem(current($products), pricePerItem: 500); // 15127+500 +$user->deposit(500); +$user->balanceInt; // 15627 +$user->balanceFloat; // 156.27 + (bool)$user->payCart($cart); // true $user->balanceFloat; // 0 ``` diff --git a/docs/credit-limits.md b/docs/credit-limits.md index 5360985df..f8e6ee8a6 100644 --- a/docs/credit-limits.md +++ b/docs/credit-limits.md @@ -11,7 +11,7 @@ An example of working with a credit limit: /** * @var \Bavix\Wallet\Interfaces\Customer $customer * @var \Bavix\Wallet\Models\Wallet $wallet - * @var \Bavix\Wallet\Interfaces\Product $product + * @var \Bavix\Wallet\Interfaces\ProductInterface $product */ $wallet = $customer->wallet; // get default wallet $wallet->meta['credit'] = 10000; // credit limit diff --git a/docs/dist/bundle.js b/docs/dist/bundle.js index d812fc5d8..196c864d2 100644 --- a/docs/dist/bundle.js +++ b/docs/dist/bundle.js @@ -1,2 +1,2 @@ /*! For license information please see bundle.js.LICENSE.txt */ -(()=>{var e={669:(e,n,t)=>{e.exports=t(609)},448:(e,n,t)=>{"use strict";var r=t(867),i=t(26),o=t(372),a=t(327),u=t(97),c=t(109),f=t(985),s=t(61);e.exports=function(e){return new Promise((function(n,t){var l=e.data,d=e.headers,p=e.responseType;r.isFormData(l)&&delete d["Content-Type"];var h=new XMLHttpRequest;if(e.auth){var g=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(g+":"+m)}var _=u(e.baseURL,e.url);function b(){if(h){var r="getAllResponseHeaders"in h?c(h.getAllResponseHeaders()):null,o={data:p&&"text"!==p&&"json"!==p?h.response:h.responseText,status:h.status,statusText:h.statusText,headers:r,config:e,request:h};i(n,t,o),h=null}}if(h.open(e.method.toUpperCase(),a(_,e.params,e.paramsSerializer),!0),h.timeout=e.timeout,"onloadend"in h?h.onloadend=b:h.onreadystatechange=function(){h&&4===h.readyState&&(0!==h.status||h.responseURL&&0===h.responseURL.indexOf("file:"))&&setTimeout(b)},h.onabort=function(){h&&(t(s("Request aborted",e,"ECONNABORTED",h)),h=null)},h.onerror=function(){t(s("Network Error",e,null,h)),h=null},h.ontimeout=function(){var n="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(n=e.timeoutErrorMessage),t(s(n,e,e.transitional&&e.transitional.clarifyTimeoutError?"ETIMEDOUT":"ECONNABORTED",h)),h=null},r.isStandardBrowserEnv()){var v=(e.withCredentials||f(_))&&e.xsrfCookieName?o.read(e.xsrfCookieName):void 0;v&&(d[e.xsrfHeaderName]=v)}"setRequestHeader"in h&&r.forEach(d,(function(e,n){void 0===l&&"content-type"===n.toLowerCase()?delete d[n]:h.setRequestHeader(n,e)})),r.isUndefined(e.withCredentials)||(h.withCredentials=!!e.withCredentials),p&&"json"!==p&&(h.responseType=e.responseType),"function"==typeof e.onDownloadProgress&&h.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&h.upload&&h.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then((function(e){h&&(h.abort(),t(e),h=null)})),l||(l=null),h.send(l)}))}},609:(e,n,t)=>{"use strict";var r=t(867),i=t(849),o=t(321),a=t(185);function u(e){var n=new o(e),t=i(o.prototype.request,n);return r.extend(t,o.prototype,n),r.extend(t,n),t}var c=u(t(655));c.Axios=o,c.create=function(e){return u(a(c.defaults,e))},c.Cancel=t(263),c.CancelToken=t(972),c.isCancel=t(502),c.all=function(e){return Promise.all(e)},c.spread=t(713),c.isAxiosError=t(268),e.exports=c,e.exports.default=c},263:e=>{"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},972:(e,n,t)=>{"use strict";var r=t(263);function i(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var n;this.promise=new Promise((function(e){n=e}));var t=this;e((function(e){t.reason||(t.reason=new r(e),n(t.reason))}))}i.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},i.source=function(){var e;return{token:new i((function(n){e=n})),cancel:e}},e.exports=i},502:e=>{"use strict";e.exports=function(e){return!(!e||!e.__CANCEL__)}},321:(e,n,t)=>{"use strict";var r=t(867),i=t(327),o=t(782),a=t(572),u=t(185),c=t(875),f=c.validators;function s(e){this.defaults=e,this.interceptors={request:new o,response:new o}}s.prototype.request=function(e){"string"==typeof e?(e=arguments[1]||{}).url=arguments[0]:e=e||{},(e=u(this.defaults,e)).method?e.method=e.method.toLowerCase():this.defaults.method?e.method=this.defaults.method.toLowerCase():e.method="get";var n=e.transitional;void 0!==n&&c.assertOptions(n,{silentJSONParsing:f.transitional(f.boolean,"1.0.0"),forcedJSONParsing:f.transitional(f.boolean,"1.0.0"),clarifyTimeoutError:f.transitional(f.boolean,"1.0.0")},!1);var t=[],r=!0;this.interceptors.request.forEach((function(n){"function"==typeof n.runWhen&&!1===n.runWhen(e)||(r=r&&n.synchronous,t.unshift(n.fulfilled,n.rejected))}));var i,o=[];if(this.interceptors.response.forEach((function(e){o.push(e.fulfilled,e.rejected)})),!r){var s=[a,void 0];for(Array.prototype.unshift.apply(s,t),s=s.concat(o),i=Promise.resolve(e);s.length;)i=i.then(s.shift(),s.shift());return i}for(var l=e;t.length;){var d=t.shift(),p=t.shift();try{l=d(l)}catch(e){p(e);break}}try{i=a(l)}catch(e){return Promise.reject(e)}for(;o.length;)i=i.then(o.shift(),o.shift());return i},s.prototype.getUri=function(e){return e=u(this.defaults,e),i(e.url,e.params,e.paramsSerializer).replace(/^\?/,"")},r.forEach(["delete","get","head","options"],(function(e){s.prototype[e]=function(n,t){return this.request(u(t||{},{method:e,url:n,data:(t||{}).data}))}})),r.forEach(["post","put","patch"],(function(e){s.prototype[e]=function(n,t,r){return this.request(u(r||{},{method:e,url:n,data:t}))}})),e.exports=s},782:(e,n,t)=>{"use strict";var r=t(867);function i(){this.handlers=[]}i.prototype.use=function(e,n,t){return this.handlers.push({fulfilled:e,rejected:n,synchronous:!!t&&t.synchronous,runWhen:t?t.runWhen:null}),this.handlers.length-1},i.prototype.eject=function(e){this.handlers[e]&&(this.handlers[e]=null)},i.prototype.forEach=function(e){r.forEach(this.handlers,(function(n){null!==n&&e(n)}))},e.exports=i},97:(e,n,t)=>{"use strict";var r=t(793),i=t(303);e.exports=function(e,n){return e&&!r(n)?i(e,n):n}},61:(e,n,t)=>{"use strict";var r=t(481);e.exports=function(e,n,t,i,o){var a=new Error(e);return r(a,n,t,i,o)}},572:(e,n,t)=>{"use strict";var r=t(867),i=t(527),o=t(502),a=t(655);function u(e){e.cancelToken&&e.cancelToken.throwIfRequested()}e.exports=function(e){return u(e),e.headers=e.headers||{},e.data=i.call(e,e.data,e.headers,e.transformRequest),e.headers=r.merge(e.headers.common||{},e.headers[e.method]||{},e.headers),r.forEach(["delete","get","head","post","put","patch","common"],(function(n){delete e.headers[n]})),(e.adapter||a.adapter)(e).then((function(n){return u(e),n.data=i.call(e,n.data,n.headers,e.transformResponse),n}),(function(n){return o(n)||(u(e),n&&n.response&&(n.response.data=i.call(e,n.response.data,n.response.headers,e.transformResponse))),Promise.reject(n)}))}},481:e=>{"use strict";e.exports=function(e,n,t,r,i){return e.config=n,t&&(e.code=t),e.request=r,e.response=i,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},185:(e,n,t)=>{"use strict";var r=t(867);e.exports=function(e,n){n=n||{};var t={},i=["url","method","data"],o=["headers","auth","proxy","params"],a=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],u=["validateStatus"];function c(e,n){return r.isPlainObject(e)&&r.isPlainObject(n)?r.merge(e,n):r.isPlainObject(n)?r.merge({},n):r.isArray(n)?n.slice():n}function f(i){r.isUndefined(n[i])?r.isUndefined(e[i])||(t[i]=c(void 0,e[i])):t[i]=c(e[i],n[i])}r.forEach(i,(function(e){r.isUndefined(n[e])||(t[e]=c(void 0,n[e]))})),r.forEach(o,f),r.forEach(a,(function(i){r.isUndefined(n[i])?r.isUndefined(e[i])||(t[i]=c(void 0,e[i])):t[i]=c(void 0,n[i])})),r.forEach(u,(function(r){r in n?t[r]=c(e[r],n[r]):r in e&&(t[r]=c(void 0,e[r]))}));var s=i.concat(o).concat(a).concat(u),l=Object.keys(e).concat(Object.keys(n)).filter((function(e){return-1===s.indexOf(e)}));return r.forEach(l,f),t}},26:(e,n,t)=>{"use strict";var r=t(61);e.exports=function(e,n,t){var i=t.config.validateStatus;t.status&&i&&!i(t.status)?n(r("Request failed with status code "+t.status,t.config,null,t.request,t)):e(t)}},527:(e,n,t)=>{"use strict";var r=t(867),i=t(655);e.exports=function(e,n,t){var o=this||i;return r.forEach(t,(function(t){e=t.call(o,e,n)})),e}},655:(e,n,t)=>{"use strict";var r=t(867),i=t(16),o=t(481),a={"Content-Type":"application/x-www-form-urlencoded"};function u(e,n){!r.isUndefined(e)&&r.isUndefined(e["Content-Type"])&&(e["Content-Type"]=n)}var c,f={transitional:{silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},adapter:(("undefined"!=typeof XMLHttpRequest||"undefined"!=typeof process&&"[object process]"===Object.prototype.toString.call(process))&&(c=t(448)),c),transformRequest:[function(e,n){return i(n,"Accept"),i(n,"Content-Type"),r.isFormData(e)||r.isArrayBuffer(e)||r.isBuffer(e)||r.isStream(e)||r.isFile(e)||r.isBlob(e)?e:r.isArrayBufferView(e)?e.buffer:r.isURLSearchParams(e)?(u(n,"application/x-www-form-urlencoded;charset=utf-8"),e.toString()):r.isObject(e)||n&&"application/json"===n["Content-Type"]?(u(n,"application/json"),function(e,n,t){if(r.isString(e))try{return(0,JSON.parse)(e),r.trim(e)}catch(e){if("SyntaxError"!==e.name)throw e}return(0,JSON.stringify)(e)}(e)):e}],transformResponse:[function(e){var n=this.transitional,t=n&&n.silentJSONParsing,i=n&&n.forcedJSONParsing,a=!t&&"json"===this.responseType;if(a||i&&r.isString(e)&&e.length)try{return JSON.parse(e)}catch(e){if(a){if("SyntaxError"===e.name)throw o(e,this,"E_JSON_PARSE");throw e}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*"}}};r.forEach(["delete","get","head"],(function(e){f.headers[e]={}})),r.forEach(["post","put","patch"],(function(e){f.headers[e]=r.merge(a)})),e.exports=f},849:e=>{"use strict";e.exports=function(e,n){return function(){for(var t=new Array(arguments.length),r=0;r{"use strict";var r=t(867);function i(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}e.exports=function(e,n,t){if(!n)return e;var o;if(t)o=t(n);else if(r.isURLSearchParams(n))o=n.toString();else{var a=[];r.forEach(n,(function(e,n){null!=e&&(r.isArray(e)?n+="[]":e=[e],r.forEach(e,(function(e){r.isDate(e)?e=e.toISOString():r.isObject(e)&&(e=JSON.stringify(e)),a.push(i(n)+"="+i(e))})))})),o=a.join("&")}if(o){var u=e.indexOf("#");-1!==u&&(e=e.slice(0,u)),e+=(-1===e.indexOf("?")?"?":"&")+o}return e}},303:e=>{"use strict";e.exports=function(e,n){return n?e.replace(/\/+$/,"")+"/"+n.replace(/^\/+/,""):e}},372:(e,n,t)=>{"use strict";var r=t(867);e.exports=r.isStandardBrowserEnv()?{write:function(e,n,t,i,o,a){var u=[];u.push(e+"="+encodeURIComponent(n)),r.isNumber(t)&&u.push("expires="+new Date(t).toGMTString()),r.isString(i)&&u.push("path="+i),r.isString(o)&&u.push("domain="+o),!0===a&&u.push("secure"),document.cookie=u.join("; ")},read:function(e){var n=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return n?decodeURIComponent(n[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}:{write:function(){},read:function(){return null},remove:function(){}}},793:e=>{"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},268:e=>{"use strict";e.exports=function(e){return"object"==typeof e&&!0===e.isAxiosError}},985:(e,n,t)=>{"use strict";var r=t(867);e.exports=r.isStandardBrowserEnv()?function(){var e,n=/(msie|trident)/i.test(navigator.userAgent),t=document.createElement("a");function i(e){var r=e;return n&&(t.setAttribute("href",r),r=t.href),t.setAttribute("href",r),{href:t.href,protocol:t.protocol?t.protocol.replace(/:$/,""):"",host:t.host,search:t.search?t.search.replace(/^\?/,""):"",hash:t.hash?t.hash.replace(/^#/,""):"",hostname:t.hostname,port:t.port,pathname:"/"===t.pathname.charAt(0)?t.pathname:"/"+t.pathname}}return e=i(window.location.href),function(n){var t=r.isString(n)?i(n):n;return t.protocol===e.protocol&&t.host===e.host}}():function(){return!0}},16:(e,n,t)=>{"use strict";var r=t(867);e.exports=function(e,n){r.forEach(e,(function(t,r){r!==n&&r.toUpperCase()===n.toUpperCase()&&(e[n]=t,delete e[r])}))}},109:(e,n,t)=>{"use strict";var r=t(867),i=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var n,t,o,a={};return e?(r.forEach(e.split("\n"),(function(e){if(o=e.indexOf(":"),n=r.trim(e.substr(0,o)).toLowerCase(),t=r.trim(e.substr(o+1)),n){if(a[n]&&i.indexOf(n)>=0)return;a[n]="set-cookie"===n?(a[n]?a[n]:[]).concat([t]):a[n]?a[n]+", "+t:t}})),a):a}},713:e=>{"use strict";e.exports=function(e){return function(n){return e.apply(null,n)}}},875:(e,n,t)=>{"use strict";var r=t(593),i={};["object","boolean","number","function","string","symbol"].forEach((function(e,n){i[e]=function(t){return typeof t===e||"a"+(n<1?"n ":" ")+e}}));var o={},a=r.version.split(".");function u(e,n){for(var t=n?n.split("."):a,r=e.split("."),i=0;i<3;i++){if(t[i]>r[i])return!0;if(t[i]0;){var o=r[i],a=n[o];if(a){var u=e[o],c=void 0===u||a(u,o,e);if(!0!==c)throw new TypeError("option "+o+" must be "+c)}else if(!0!==t)throw Error("Unknown option "+o)}},validators:i}},867:(e,n,t)=>{"use strict";var r=t(849),i=Object.prototype.toString;function o(e){return"[object Array]"===i.call(e)}function a(e){return void 0===e}function u(e){return null!==e&&"object"==typeof e}function c(e){if("[object Object]"!==i.call(e))return!1;var n=Object.getPrototypeOf(e);return null===n||n===Object.prototype}function f(e){return"[object Function]"===i.call(e)}function s(e,n){if(null!=e)if("object"!=typeof e&&(e=[e]),o(e))for(var t=0,r=e.length;t{!function(){function e(e){var n=Object.create(null);return function(t){var r=o(t)?t:JSON.stringify(t);return n[r]||(n[r]=e(t))}}var n=e((function(e){return e.replace(/([A-Z])/g,(function(e){return"-"+e.toLowerCase()}))})),r=Object.prototype.hasOwnProperty,i=Object.assign||function(e){for(var n=arguments,t=1;t0&&n[1].toLowerCase()!==location.protocol||"string"==typeof n[2]&&n[2].length>0&&n[2].replace(new RegExp(":("+{"http:":80,"https:":443}[location.protocol]+")?$"),"")!==location.host}var f=document.body.clientWidth<=600,s=window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/),l={};function d(e,n){if(void 0===n&&(n=!1),"string"==typeof e){if(void 0!==window.Vue)return m(e);e=n?m(e):l[e]||(l[e]=m(e))}return e}var p=document,h=p.body,g=p.head;function m(e,n){return n?e.querySelector(n):p.querySelector(e)}function _(e,n){return[].slice.call(n?e.querySelectorAll(n):p.querySelectorAll(e))}function b(e,n){return e=p.createElement(e),n&&(e.innerHTML=n),e}function v(e,n){return e.appendChild(n)}function y(e,n){return e.insertBefore(n,e.children[0])}function w(e,n,t){u(n)?window.addEventListener(e,n):e.addEventListener(n,t)}function k(e,n,t){u(n)?window.removeEventListener(e,n):e.removeEventListener(n,t)}function x(e,n,t){e&&e.classList[t?n:"toggle"](t||n)}function S(e,n){void 0===n&&(n=document);var t=n.readyState;if("complete"===t||"interactive"===t)return setTimeout(e,0);n.addEventListener("DOMContentLoaded",e)}var A=Object.freeze({__proto__:null,getNode:d,$:p,body:h,head:g,find:m,findAll:_,create:b,appendTo:v,before:y,on:w,off:k,toggleClass:x,style:function(e){v(g,b("style",e))},documentReady:S}),E=decodeURIComponent,T=encodeURIComponent;function O(e){var n={};return(e=e.trim().replace(/^(\?|#|&)/,""))?(e.split("&").forEach((function(e){var t=e.replace(/\+/g," ").split("=");n[t[0]]=t[1]&&E(t[1])})),n):n}function R(e,n){void 0===n&&(n=[]);var t=[];for(var r in e)n.indexOf(r)>-1||t.push(e[r]?(T(r)+"="+T(e[r])).toLowerCase():T(r));return t.length?"?"+t.join("&"):""}var z=e((function(e){return/(:|(\/{2}))/g.test(e)})),j=e((function(e){return e.split(/[?#]/)[0]})),C=e((function(e){if(/\/$/g.test(e))return e;var n=e.match(/(\S*\/)[^/]+$/);return n?n[1]:""})),L=e((function(e){return e.replace(/^\/+/,"/").replace(/([^:])\/{2,}/g,"$1/")})),N=e((function(e){for(var n=e.replace(/^\//,"").split("/"),t=[],r=0,i=n.length;r=0?n:0)+"#"+e)}q.prototype.getBasePath=function(){return this.config.basePath},q.prototype.getFile=function(e,n){void 0===e&&(e=this.getCurrentPath());var t=this.config,r=this.getBasePath(),i="string"==typeof t.ext?t.ext:".md";return e=function(e,n){return new RegExp("\\.("+n.replace(/^\./,"")+"|html)$","g").test(e)?e:/\/$/g.test(e)?e+"README"+n:""+e+n}(e=t.alias?M(e,t.alias):e,i),e=e==="/README"+i&&t.homepage||e,e=z(e)?e:I(r,e),n&&(e=e.replace(new RegExp("^"+r),"")),e},q.prototype.onchange=function(e){void 0===e&&(e=a),e()},q.prototype.getCurrentPath=function(){},q.prototype.normalize=function(){},q.prototype.parse=function(){},q.prototype.toURL=function(e,n,t){var r=t&&"#"===e[0],o=this.parse(D(e));if(o.query=i({},o.query,n),e=(e=o.path+R(o.query)).replace(/\.md(\?)|\.md$/,"$1"),r){var a=t.indexOf("?");e=(a>0?t.substring(0,a):t)+e}if(this.config.relativePath&&0!==e.indexOf("/")){var u=t.substring(0,t.lastIndexOf("/")+1);return L(N(u+e))}return L("/"+e)};var B=function(e){function n(n){e.call(this,n),this.mode="hash"}return e&&(n.__proto__=e),n.prototype=Object.create(e&&e.prototype),n.prototype.constructor=n,n.prototype.getBasePath=function(){var e=window.location.pathname||"",n=this.config.basePath,t=P(e,".html")?e+"#/"+n:e+"/"+n;return/^(\/|https?:)/g.test(n)?n:L(t)},n.prototype.getCurrentPath=function(){var e=location.href,n=e.indexOf("#");return-1===n?"":e.slice(n+1)},n.prototype.onchange=function(e){void 0===e&&(e=a);var n=!1;w("click",(function(e){var t="A"===e.target.tagName?e.target:e.target.parentNode;t&&"A"===t.tagName&&!/_blank/.test(t.target)&&(n=!0)})),w("hashchange",(function(t){var r=n?"navigate":"history";n=!1,e({event:t,source:r})}))},n.prototype.normalize=function(){var e=this.getCurrentPath();if("/"===(e=D(e)).charAt(0))return U(e);U("/"+e)},n.prototype.parse=function(e){void 0===e&&(e=location.href);var n="",t=e.indexOf("#");t>=0&&(e=e.slice(t+1));var r=e.indexOf("?");return r>=0&&(n=e.slice(r+1),e=e.slice(0,r)),{path:e,file:this.getFile(e,!0),query:O(n)}},n.prototype.toURL=function(n,t,r){return"#"+e.prototype.toURL.call(this,n,t,r)},n}(q),H=function(e){function n(n){e.call(this,n),this.mode="history"}return e&&(n.__proto__=e),n.prototype=Object.create(e&&e.prototype),n.prototype.constructor=n,n.prototype.getCurrentPath=function(){var e=this.getBasePath(),n=window.location.pathname;return e&&0===n.indexOf(e)&&(n=n.slice(e.length)),(n||"/")+window.location.search+window.location.hash},n.prototype.onchange=function(e){var n=this;void 0===e&&(e=a),w("click",(function(t){var r="A"===t.target.tagName?t.target:t.target.parentNode;if(r&&"A"===r.tagName&&!/_blank/.test(r.target)){t.preventDefault();var i=r.href;-1!==n.config.crossOriginLinks.indexOf(i)?window.open(i,"_self"):window.history.pushState({key:i},"",i),e({event:t,source:"navigate"})}})),w("popstate",(function(n){e({event:n,source:"history"})}))},n.prototype.parse=function(e){void 0===e&&(e=location.href);var n="",t=e.indexOf("?");t>=0&&(n=e.slice(t+1),e=e.slice(0,t));var r=I(location.origin),i=e.indexOf(r);return i>-1&&(e=e.slice(i+r.length)),{path:e,file:this.getFile(e),query:O(n)}},n}(q),W={},G=/([^{]*?)\w(?=\})/g,Z={YYYY:"getFullYear",YY:"getYear",MM:function(e){return e.getMonth()+1},DD:"getDate",HH:"getHours",mm:"getMinutes",ss:"getSeconds",fff:"getMilliseconds"},V=Object.hasOwnProperty,Y=Object.setPrototypeOf,X=Object.isFrozen,J=Object.getPrototypeOf,K=Object.getOwnPropertyDescriptor,Q=Object.freeze,ee=Object.seal,ne=Object.create,te="undefined"!=typeof Reflect&&Reflect,re=te.apply,ie=te.construct;re||(re=function(e,n,t){return e.apply(n,t)}),Q||(Q=function(e){return e}),ee||(ee=function(e){return e}),ie||(ie=function(e,n){return new(Function.prototype.bind.apply(e,[null].concat(function(e){if(Array.isArray(e)){for(var n=0,t=Array(e.length);n1?r-1:0),o=1;o/gm),Le=ee(/^data-[\-\w.\u00B7-\uFFFF]/),Ne=ee(/^aria-[\-\w]+$/),$e=ee(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Ie=ee(/^(?:\w+script|data):/i),De=ee(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Pe="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};function Fe(e){if(Array.isArray(e)){for(var n=0,t=Array(e.length);n0&&void 0!==arguments[0]?arguments[0]:Ue(),t=function(n){return e(n)};if(t.version="2.3.1",t.removed=[],!n||!n.document||9!==n.document.nodeType)return t.isSupported=!1,t;var r=n.document,i=n.document,o=n.DocumentFragment,a=n.HTMLTemplateElement,u=n.Node,c=n.Element,f=n.NodeFilter,s=n.NamedNodeMap,l=void 0===s?n.NamedNodeMap||n.MozNamedAttrMap:s,d=n.Text,p=n.Comment,h=n.DOMParser,g=n.trustedTypes,m=c.prototype,_=ve(m,"cloneNode"),b=ve(m,"nextSibling"),v=ve(m,"childNodes"),y=ve(m,"parentNode");if("function"==typeof a){var w=i.createElement("template");w.content&&w.content.ownerDocument&&(i=w.content.ownerDocument)}var k=Be(g,r),x=k&&te?k.createHTML(""):"",S=i,A=S.implementation,E=S.createNodeIterator,T=S.createDocumentFragment,O=S.getElementsByTagName,R=r.importNode,z={};try{z=be(i).documentMode?i.documentMode:{}}catch(e){}var j={};t.isSupported="function"==typeof y&&A&&void 0!==A.createHTMLDocument&&9!==z;var C=je,L=Ce,N=Le,$=Ne,I=Ie,D=De,P=$e,F=null,M=_e({},[].concat(Fe(ye),Fe(we),Fe(ke),Fe(Se),Fe(Ee))),q=null,U=_e({},[].concat(Fe(Te),Fe(Oe),Fe(Re),Fe(ze))),B=null,H=null,W=!0,G=!0,Z=!1,V=!1,Y=!1,X=!1,J=!1,K=!1,ee=!1,ne=!0,te=!1,re=!0,ie=!0,oe=!1,me={},Me=null,qe=_e({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),He=null,We=_e({},["audio","video","img","source","image","track"]),Ge=null,Ze=_e({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ve="http://www.w3.org/1998/Math/MathML",Ye="http://www.w3.org/2000/svg",Xe="http://www.w3.org/1999/xhtml",Je=Xe,Ke=!1,Qe=null,en=i.createElement("form"),nn=function(e){Qe&&Qe===e||(e&&"object"===(void 0===e?"undefined":Pe(e))||(e={}),e=be(e),F="ALLOWED_TAGS"in e?_e({},e.ALLOWED_TAGS):M,q="ALLOWED_ATTR"in e?_e({},e.ALLOWED_ATTR):U,Ge="ADD_URI_SAFE_ATTR"in e?_e(be(Ze),e.ADD_URI_SAFE_ATTR):Ze,He="ADD_DATA_URI_TAGS"in e?_e(be(We),e.ADD_DATA_URI_TAGS):We,Me="FORBID_CONTENTS"in e?_e({},e.FORBID_CONTENTS):qe,B="FORBID_TAGS"in e?_e({},e.FORBID_TAGS):{},H="FORBID_ATTR"in e?_e({},e.FORBID_ATTR):{},me="USE_PROFILES"in e&&e.USE_PROFILES,W=!1!==e.ALLOW_ARIA_ATTR,G=!1!==e.ALLOW_DATA_ATTR,Z=e.ALLOW_UNKNOWN_PROTOCOLS||!1,V=e.SAFE_FOR_TEMPLATES||!1,Y=e.WHOLE_DOCUMENT||!1,K=e.RETURN_DOM||!1,ee=e.RETURN_DOM_FRAGMENT||!1,ne=!1!==e.RETURN_DOM_IMPORT,te=e.RETURN_TRUSTED_TYPE||!1,J=e.FORCE_BODY||!1,re=!1!==e.SANITIZE_DOM,ie=!1!==e.KEEP_CONTENT,oe=e.IN_PLACE||!1,P=e.ALLOWED_URI_REGEXP||P,Je=e.NAMESPACE||Xe,V&&(G=!1),ee&&(K=!0),me&&(F=_e({},[].concat(Fe(Ee))),q=[],!0===me.html&&(_e(F,ye),_e(q,Te)),!0===me.svg&&(_e(F,we),_e(q,Oe),_e(q,ze)),!0===me.svgFilters&&(_e(F,ke),_e(q,Oe),_e(q,ze)),!0===me.mathMl&&(_e(F,Se),_e(q,Re),_e(q,ze))),e.ADD_TAGS&&(F===M&&(F=be(F)),_e(F,e.ADD_TAGS)),e.ADD_ATTR&&(q===U&&(q=be(q)),_e(q,e.ADD_ATTR)),e.ADD_URI_SAFE_ATTR&&_e(Ge,e.ADD_URI_SAFE_ATTR),e.FORBID_CONTENTS&&(Me===qe&&(Me=be(Me)),_e(Me,e.FORBID_CONTENTS)),ie&&(F["#text"]=!0),Y&&_e(F,["html","head","body"]),F.table&&(_e(F,["tbody"]),delete B.tbody),Q&&Q(e),Qe=e)},tn=_e({},["mi","mo","mn","ms","mtext"]),rn=_e({},["foreignobject","desc","title","annotation-xml"]),on=_e({},we);_e(on,ke),_e(on,xe);var an=_e({},Se);_e(an,Ae);var un=function(e){var n=y(e);n&&n.tagName||(n={namespaceURI:Xe,tagName:"template"});var t=fe(e.tagName),r=fe(n.tagName);if(e.namespaceURI===Ye)return n.namespaceURI===Xe?"svg"===t:n.namespaceURI===Ve?"svg"===t&&("annotation-xml"===r||tn[r]):Boolean(on[t]);if(e.namespaceURI===Ve)return n.namespaceURI===Xe?"math"===t:n.namespaceURI===Ye?"math"===t&&rn[r]:Boolean(an[t]);if(e.namespaceURI===Xe){if(n.namespaceURI===Ye&&!rn[r])return!1;if(n.namespaceURI===Ve&&!tn[r])return!1;var i=_e({},["title","style","font","a","script"]);return!an[t]&&(i[t]||!on[t])}return!1},cn=function(e){ce(t.removed,{element:e});try{e.parentNode.removeChild(e)}catch(n){try{e.outerHTML=x}catch(n){e.remove()}}},fn=function(e,n){try{ce(t.removed,{attribute:n.getAttributeNode(e),from:n})}catch(e){ce(t.removed,{attribute:null,from:n})}if(n.removeAttribute(e),"is"===e&&!q[e])if(K||ee)try{cn(n)}catch(e){}else try{n.setAttribute(e,"")}catch(e){}},sn=function(e){var n=void 0,t=void 0;if(J)e=""+e;else{var r=se(e,/^[\r\n\t ]+/);t=r&&r[0]}var o=k?k.createHTML(e):e;if(Je===Xe)try{n=(new h).parseFromString(o,"text/html")}catch(e){}if(!n||!n.documentElement){n=A.createDocument(Je,"template",null);try{n.documentElement.innerHTML=Ke?"":o}catch(e){}}var a=n.body||n.documentElement;return e&&t&&a.insertBefore(i.createTextNode(t),a.childNodes[0]||null),Je===Xe?O.call(n,Y?"html":"body")[0]:Y?n.documentElement:a},ln=function(e){return E.call(e.ownerDocument||e,e,f.SHOW_ELEMENT|f.SHOW_COMMENT|f.SHOW_TEXT,null,!1)},dn=function(e){return!(e instanceof d||e instanceof p||"string"==typeof e.nodeName&&"string"==typeof e.textContent&&"function"==typeof e.removeChild&&e.attributes instanceof l&&"function"==typeof e.removeAttribute&&"function"==typeof e.setAttribute&&"string"==typeof e.namespaceURI&&"function"==typeof e.insertBefore)},pn=function(e){return"object"===(void 0===u?"undefined":Pe(u))?e instanceof u:e&&"object"===(void 0===e?"undefined":Pe(e))&&"number"==typeof e.nodeType&&"string"==typeof e.nodeName},hn=function(e,n,r){j[e]&&ae(j[e],(function(e){e.call(t,n,r,Qe)}))},gn=function(e){var n=void 0;if(hn("beforeSanitizeElements",e,null),dn(e))return cn(e),!0;if(se(e.nodeName,/[\u0080-\uFFFF]/))return cn(e),!0;var r=fe(e.nodeName);if(hn("uponSanitizeElement",e,{tagName:r,allowedTags:F}),!pn(e.firstElementChild)&&(!pn(e.content)||!pn(e.content.firstElementChild))&&he(/<[/\w]/g,e.innerHTML)&&he(/<[/\w]/g,e.textContent))return cn(e),!0;if("select"===r&&he(/