├── .codeclimate.yml ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── error-request.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── autofix.yaml │ ├── changelog.yml │ ├── deploy.yml │ ├── deptrac.yaml │ ├── lock.yml │ ├── phpstan.yaml │ ├── phpunits.yaml │ ├── semgrep.yml │ └── stale.yml ├── .gitignore ├── .phpstorm.meta.php ├── .whitesource ├── LICENSE ├── README.md ├── changelog.md ├── composer.json ├── config └── config.php ├── database ├── 2018_11_06_222923_create_transactions_table.php ├── 2018_11_07_192923_create_transfers_table.php ├── 2018_11_15_124230_create_wallets_table.php ├── 2021_11_02_202021_update_wallets_uuid_table.php ├── 2023_12_30_113122_extra_columns_removed.php ├── 2023_12_30_204610_soft_delete.php └── 2024_01_24_185401_add_extra_column_in_transfer.php ├── deptrac.yaml ├── docs ├── .vitepress │ ├── .gitignore │ ├── config.mts │ └── theme │ │ ├── custom.css │ │ └── index.js ├── _include │ ├── composer.md │ ├── eager_loading.md │ └── models │ │ ├── user_simple.md │ │ └── user_simple_float.md ├── guide │ ├── additions │ │ ├── swap.md │ │ └── uuid.md │ ├── cqrs │ │ └── create-wallet.md │ ├── db │ │ ├── atomic-service.md │ │ ├── race-condition.md │ │ └── transaction.md │ ├── events │ │ ├── balance-updated-event.md │ │ ├── customize.md │ │ ├── transaction-created-event.md │ │ └── wallet-created-event.md │ ├── fractional │ │ ├── deposit.md │ │ ├── transfer.md │ │ └── withdraw.md │ ├── helpers │ │ └── formatter.md │ ├── high-performance │ │ ├── batch-transactions.md │ │ └── batch-transfers.md │ ├── introduction │ │ ├── basic-usage.md │ │ ├── configuration.md │ │ ├── index.md │ │ ├── installation.md │ │ └── upgrade.md │ ├── multi │ │ ├── new-wallet.md │ │ ├── transaction-filter.md │ │ └── transfer.md │ ├── purchases │ │ ├── cart.md │ │ ├── commissions.md │ │ ├── gift.md │ │ ├── payment-free.md │ │ ├── payment.md │ │ ├── receiving.md │ │ └── refund.md │ └── single │ │ ├── cancel.md │ │ ├── confirm.md │ │ ├── credit-limits.md │ │ ├── deposit.md │ │ ├── exchange.md │ │ ├── refresh.md │ │ ├── transfer.md │ │ └── withdraw.md ├── icon.svg └── index.md ├── ecs.php ├── infection.json.dist ├── package-lock.json ├── package.json ├── phpstan.common.neon ├── phpstan.src.baseline.neon ├── phpstan.src.neon ├── phpstan.tests.baseline.neon ├── phpstan.tests.neon ├── phpunit.xml ├── rector.php ├── resources └── lang │ ├── ar │ └── errors.php │ ├── en │ └── errors.php │ ├── es │ └── errors.php │ ├── fa │ └── errors.php │ ├── ru │ └── errors.php │ └── zh-CN │ └── errors.php ├── src ├── Exceptions │ ├── AmountInvalid.php │ ├── BalanceIsEmpty.php │ ├── ConfirmedInvalid.php │ ├── InsufficientFunds.php │ ├── ProductEnded.php │ ├── UnconfirmedInvalid.php │ └── WalletOwnerInvalid.php ├── External │ ├── Api │ │ ├── TransactionFloatQuery.php │ │ ├── TransactionQuery.php │ │ ├── TransactionQueryHandler.php │ │ ├── TransactionQueryHandlerInterface.php │ │ ├── TransactionQueryInterface.php │ │ ├── TransferFloatQuery.php │ │ ├── TransferQuery.php │ │ ├── TransferQueryHandler.php │ │ ├── TransferQueryHandlerInterface.php │ │ └── TransferQueryInterface.php │ ├── Contracts │ │ ├── ExtraDtoInterface.php │ │ └── OptionDtoInterface.php │ └── Dto │ │ ├── Extra.php │ │ └── Option.php ├── Interfaces │ ├── CartInterface.php │ ├── Confirmable.php │ ├── Customer.php │ ├── Discount.php │ ├── Exchangeable.php │ ├── MaximalTaxable.php │ ├── MinimalTaxable.php │ ├── ProductInterface.php │ ├── ProductLimitedInterface.php │ ├── Taxable.php │ ├── Wallet.php │ └── WalletFloat.php ├── Internal │ ├── Assembler │ │ ├── AvailabilityDtoAssembler.php │ │ ├── AvailabilityDtoAssemblerInterface.php │ │ ├── BalanceUpdatedEventAssembler.php │ │ ├── BalanceUpdatedEventAssemblerInterface.php │ │ ├── ExtraDtoAssembler.php │ │ ├── ExtraDtoAssemblerInterface.php │ │ ├── OptionDtoAssembler.php │ │ ├── OptionDtoAssemblerInterface.php │ │ ├── TransactionCreatedEventAssembler.php │ │ ├── TransactionCreatedEventAssemblerInterface.php │ │ ├── TransactionDtoAssembler.php │ │ ├── TransactionDtoAssemblerInterface.php │ │ ├── TransactionQueryAssembler.php │ │ ├── TransactionQueryAssemblerInterface.php │ │ ├── TransferDtoAssembler.php │ │ ├── TransferDtoAssemblerInterface.php │ │ ├── TransferLazyDtoAssembler.php │ │ ├── TransferLazyDtoAssemblerInterface.php │ │ ├── TransferQueryAssembler.php │ │ ├── TransferQueryAssemblerInterface.php │ │ ├── WalletCreatedEventAssembler.php │ │ └── WalletCreatedEventAssemblerInterface.php │ ├── Decorator │ │ └── StorageServiceLockDecorator.php │ ├── Dto │ │ ├── AvailabilityDto.php │ │ ├── AvailabilityDtoInterface.php │ │ ├── BasketDto.php │ │ ├── BasketDtoInterface.php │ │ ├── ItemDto.php │ │ ├── ItemDtoInterface.php │ │ ├── TransactionDto.php │ │ ├── TransactionDtoInterface.php │ │ ├── TransferDto.php │ │ ├── TransferDtoInterface.php │ │ ├── TransferLazyDto.php │ │ └── TransferLazyDtoInterface.php │ ├── Events │ │ ├── BalanceUpdatedEvent.php │ │ ├── BalanceUpdatedEventInterface.php │ │ ├── EventInterface.php │ │ ├── TransactionCreatedEvent.php │ │ ├── TransactionCreatedEventInterface.php │ │ ├── WalletCreatedEvent.php │ │ └── WalletCreatedEventInterface.php │ ├── Exceptions │ │ ├── CartEmptyException.php │ │ ├── ExceptionInterface.php │ │ ├── InvalidArgumentExceptionInterface.php │ │ ├── LogicExceptionInterface.php │ │ ├── ModelNotFoundException.php │ │ ├── RecordNotFoundException.php │ │ ├── RuntimeExceptionInterface.php │ │ ├── TransactionFailedException.php │ │ ├── TransactionRollbackException.php │ │ └── UnderflowExceptionInterface.php │ ├── Listeners │ │ ├── TransactionBeginningListener.php │ │ ├── TransactionCommittedListener.php │ │ ├── TransactionCommittingListener.php │ │ └── TransactionRolledBackListener.php │ ├── Observers │ │ ├── TransactionObserver.php │ │ └── TransferObserver.php │ ├── Query │ │ ├── TransactionQuery.php │ │ ├── TransactionQueryInterface.php │ │ ├── TransferQuery.php │ │ └── TransferQueryInterface.php │ ├── Repository │ │ ├── TransactionRepository.php │ │ ├── TransactionRepositoryInterface.php │ │ ├── TransferRepository.php │ │ ├── TransferRepositoryInterface.php │ │ ├── WalletRepository.php │ │ └── WalletRepositoryInterface.php │ ├── Service │ │ ├── ClockService.php │ │ ├── ClockServiceInterface.php │ │ ├── ConnectionService.php │ │ ├── ConnectionServiceInterface.php │ │ ├── DatabaseService.php │ │ ├── DatabaseServiceInterface.php │ │ ├── DispatcherService.php │ │ ├── DispatcherServiceInterface.php │ │ ├── IdentifierFactoryService.php │ │ ├── IdentifierFactoryServiceInterface.php │ │ ├── JsonService.php │ │ ├── JsonServiceInterface.php │ │ ├── LockService.php │ │ ├── LockServiceInterface.php │ │ ├── MathService.php │ │ ├── MathServiceInterface.php │ │ ├── StateService.php │ │ ├── StateServiceInterface.php │ │ ├── StorageService.php │ │ ├── StorageServiceInterface.php │ │ ├── TranslatorService.php │ │ ├── TranslatorServiceInterface.php │ │ ├── UuidFactoryService.php │ │ └── UuidFactoryServiceInterface.php │ └── Transform │ │ ├── TransactionDtoTransformer.php │ │ ├── TransactionDtoTransformerInterface.php │ │ ├── TransferDtoTransformer.php │ │ └── TransferDtoTransformerInterface.php ├── Models │ ├── Transaction.php │ ├── Transfer.php │ └── Wallet.php ├── Objects │ └── Cart.php ├── Services │ ├── AssistantService.php │ ├── AssistantServiceInterface.php │ ├── AtmService.php │ ├── AtmServiceInterface.php │ ├── AtomicService.php │ ├── AtomicServiceInterface.php │ ├── BasketService.php │ ├── BasketServiceInterface.php │ ├── BookkeeperService.php │ ├── BookkeeperServiceInterface.php │ ├── CastService.php │ ├── CastServiceInterface.php │ ├── ConsistencyService.php │ ├── ConsistencyServiceInterface.php │ ├── DiscountService.php │ ├── DiscountServiceInterface.php │ ├── EagerLoaderService.php │ ├── EagerLoaderServiceInterface.php │ ├── ExchangeService.php │ ├── ExchangeServiceInterface.php │ ├── FormatterService.php │ ├── FormatterServiceInterface.php │ ├── PrepareService.php │ ├── PrepareServiceInterface.php │ ├── PurchaseService.php │ ├── PurchaseServiceInterface.php │ ├── RegulatorService.php │ ├── RegulatorServiceInterface.php │ ├── TaxService.php │ ├── TaxServiceInterface.php │ ├── TransactionService.php │ ├── TransactionServiceInterface.php │ ├── TransferService.php │ ├── TransferServiceInterface.php │ ├── WalletService.php │ └── WalletServiceInterface.php ├── Traits │ ├── CanConfirm.php │ ├── CanExchange.php │ ├── CanPay.php │ ├── CanPayFloat.php │ ├── CartPay.php │ ├── HasGift.php │ ├── HasWallet.php │ ├── HasWalletFloat.php │ ├── HasWallets.php │ └── MorphOneWallet.php ├── WalletConfigure.php └── WalletServiceProvider.php └── tests ├── Infra ├── Exceptions │ ├── PriceNotSetException.php │ └── UnknownEventException.php ├── Factories │ ├── BuyerFactory.php │ ├── ItemDiscountFactory.php │ ├── ItemDiscountTaxFactory.php │ ├── ItemFactory.php │ ├── ItemMaxTaxFactory.php │ ├── ItemMetaFactory.php │ ├── ItemMinTaxFactory.php │ ├── ItemMultiPriceFactory.php │ ├── ItemTaxFactory.php │ ├── ItemWalletFactory.php │ ├── ManagerFactory.php │ ├── UserCashierFactory.php │ ├── UserConfirmFactory.php │ ├── UserDynamicFactory.php │ ├── UserFactory.php │ ├── UserFloatFactory.php │ └── UserMultiFactory.php ├── Helpers │ └── Config.php ├── Listeners │ ├── BalanceUpdatedThrowDateListener.php │ ├── BalanceUpdatedThrowIdListener.php │ ├── BalanceUpdatedThrowUuidListener.php │ ├── TransactionCreatedThrowListener.php │ └── WalletCreatedThrowListener.php ├── Models │ ├── Buyer.php │ ├── Item.php │ ├── ItemDiscount.php │ ├── ItemDiscountTax.php │ ├── ItemMaxTax.php │ ├── ItemMeta.php │ ├── ItemMinTax.php │ ├── ItemMultiPrice.php │ ├── ItemTax.php │ ├── ItemWallet.php │ ├── Manager.php │ ├── User.php │ ├── UserCashier.php │ ├── UserConfirm.php │ ├── UserDynamic.php │ ├── UserFloat.php │ └── UserMulti.php ├── PackageModels │ ├── MyWallet.php │ ├── Transaction.php │ ├── TransactionMoney.php │ ├── Transfer.php │ └── Wallet.php ├── Services │ ├── ClockFakeService.php │ ├── ExchangeUsdToBtcService.php │ └── MyExchangeService.php ├── TestCase.php ├── TestServiceProvider.php ├── Transform │ └── TransactionDtoTransformerCustom.php └── Values │ └── Money.php ├── Units ├── Api │ ├── TransactionHandlerTest.php │ └── TransferHandlerTest.php ├── Domain │ ├── BalanceTest.php │ ├── BasketTest.php │ ├── BlockTest.php │ ├── CartReceivingTest.php │ ├── CartTest.php │ ├── ConfirmTest.php │ ├── CreditWalletTest.php │ ├── DiscountTaxTest.php │ ├── DiscountTest.php │ ├── EagerLoadingTest.php │ ├── EventTest.php │ ├── ExchangeTest.php │ ├── ExtraTest.php │ ├── GiftDiscountTaxTest.php │ ├── GiftDiscountTest.php │ ├── GiftTest.php │ ├── MinTaxTest.php │ ├── ModelTableTest.php │ ├── MultiWalletGiftTest.php │ ├── MultiWalletTest.php │ ├── ProductTest.php │ ├── SilentlyDiscardingTest.php │ ├── SoftDeletesTest.php │ ├── StateTest.php │ ├── TaxTest.php │ ├── TransactionAmountFloatAccessorTest.php │ ├── TransactionsFilterTest.php │ ├── WalletExtensionTest.php │ ├── WalletFloatTest.php │ └── WalletTest.php ├── Expand │ └── WalletTest.php └── Service │ ├── AtomicServiceTest.php │ ├── BookkeeperTest.php │ ├── DatabaseTest.php │ ├── DeferrableTest.php │ ├── FormatterTest.php │ ├── JsonServiceTest.php │ ├── LockServiceTest.php │ ├── MathTest.php │ ├── SingletonTest.php │ ├── StorageTest.php │ ├── WalletConfigureTest.php │ └── WalletTest.php └── migrations ├── 2014_10_12_000000_create_users_table.php ├── 2018_11_08_214421_create_items_table.php ├── 2019_09_19_191432_alter_column_transaction_table.php ├── 2022_01_26_185311_create_managers_table.php └── 2023_12_13_190445_create_cache_table.php /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_patterns: 2 | - "config/" 3 | - "database/" 4 | - "docs/" 5 | - "resources/" 6 | - "tests/" 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: laravel-wallet 2 | custom: ["https://www.tinkoff.ru/cf/1OkIPwTvTLf"] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, question 6 | assignees: rez1dent3 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Trace Error** 21 | Full error trace... 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Server:** 30 | - php version: [e.g. 8.1] 31 | - database: [e.g mysql 8.0] 32 | - wallet version [e.g. 8.2] 33 | - cache lock: [e.g. redis] 34 | - cache wallets: [e.g. redis] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/error-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Error request 3 | about: An error occurred, please help 4 | title: '' 5 | labels: question 6 | assignees: rez1dent3 7 | 8 | --- 9 | 10 | **Describe your task** 11 | A clear and concise description of your task. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Trace Error** 21 | Full error trace... If there is 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Screenshots** 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | **Server:** 30 | - php version: [e.g. 8.1] 31 | - database: [e.g mysql 8.0] 32 | - wallet version [e.g. 8.2] 33 | - cache lock: [e.g. redis] 34 | - cache wallets: [e.g. redis] 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: rez1dent3 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: npm 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 10 13 | - package-ecosystem: github-actions 14 | directory: "/" 15 | schedule: 16 | interval: daily 17 | open-pull-requests-limit: 10 18 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: update changelog 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | permissions: {} 8 | 9 | jobs: 10 | changelog: 11 | permissions: 12 | contents: write 13 | secrets: inherit 14 | uses: bavix/.github/.github/workflows/changelog.yml@0.3.3 15 | -------------------------------------------------------------------------------- /.github/workflows/deptrac.yaml: -------------------------------------------------------------------------------- 1 | name: deptrac 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | deptrac: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: 8.2 21 | extensions: bcmath 22 | env: 23 | runner: self-hosted 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate --strict 27 | 28 | - name: Cache Composer packages 29 | id: composer-cache 30 | uses: actions/cache@v4 31 | with: 32 | path: vendor 33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-php- 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress 39 | 40 | - name: Install deptrac 41 | run: composer req --dev qossmic/deptrac-shim 42 | if: hashFiles('vendor/bin/deptrac') == '' 43 | 44 | - name: Run deptrac 45 | run: php vendor/bin/deptrac analyse 46 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: "Lock Threads" 2 | 3 | on: 4 | schedule: 5 | - cron: "0 1 * * *" 6 | 7 | permissions: 8 | issues: write 9 | 10 | jobs: 11 | action: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: dessant/lock-threads@v5 15 | with: 16 | github-token: ${{ secrets.GITHUB_TOKEN }} 17 | issue-inactive-days: "45" 18 | process-only: "issues" 19 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yaml: -------------------------------------------------------------------------------- 1 | name: phpstan 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | phpstan: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: 8.2 21 | extensions: bcmath 22 | env: 23 | runner: self-hosted 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate --strict 27 | 28 | - name: Cache Composer packages 29 | id: composer-cache 30 | uses: actions/cache@v4 31 | with: 32 | path: vendor 33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-php- 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress 39 | 40 | - name: Run phpstan 41 | run: composer phpstan 42 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: semgrep 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | semgrep: 11 | uses: bavix/.github/.github/workflows/php-semgrep.yml@0.3.3 12 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues 2 | on: 3 | schedule: 4 | - cron: "0 */8 * * *" 5 | jobs: 6 | stale: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/stale@v9 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | stale-issue-message: 'This issue is stale because it has been open 7 days with no activity.' 13 | days-before-stale: 7 14 | days-before-close: 3 15 | exempt-issue-labels: 'bug,backlog,in-developing' 16 | exempt-pr-labels: 'bug,frozen,in-developing' 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | /vendor/ 3 | composer.lock 4 | .idea/ 5 | build/ 6 | .php_cs_cache 7 | node_modules/ 8 | .deptrac.cache 9 | .phpunit.cache/ 10 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@', 6 | ])); 7 | } 8 | -------------------------------------------------------------------------------- /.whitesource: -------------------------------------------------------------------------------- 1 | { 2 | "scanSettings": { 3 | "baseBranches": [] 4 | }, 5 | "checkRunSettings": { 6 | "vulnerableCheckRunConclusionLevel": "failure", 7 | "displayMode": "diff", 8 | "useMendCheckNames": true 9 | }, 10 | "issueSettings": { 11 | "minSeverityLevel": "LOW", 12 | "issueType": "DEPENDENCY" 13 | } 14 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 bavix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /database/2018_11_06_222923_create_transactions_table.php: -------------------------------------------------------------------------------- 1 | table(), static function (Blueprint $table) { 15 | $table->bigIncrements('id'); 16 | $table->morphs('payable'); 17 | $table->unsignedBigInteger('wallet_id'); 18 | $table->enum('type', ['deposit', 'withdraw'])->index(); 19 | $table->decimal('amount', 64, 0); 20 | $table->boolean('confirmed'); 21 | $table->json('meta') 22 | ->nullable(); 23 | $table->uuid('uuid') 24 | ->unique(); 25 | $table->timestamps(); 26 | 27 | $table->index(['payable_type', 'payable_id'], 'payable_type_payable_id_ind'); 28 | $table->index(['payable_type', 'payable_id', 'type'], 'payable_type_ind'); 29 | $table->index(['payable_type', 'payable_id', 'confirmed'], 'payable_confirmed_ind'); 30 | $table->index(['payable_type', 'payable_id', 'type', 'confirmed'], 'payable_type_confirmed_ind'); 31 | }); 32 | } 33 | 34 | public function down(): void 35 | { 36 | Schema::dropIfExists($this->table()); 37 | } 38 | 39 | private function table(): string 40 | { 41 | return (new Transaction())->getTable(); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /database/2021_11_02_202021_update_wallets_uuid_table.php: -------------------------------------------------------------------------------- 1 | table(), 'uuid')) { 17 | return; 18 | } 19 | 20 | // upgrade from 6.x 21 | Schema::table($this->table(), static function (Blueprint $table) { 22 | $table->uuid('uuid') 23 | ->after('slug') 24 | ->nullable() 25 | ->unique(); 26 | }); 27 | 28 | Wallet::query()->chunk(10000, static function (Collection $wallets) { 29 | $wallets->each(function (Wallet $wallet) { 30 | $wallet->uuid = app(IdentifierFactoryServiceInterface::class)->generate(); 31 | $wallet->save(); 32 | }); 33 | }); 34 | 35 | Schema::table($this->table(), static function (Blueprint $table) { 36 | $table->uuid('uuid') 37 | ->change(); 38 | }); 39 | } 40 | 41 | public function down(): void 42 | { 43 | Schema::table($this->table(), function (Blueprint $table) { 44 | if (Schema::hasColumn($this->table(), 'uuid')) { 45 | $table->dropIndex('wallets_uuid_unique'); 46 | $table->dropColumn('uuid'); 47 | } 48 | }); 49 | } 50 | 51 | private function table(): string 52 | { 53 | return (new Wallet())->getTable(); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /database/2023_12_30_113122_extra_columns_removed.php: -------------------------------------------------------------------------------- 1 | table(), function (Blueprint $table) { 15 | $table->dropIndex(['from_type', 'from_id']); 16 | $table->dropIndex(['to_type', 'to_id']); 17 | 18 | $table->index('from_id'); 19 | $table->index('to_id'); 20 | }); 21 | 22 | Schema::dropColumns($this->table(), ['from_type', 'to_type']); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Schema::table($this->table(), static function (Blueprint $table) { 28 | $table->string('from_type') 29 | ->after('from_id'); 30 | $table->string('to_type') 31 | ->after('to_id'); 32 | }); 33 | } 34 | 35 | private function table(): string 36 | { 37 | return (new Transfer())->getTable(); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /database/2023_12_30_204610_soft_delete.php: -------------------------------------------------------------------------------- 1 | getTable(), static function (Blueprint $table) { 17 | $table->softDeletesTz(); 18 | }); 19 | Schema::table((new Transfer())->getTable(), static function (Blueprint $table) { 20 | $table->softDeletesTz(); 21 | }); 22 | Schema::table((new Transaction())->getTable(), static function (Blueprint $table) { 23 | $table->softDeletesTz(); 24 | }); 25 | } 26 | 27 | public function down(): void 28 | { 29 | Schema::table((new Wallet())->getTable(), static function (Blueprint $table) { 30 | $table->dropSoftDeletes(); 31 | }); 32 | Schema::table((new Transfer())->getTable(), static function (Blueprint $table) { 33 | $table->dropSoftDeletes(); 34 | }); 35 | Schema::table((new Transaction())->getTable(), static function (Blueprint $table) { 36 | $table->dropSoftDeletes(); 37 | }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /database/2024_01_24_185401_add_extra_column_in_transfer.php: -------------------------------------------------------------------------------- 1 | table(), static function (Blueprint $table) { 15 | $table->json('extra') 16 | ->nullable() 17 | ->after('fee'); 18 | }); 19 | } 20 | 21 | public function down(): void 22 | { 23 | Schema::dropColumns($this->table(), ['extra']); 24 | } 25 | 26 | private function table(): string 27 | { 28 | return (new Transfer())->getTable(); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /docs/.vitepress/.gitignore: -------------------------------------------------------------------------------- 1 | cache/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-home-hero-image-background-image: linear-gradient(-44deg, #ec9333 50%, #f1f09c 50%); 3 | --vp-home-hero-image-filter: blur(46px); 4 | } 5 | 6 | @media (min-width: 640px) { 7 | :root { 8 | --vp-home-hero-image-filter: blur(50px); 9 | } 10 | } 11 | 12 | @media (min-width: 960px) { 13 | :root { 14 | --vp-home-hero-image-filter: blur(75px); 15 | } 16 | } -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.js: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme' 2 | import './custom.css' 3 | 4 | export default DefaultTheme 5 | -------------------------------------------------------------------------------- /docs/_include/composer.md: -------------------------------------------------------------------------------- 1 | ## Composer 2 | 3 | The recommended installation method is using [Composer](https://getcomposer.org/). 4 | 5 | In your project root just run: 6 | 7 | ```bash 8 | composer req bavix/laravel-wallet 9 | ``` 10 | 11 | Ensure that you’ve set up your project to [autoload Composer-installed packages](https://getcomposer.org/doc/01-basic-usage.md#autoloading). 12 | -------------------------------------------------------------------------------- /docs/_include/eager_loading.md: -------------------------------------------------------------------------------- 1 | ## Eager Loading 2 | 3 | When accessing Eloquent relationships as properties, the relationship data is "lazy loaded". 4 | This means the relationship data is not actually loaded until you first access the property. However, Eloquent can "eager load" relationships at the time you query the parent model. Eager loading alleviates the N + 1 query problem. To illustrate the N + 1 query problem, consider a `Wallet` model that is related to `User`: 5 | 6 | Add the `HasWallet` trait and `Wallet` interface to model. 7 | ```php 8 | use Bavix\Wallet\Traits\HasWallet; 9 | use Bavix\Wallet\Interfaces\Wallet; 10 | 11 | class User extends Model implements Wallet 12 | { 13 | use HasWallet; // public function wallet(): MorphOne... 14 | } 15 | ``` 16 | 17 | Now, let's retrieve all wallets and their users: 18 | 19 | ```php 20 | $users = User::all(); 21 | 22 | foreach ($users as $user) { 23 | // echo $user->wallet->balance; 24 | echo $user->balance; // Abbreviated notation 25 | } 26 | ``` 27 | 28 | This loop will execute 1 query to retrieve all of the users on the table, then another query for each user to retrieve the wallet. So, if we have 25 users, the code above would run 26 queries: 1 for the original user, and 25 additional queries to retrieve the wallet of each user. 29 | 30 | Thankfully, we can use eager loading to reduce this operation to just 2 queries. When querying, you may specify which relationships should be eager loaded using the with method: 31 | 32 | ```php 33 | $users = User::with('wallet')->all(); 34 | 35 | foreach ($users as $user) { 36 | // echo $user->wallet->balance; 37 | echo $user->balance; // Abbreviated notation 38 | } 39 | ``` 40 | 41 | For this operation, only two queries will be executed. 42 | -------------------------------------------------------------------------------- /docs/_include/models/user_simple.md: -------------------------------------------------------------------------------- 1 | It is necessary to expand the model that will have the wallet. 2 | This is done in two stages: 3 | - Add `Wallet` interface; 4 | - Add the `HasWallet` trait; 5 | 6 | Let's get started. 7 | ```php 8 | use Bavix\Wallet\Traits\HasWallet; 9 | use Bavix\Wallet\Interfaces\Wallet; 10 | 11 | class User extends Model implements Wallet 12 | { 13 | use HasWallet; 14 | } 15 | ``` 16 | 17 | The model is prepared to work with a wallet. 18 | -------------------------------------------------------------------------------- /docs/_include/models/user_simple_float.md: -------------------------------------------------------------------------------- 1 | It is necessary to expand the model that will have the wallet. 2 | This is done in two stages: 3 | - Add `Wallet` interface; 4 | - Add the `HasWalletFloat` trait; 5 | 6 | Let's get started. 7 | ```php 8 | use Bavix\Wallet\Traits\HasWalletFloat; 9 | use Bavix\Wallet\Interfaces\Wallet; 10 | 11 | class User extends Model implements Wallet 12 | { 13 | use HasWalletFloat; 14 | } 15 | ``` 16 | 17 | The model is prepared to work with a wallet. 18 | -------------------------------------------------------------------------------- /docs/guide/additions/swap.md: -------------------------------------------------------------------------------- 1 | # Laravel Wallet Swap 2 | 3 | ## Composer 4 | 5 | The recommended installation method is using [Composer](https://getcomposer.org/). 6 | 7 | In your project root just run: 8 | 9 | ```bash 10 | composer req bavix/laravel-wallet-swap 11 | ``` 12 | 13 | ## User model 14 | We need a simple model with the ability to work multi-wallets. 15 | 16 | ```php 17 | use Bavix\Wallet\Interfaces\Wallet; 18 | use Bavix\Wallet\Traits\HasWallets; 19 | use Bavix\Wallet\Traits\HasWallet; 20 | 21 | class User extends Model implements Wallet 22 | { 23 | use HasWallet, HasWallets; 24 | } 25 | ``` 26 | 27 | ## Simple example 28 | Let's create wallets with currency: 29 | ```php 30 | $usd = $user->createWallet([ 31 | 'name' => 'My Dollars', 32 | 'slug' => 'usd', 33 | 'meta' => ['currency' => 'USD'], 34 | ]); 35 | 36 | $rub = $user->createWallet([ 37 | 'name' => 'My Ruble', 38 | 'slug' => 'rub', 39 | 'meta' => ['currency' => 'RUB'], 40 | ]); 41 | ``` 42 | 43 | Find wallets and exchange from one to another. 44 | 45 | ```php 46 | $rub = $user->getWallet('rub'); 47 | $usd = $user->getWallet('usd'); 48 | 49 | $usd->balance; // 200 50 | $rub->balance; // 0 51 | 52 | $usd->exchange($rub, 10); 53 | $usd->balance; // 190 54 | $rub->balance; // 622 55 | ``` 56 | 57 | It's simple! 58 | -------------------------------------------------------------------------------- /docs/guide/additions/uuid.md: -------------------------------------------------------------------------------- 1 | # Laravel Wallet UUID 2 | 3 | > Using uuid greatly reduces package performance. We recommend using int. 4 | 5 | Often there is a need to store an identifier in uuid/ulid. Since version 9.0 laravel-wallet supports string identifiers, you only need to perform the migration. 6 | 7 | To simplify the process, you can use a ready-made package. 8 | 9 | > Attention! It will not work to use UUID instead of ID in wallet models; there is a special uuid field for this. 10 | 11 | ## Composer 12 | 13 | The recommended installation method is using [Composer](https://getcomposer.org/). 14 | 15 | In your project root just run: 16 | 17 | ```bash 18 | composer req bavix/laravel-wallet-uuid 19 | ``` 20 | 21 | Now you need to migrate! 22 | 23 | After migration, you can use the UUID in your models. 24 | 25 | You can find implementation examples in the package tests: https://github.com/bavix/laravel-wallet-uuid/tree/master/tests 26 | 27 | It's simple! 28 | -------------------------------------------------------------------------------- /docs/guide/db/race-condition.md: -------------------------------------------------------------------------------- 1 | # Race Condition 2 | 3 | A common issue in the issue is about race conditions. 4 | 5 | If you have not yet imported the config into the project, then you need to do this. 6 | ```bash 7 | php artisan vendor:publish --tag=laravel-wallet-config 8 | ``` 9 | 10 | Previously, there was a vacuum package, but now it is a part of the core. You just need to configure the lock service and the cache service in the package configuration `wallet.php`. 11 | 12 | ```php 13 | /** 14 | * A system for dealing with race conditions. 15 | */ 16 | 'lock' => [ 17 | 'driver' => 'array', 18 | 'seconds' => 1, 19 | ], 20 | ``` 21 | 22 | To enable the fight against race conditions, you need to select a provider that supports work with locks. I recommend `redis`. 23 | 24 | There is a setting for storing the state of the wallet, I recommend choosing `redis` here too. 25 | 26 | ```php 27 | /** 28 | * Storage of the state of the balance of wallets. 29 | */ 30 | 'cache' => ['driver' => 'array'], 31 | ``` 32 | 33 | You need `redis-server` and `php-redis`. 34 | 35 | Redis is recommended but not required. You can choose whatever the [framework](https://laravel.com/docs/8.x/cache#introduction) offers you. 36 | 37 | It's simple! 38 | -------------------------------------------------------------------------------- /docs/guide/db/transaction.md: -------------------------------------------------------------------------------- 1 | # Transaction 2 | 3 | You definitely need to know the feature of transactions. The wallet is automatically blocked from the moment it is used until the end of the transaction. Therefore, it is necessary to use the wallet closer to the end of the transaction. 4 | 5 | Very important! Almost all wallet transactions are blocking. 6 | 7 | ```php 8 | use Illuminate\Support\Facades\DB; 9 | 10 | DB::beginTransaction(); 11 | $wallet->balanceInt; // now the wallet is blocked 12 | doingMagic(); // running for a long time. 13 | DB::commit(); // here will unlock the wallet 14 | ``` 15 | 16 | The point is that you need to minimize operations within transactions as much as possible. The longer the transaction, the longer the wallet lock. 17 | The maximum wallet blocking time is set in the configuration. The longer the transaction takes, the more likely it is to get a race for the wallet. 18 | 19 | --- 20 | It's simple! 21 | -------------------------------------------------------------------------------- /docs/guide/events/balance-updated-event.md: -------------------------------------------------------------------------------- 1 | # Tracking balance changes 2 | 3 | There are tasks when you urgently need to do something when the user's balance changes. A frequent case of transferring data via websockets to the front-end. 4 | 5 | Version 7.2 introduces an interface to which you can subscribe. 6 | This is done using standard Laravel methods. 7 | More information in the [documentation](https://laravel.com/docs/8.x/events). 8 | 9 | ```php 10 | use Bavix\Wallet\Internal\Events\BalanceUpdatedEventInterface; 11 | 12 | protected $listen = [ 13 | BalanceUpdatedEventInterface::class => [ 14 | MyBalanceUpdatedListener::class, 15 | ], 16 | ]; 17 | ``` 18 | 19 | And then we create a listener. 20 | 21 | ```php 22 | use Bavix\Wallet\Internal\Events\BalanceUpdatedEventInterface; 23 | 24 | class MyBalanceUpdatedListener 25 | { 26 | public function handle(BalanceUpdatedEventInterface $event): void 27 | { 28 | // And then the implementation... 29 | } 30 | } 31 | ``` 32 | 33 | It's simple! 34 | -------------------------------------------------------------------------------- /docs/guide/events/transaction-created-event.md: -------------------------------------------------------------------------------- 1 | # Tracking the creation of wallet transactions 2 | 3 | The events are similar to the events for updating the balance, only for the creation of a wallet. A frequent case of transferring data via websockets to the front-end. 4 | 5 | Version 9.1 introduces an interface to which you can subscribe. 6 | This is done using standard Laravel methods. 7 | More information in the [documentation](https://laravel.com/docs/8.x/events). 8 | 9 | ```php 10 | use Bavix\Wallet\Internal\Events\TransactionCreatedEventInterface; 11 | 12 | protected $listen = [ 13 | TransactionCreatedEventInterface::class => [ 14 | MyWalletTransactionCreatedListener::class, 15 | ], 16 | ]; 17 | ``` 18 | 19 | And then we create a listener. 20 | 21 | ```php 22 | use Bavix\Wallet\Internal\Events\TransactionCreatedEventInterface; 23 | 24 | class MyWalletTransactionCreatedListener 25 | { 26 | public function handle(TransactionCreatedEventInterface $event): void 27 | { 28 | // And then the implementation... 29 | } 30 | } 31 | ``` 32 | 33 | It's simple! 34 | -------------------------------------------------------------------------------- /docs/guide/events/wallet-created-event.md: -------------------------------------------------------------------------------- 1 | # Tracking the creation of wallets 2 | 3 | The events are similar to the events for updating the balance, only for the creation of a wallet. A frequent case of transferring data via websockets to the front-end. 4 | 5 | Version 7.3 introduces an interface to which you can subscribe. 6 | This is done using standard Laravel methods. 7 | More information in the [documentation](https://laravel.com/docs/8.x/events). 8 | 9 | ```php 10 | use Bavix\Wallet\Internal\Events\WalletCreatedEventInterface; 11 | 12 | protected $listen = [ 13 | WalletCreatedEventInterface::class => [ 14 | MyWalletCreatedListener::class, 15 | ], 16 | ]; 17 | ``` 18 | 19 | And then we create a listener. 20 | 21 | ```php 22 | use Bavix\Wallet\Internal\Events\WalletCreatedEventInterface; 23 | 24 | class MyWalletCreatedListener 25 | { 26 | public function handle(WalletCreatedEventInterface $event): void 27 | { 28 | // And then the implementation... 29 | } 30 | } 31 | ``` 32 | 33 | It's simple! 34 | -------------------------------------------------------------------------------- /docs/guide/fractional/deposit.md: -------------------------------------------------------------------------------- 1 | # Deposit float 2 | 3 | A deposit is a sum of money which is part of the full price of something, 4 | and which you pay when you agree to buy it. 5 | 6 | In this case, the Deposit is the replenishment of the wallet. 7 | 8 | ## User Model 9 | 10 | 11 | 12 | ## Make a Deposit 13 | 14 | Find user: 15 | 16 | ```php 17 | $user = User::first(); 18 | ``` 19 | 20 | As the user uses `HasWalletFloat`, he will have `balance` property. 21 | Check the user's balance. 22 | 23 | ```php 24 | $user->balance; // 0 25 | $user->balanceInt; // 0 26 | $user->balanceFloatNum; // 0 27 | ``` 28 | 29 | The balance is zero, which is what we expected. 30 | 31 | ```php 32 | $user->depositFloat(10.1); 33 | $user->balance; // 1010 34 | $user->balanceInt; // 1010 35 | $user->balanceFloatNum; // 10.1 36 | ``` 37 | 38 | Wow! 39 | -------------------------------------------------------------------------------- /docs/guide/fractional/withdraw.md: -------------------------------------------------------------------------------- 1 | # Withdraw 2 | 3 | When there is enough money in the account, you can transfer/withdraw 4 | it or buy something in the system. 5 | 6 | Since the currency is virtual, you can buy any services on your website. 7 | For example, priority in search results. 8 | 9 | ## User Model 10 | 11 | 12 | 13 | ## Make a Withdraw 14 | 15 | Find user: 16 | 17 | ```php 18 | $user = User::first(); 19 | ``` 20 | 21 | As the user uses `HasWalletFloat`, he will have `balance` property. 22 | Check the user's balance. 23 | 24 | ```php 25 | $user->balance; // 10000 26 | $user->balanceInt; // 10000 27 | $user->balanceFloatNum; // 100.00 28 | ``` 29 | 30 | The balance is not empty, so you can withdraw funds. 31 | 32 | ```php 33 | $user->withdrawFloat(10); 34 | $user->balance; // 9000 35 | $user->balanceInt; // 9000 36 | $user->balanceFloatNum; // 90.00 37 | ``` 38 | 39 | It's simple! 40 | 41 | ## Force Withdraw 42 | 43 | Forced withdrawal is necessary for those cases when 44 | the user has no funds. For example, a fine for spam. 45 | 46 | ```php 47 | $user->balanceFloatNum; // 90.00 48 | $user->forceWithdrawFloat(101); 49 | $user->balanceFloatNum; // -11.00 50 | ``` 51 | 52 | ## And what will happen if the money is not enough? 53 | 54 | There can be two situations: 55 | 56 | - The user's balance is zero, then we get an error 57 | `Bavix\Wallet\Exceptions\BalanceIsEmpty` 58 | - If the balance is greater than zero, but it is not enough 59 | `Bavix\Wallet\Exceptions\InsufficientFunds` 60 | -------------------------------------------------------------------------------- /docs/guide/helpers/formatter.md: -------------------------------------------------------------------------------- 1 | # Formatter 2 | 3 | Sometimes you need to convert the balance to some format. A small and simple helper has appeared that will simplify the process a little. 4 | 5 | ## To fractional numbers 6 | 7 | ```php 8 | app(FormatterServiceInterface::class)->floatValue('12345', 2); // 123.45 9 | app(FormatterServiceInterface::class)->floatValue('12345', 3); // 12.345 10 | ``` 11 | 12 | ## To whole numbers 13 | 14 | ```php 15 | app(FormatterServiceInterface::class)->intValue('12.345', 3); // 12345 16 | app(FormatterServiceInterface::class)->intValue('123.45', 2); // 12345 17 | ``` 18 | 19 | --- 20 | It's simple! 21 | -------------------------------------------------------------------------------- /docs/guide/introduction/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | --- 4 | 5 | ![Laravel Wallet](https://user-images.githubusercontent.com/5111255/48687709-a7c2fa00-ebd3-11e8-8714-c4f3efe93f02.png) 6 | 7 | `laravel-wallet` - It's easy to work with a virtual wallet. 8 | 9 | ### Support 10 | 11 | Please ask questions on the [Github issues page](https://github.com/bavix/laravel-wallet/issues). 12 | -------------------------------------------------------------------------------- /docs/guide/introduction/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 4 | 5 | ## Customize 6 | 7 | Sometimes it is useful... 8 | 9 | ### Run Migrations 10 | Publish the migrations with this artisan command: 11 | ```bash 12 | php artisan vendor:publish --tag=laravel-wallet-migrations 13 | ``` 14 | 15 | ### Configuration 16 | You can publish the config file with this artisan command: 17 | ```bash 18 | php artisan vendor:publish --tag=laravel-wallet-config 19 | ``` 20 | 21 | After installing the package, you can proceed to [use it](basic-usage) or [configure](configuration) it to suit your needs. 22 | -------------------------------------------------------------------------------- /docs/guide/multi/new-wallet.md: -------------------------------------------------------------------------------- 1 | # New Wallet 2 | 3 | You can create an unlimited number of wallets, but the `slug` for each wallet should be unique. 4 | 5 | ## User Model 6 | 7 | Add the `HasWallets` trait's and `Wallet` interface to model. 8 | 9 | ```php 10 | use Bavix\Wallet\Traits\HasWallets; 11 | use Bavix\Wallet\Interfaces\Wallet; 12 | 13 | class User extends Model implements Wallet 14 | { 15 | use HasWallets; 16 | } 17 | ``` 18 | 19 | ## Create a wallet 20 | 21 | Find user: 22 | 23 | ```php 24 | $user = User::first(); 25 | ``` 26 | 27 | Create a new wallet. 28 | 29 | ```php 30 | $user->hasWallet('my-wallet'); // bool(false) 31 | $wallet = $user->createWallet([ 32 | 'name' => 'New Wallet', 33 | 'slug' => 'my-wallet', 34 | ]); 35 | 36 | $user->hasWallet('my-wallet'); // bool(true) 37 | 38 | $wallet->deposit(100); 39 | $wallet->balance; // 100 40 | $wallet->balanceFloatNum; // 1.00 41 | ``` 42 | 43 | ## How to get the right wallet? 44 | 45 | ```php 46 | $myWallet = $user->getWallet('my-wallet'); 47 | $myWallet->balance; // 100 48 | $myWallet->balanceFloatNum; // 1.00 49 | ``` 50 | 51 | ## Default Wallet + MultiWallet 52 | 53 | Is it possible to use the default wallet and multi-wallets at the same time? Yes. 54 | 55 | ```php 56 | use Bavix\Wallet\Traits\HasWallet; 57 | use Bavix\Wallet\Traits\HasWallets; 58 | use Bavix\Wallet\Interfaces\Wallet; 59 | 60 | class User extends Model implements Wallet 61 | { 62 | use HasWallet, HasWallets; 63 | } 64 | ``` 65 | 66 | How to get the default wallet? 67 | 68 | ```php 69 | $wallet = $user->wallet; 70 | $wallet->balance; // 10 71 | $wallet->balanceFloatNum; // 0.10 72 | ``` 73 | 74 | It's simple! 75 | -------------------------------------------------------------------------------- /docs/guide/multi/transaction-filter.md: -------------------------------------------------------------------------------- 1 | # Transaction Filter 2 | 3 | Often developers ask me about the `transactions` method. 4 | Yes, this method displays ALL transactions for the wallet owner. 5 | If you only need to filter one wallet at a time, now you can use the `walletTransactions` method. 6 | 7 | ```php 8 | /** @var \Bavix\Wallet\Models\Wallet $wallet */ 9 | 10 | $query = $wallet->walletTransactions(); 11 | ``` 12 | 13 | Let's take a look at a livelier code example: 14 | ```php 15 | $user->transactions()->count(); // 0 16 | 17 | // Multi wallets and default wallet can be used together 18 | // default wallet 19 | $user->deposit(100); 20 | $user->wallet->deposit(200); 21 | $user->wallet->withdraw(1); 22 | 23 | // usd 24 | $usd = $user->createWallet(['name' => 'USD']); 25 | $usd->deposit(100); 26 | 27 | // eur 28 | $eur = $user->createWallet(['name' => 'EUR']); 29 | $eur->deposit(100); 30 | 31 | $user->transactions()->count(); // 5 32 | $user->wallet->transactions()->count(); // 5 33 | $usd->transactions()->count(); // 5 34 | $eur->transactions()->count(); // 5 35 | // the transactions method returns data relative to the owner of the wallet, for all transactions 36 | 37 | $user->walletTransactions()->count(); // 3. we get the default wallet 38 | $user->wallet->walletTransactions()->count(); // 3 39 | $usd->walletTransactions()->count(); // 1 40 | $eur->walletTransactions()->count(); // 1 41 | ``` 42 | 43 | It's simple! 44 | -------------------------------------------------------------------------------- /docs/guide/single/cancel.md: -------------------------------------------------------------------------------- 1 | # Cancel Transaction 2 | 3 | Sometimes you need to cancel a confirmed transaction. For example, money was received or debited by mistake. You can reset the confirmation of a specific transaction. 4 | 5 | ## User Model 6 | 7 | Add the `CanConfirm` trait and `Confirmable` interface to your User model. 8 | 9 | ```php 10 | use Bavix\Wallet\Interfaces\Confirmable; 11 | use Bavix\Wallet\Interfaces\Wallet; 12 | use Bavix\Wallet\Traits\CanConfirm; 13 | use Bavix\Wallet\Traits\HasWallet; 14 | 15 | class UserConfirm extends Model implements Wallet, Confirmable 16 | { 17 | use HasWallet, CanConfirm; 18 | } 19 | ``` 20 | 21 | > You can only cancel the transaction with the wallet you paid with. 22 | 23 | ## To cancel 24 | 25 | ### Example: 26 | 27 | Created a transaction, and after resetting its confirmation. 28 | 29 | ```php 30 | $user->balance; // 0 31 | $transaction = $user->deposit(100); // confirmed transaction 32 | $transaction->confirmed; // bool(true) 33 | $user->balance; // 100 34 | 35 | $user->resetConfirm($transaction); // bool(true) 36 | $transaction->confirmed; // bool(false) 37 | 38 | $user->balance; // 0 39 | ``` 40 | 41 | It's simple! 42 | -------------------------------------------------------------------------------- /docs/guide/single/confirm.md: -------------------------------------------------------------------------------- 1 | # Confirm Transaction 2 | 3 | There are situations when it is necessary to create a transaction without crediting to the wallet or debiting. Laravel-wallet has such a mode of unconfirmed transactions. 4 | 5 | You create a transaction without confirmation, and a little later you confirm it. 6 | 7 | ## User Model 8 | 9 | Add the `CanConfirm` trait and `Confirmable` interface to your User model. 10 | 11 | ```php 12 | use Bavix\Wallet\Interfaces\Confirmable; 13 | use Bavix\Wallet\Interfaces\Wallet; 14 | use Bavix\Wallet\Traits\CanConfirm; 15 | use Bavix\Wallet\Traits\HasWallet; 16 | 17 | class UserConfirm extends Model implements Wallet, Confirmable 18 | { 19 | use HasWallet, CanConfirm; 20 | } 21 | ``` 22 | 23 | > You can only confirm the transaction with the wallet you paid with. 24 | 25 | ## To confirmation 26 | 27 | ### Example: 28 | 29 | Sometimes you need to create an operation and confirm its field. 30 | That is what this trey does. 31 | 32 | ```php 33 | $user->balance; // 0 34 | $transaction = $user->deposit(100, null, false); // not confirm 35 | $transaction->confirmed; // bool(false) 36 | $user->balance; // 0 37 | 38 | $user->confirm($transaction); // bool(true) 39 | $transaction->confirmed; // bool(true) 40 | 41 | $user->balance; // 100 42 | ``` 43 | 44 | It's simple! 45 | -------------------------------------------------------------------------------- /docs/guide/single/credit-limits.md: -------------------------------------------------------------------------------- 1 | # Credit Limits 2 | 3 | If you need the ability to have wallets have a credit limit, then this functionality is for you. 4 | 5 | The functionality does nothing, it only allows you not to use "force" for most of the operations within the credit limit. You should write the logic for collecting interest, notifications on debts, etc. 6 | 7 | By default, the credit limit is zero. 8 | 9 | An example of working with a credit limit: 10 | ```php 11 | /** 12 | * @var \Bavix\Wallet\Interfaces\Customer $customer 13 | * @var \Bavix\Wallet\Models\Wallet $wallet 14 | * @var \Bavix\Wallet\Interfaces\ProductInterface $product 15 | */ 16 | $wallet = $customer->wallet; // get default wallet 17 | $wallet->meta['credit'] = 10000; // credit limit 18 | $wallet->save(); // update credit limit 19 | 20 | $wallet->balanceInt; // 0 21 | $product->getAmountProduct($customer); // 500 22 | 23 | $wallet->pay($product); // success 24 | $wallet->balanceInt; // -500 25 | ``` 26 | 27 | For multi-wallets when creating: 28 | ```php 29 | /** @var \Bavix\Wallet\Traits\HasWallets $user */ 30 | $wallet = $user->createWallet([ 31 | 'name' => 'My Wallet', 32 | 'meta' => ['credit' => 500], 33 | ]); 34 | ``` 35 | 36 | It's simple! 37 | -------------------------------------------------------------------------------- /docs/guide/single/deposit.md: -------------------------------------------------------------------------------- 1 | # Deposit 2 | 3 | A deposit is a sum of money which is part of the full price of something, 4 | and which you pay when you agree to buy it. 5 | 6 | In this case, the Deposit is the replenishment of the wallet. 7 | 8 | ## User Model 9 | 10 | 11 | 12 | ## Make a Deposit 13 | 14 | Find user: 15 | 16 | ```php 17 | $user = User::first(); 18 | ``` 19 | 20 | As the user uses `HasWallet`, he will have `balance` property. 21 | Check the user's balance. 22 | 23 | ```php 24 | $user->balance; // 0 25 | $user->balanceInt; // 0 26 | ``` 27 | 28 | The balance is zero, which is what we expected. 29 | Put it on his 10 cents account. 30 | 31 | ```php 32 | $user->deposit(10); 33 | $user->balance; // 10 34 | $user->balanceInt; // 10 35 | ``` 36 | 37 | Wow! The balance is 10 cents, the money is credited. 38 | -------------------------------------------------------------------------------- /docs/guide/single/refresh.md: -------------------------------------------------------------------------------- 1 | # To refresh the balance 2 | 3 | There are situations when you create a lot of unconfirmed operations, 4 | and then abruptly confirm everything. 5 | In this case, the user's balance will not change. 6 | You must be forced to refresh the balance. 7 | 8 | ## User Model 9 | 10 | 11 | 12 | ## Get the current balance for your wallet 13 | 14 | Let's say the user's balance 15 | 16 | ```php 17 | $user->id; // 5 18 | $user->balance; // 27 19 | ``` 20 | 21 | And he has unconfirmed transactions. 22 | Confirm all transactions. 23 | 24 | ```sql 25 | update transactions 26 | set confirmed=1 27 | where confirmed=0 and 28 | payable_type='App\Models\User' and 29 | payable_id=5; 30 | -- 212 rows affected in 54 ms 31 | ``` 32 | 33 | Refresh the balance. 34 | 35 | ```php 36 | $user->balance; // 27 37 | $user->wallet->refreshBalance(); 38 | $user->balance; // 42 39 | ``` 40 | 41 | It's simple! 42 | -------------------------------------------------------------------------------- /docs/guide/single/transfer.md: -------------------------------------------------------------------------------- 1 | # Transfer 2 | 3 | Transfer in our system are two well-known [Deposit](deposit.md) and [Withdraw](withdraw.md) 4 | operations that are performed in one transaction. 5 | 6 | The transfer takes place between wallets. 7 | 8 | ## User Model 9 | 10 | 11 | 12 | ### Example contract 13 | 14 | ```php 15 | $transfer = $user1->transfer( 16 | $user2, 17 | 511, 18 | new Extra( 19 | deposit: [ 20 | 'type' => 'extra-deposit', 21 | ], 22 | withdraw: new Option( 23 | [ 24 | 'type' => 'extra-withdraw', 25 | ], 26 | false // confirmed 27 | ), 28 | extra: [ 29 | 'msg' => 'hello world', 30 | ], 31 | ) 32 | ); 33 | ``` 34 | 35 | ## Make a Transfer 36 | 37 | Find user: 38 | 39 | ```php 40 | $first = User::first(); 41 | $last = User::orderBy('id', 'desc')->first(); // last user 42 | $first->getKey() !== $last->getKey(); // true 43 | ``` 44 | 45 | As the user uses `HasWallet`, he will have `balance` property. 46 | Check the user's balance. 47 | 48 | ```php 49 | $first->balance; // 100 50 | $last->balance; // 0 51 | ``` 52 | 53 | The transfer will be from the first user to the second. 54 | 55 | ```php 56 | $first->transfer($last, 5); 57 | $first->balance; // 95 58 | $last->balance; // 5 59 | ``` 60 | 61 | It's simple! 62 | 63 | ## Force Transfer 64 | 65 | Check the user's balance. 66 | 67 | ```php 68 | $first->balance; // 100 69 | $last->balance; // 0 70 | ``` 71 | 72 | The transfer will be from the first user to the second. 73 | 74 | ```php 75 | $first->forceTransfer($last, 500); 76 | $first->balance; // -400 77 | $last->balance; // 500 78 | ``` 79 | 80 | It's simple! 81 | -------------------------------------------------------------------------------- /docs/guide/single/withdraw.md: -------------------------------------------------------------------------------- 1 | # Withdraw 2 | 3 | When there is enough money in the account, you can transfer/withdraw 4 | it or buy something in the system. 5 | 6 | Since the currency is virtual, you can buy any services on your website. 7 | For example, priority in search results. 8 | 9 | ## User Model 10 | 11 | 12 | 13 | ## Make a Withdraw 14 | 15 | Find user: 16 | 17 | ```php 18 | $user = User::first(); 19 | ``` 20 | 21 | As the user uses `HasWallet`, he will have `balance` property. 22 | Check the user's balance. 23 | 24 | ```php 25 | $user->balance; // 100 26 | $user->balanceInt; // 100 27 | ``` 28 | 29 | The balance is not empty, so you can withdraw funds. 30 | 31 | ```php 32 | $user->withdraw(10); 33 | $user->balance; // 90 34 | $user->balanceInt; // 90 35 | ``` 36 | 37 | It's simple! 38 | 39 | ## Force Withdraw 40 | 41 | Forced withdrawal is necessary for those cases when 42 | the user has no funds. For example, a fine for spam. 43 | 44 | ```php 45 | $user->balance; // 100 46 | $user->balanceInt; // 100 47 | $user->forceWithdraw(101); 48 | $user->balance; // -1 49 | $user->balanceInt; // -1 50 | ``` 51 | 52 | ## And what will happen if the money is not enough? 53 | 54 | There can be two situations: 55 | 56 | - The user's balance is zero, then we get an error 57 | `Bavix\Wallet\Exceptions\BalanceIsEmpty` 58 | - If the balance is greater than zero, but it is not enough 59 | `Bavix\Wallet\Exceptions\InsufficientFunds` 60 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Laravel Wallet" 7 | text: It's simple! 8 | tagline: It's easy to work with a virtual wallet 9 | image: 10 | src: https://github.com/bavix/laravel-wallet/assets/5111255/24cf424a-0177-4712-b74c-52b4ba88c428 11 | alt: Laravel Wallet 12 | actions: 13 | - theme: brand 14 | text: Getting started 15 | link: /guide/introduction/ 16 | - theme: alt 17 | text: Upgrade Guide 18 | link: /guide/introduction/upgrade 19 | 20 | features: 21 | - title: Default Wallet 22 | details: For simple projects when there is no need for multiple wallets. 23 | icon: 💰 24 | link: /guide/single/deposit 25 | - title: Multi wallets 26 | details: Many wallets for one model. Easy API. 27 | icon: 🎒 28 | link: /guide/multi/new-wallet 29 | - title: Purchases 30 | details: E-commerce. Create goods and buy them using wallets. There are also shopping carts, availability, taxes and fees. 31 | icon: 🛍️ 32 | link: /guide/purchases/payment 33 | - title: Exchanges 34 | details: Exchanges between wallets. Convert currency from one wallet to another. 35 | icon: 💱 36 | link: /guide/single/exchange 37 | - title: Support UUID 38 | details: Models with UUID are supported. 39 | icon: ❄️ 40 | link: /guide/additions/uuid 41 | - title: Events 42 | details: For more complex projects there are events and high performance API. 43 | icon: 📻 44 | link: /guide/events/balance-updated-event 45 | --- 46 | 47 | -------------------------------------------------------------------------------- /ecs.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/config', 12 | __DIR__ . '/database', 13 | __DIR__ . '/resources/lang', 14 | __DIR__ . '/src', 15 | __DIR__ . '/tests', 16 | ]); 17 | 18 | $config->skip([ 19 | GeneralPhpdocAnnotationRemoveFixer::class, 20 | \PhpCsFixer\Fixer\Import\FullyQualifiedStrictTypesFixer::class, 21 | ]); 22 | 23 | $config->sets([ 24 | SetList::CLEAN_CODE, 25 | SetList::SYMPLIFY, 26 | SetList::ARRAY, 27 | SetList::COMMON, 28 | SetList::PSR_12, 29 | SetList::CONTROL_STRUCTURES, 30 | SetList::NAMESPACES, 31 | SetList::STRICT, 32 | SetList::PHPUNIT, 33 | SetList::LARAVEL, 34 | ]); 35 | }; 36 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 2, 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "logs": { 9 | "text": "build/infection.log", 10 | "stryker": { 11 | "badge": "master" 12 | } 13 | }, 14 | "mutators": { 15 | "@default": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-wallet", 3 | "version": "2.0.0", 4 | "description": "Package documentation", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/bavix/laravel-wallet.git" 8 | }, 9 | "author": "Maxim Babichev", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/bavix/laravel-wallet/issues" 13 | }, 14 | "homepage": "https://bavix.github.io/laravel-wallet/", 15 | "scripts": { 16 | "docs:dev": "vitepress dev docs", 17 | "docs:build": "vitepress build docs", 18 | "docs:preview": "vitepress preview docs", 19 | "build": "vitepress build docs" 20 | }, 21 | "dependencies": { 22 | "vitepress": "^1.6.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /phpstan.common.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/larastan/larastan/extension.neon 3 | - vendor/ergebnis/phpstan-rules/rules.neon 4 | 5 | parameters: 6 | level: 9 7 | reportUnmatchedIgnoredErrors: false 8 | fileExtensions: 9 | - php 10 | -------------------------------------------------------------------------------- /phpstan.src.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan.common.neon 3 | - phpstan.src.baseline.neon 4 | 5 | parameters: 6 | level: 9 7 | fileExtensions: 8 | - php 9 | ergebnis: 10 | noParameterWithNullableTypeDeclaration: 11 | enabled: false 12 | noNullableReturnTypeDeclaration: 13 | enabled: false 14 | noParameterWithNullDefaultValue: 15 | enabled: false 16 | final: 17 | allowAbstractClasses: true 18 | classesNotRequiredToBeAbstractOrFinal: 19 | - Bavix\Wallet\Models\Wallet 20 | - Bavix\Wallet\Models\Transfer 21 | - Bavix\Wallet\Models\Transaction 22 | noExtends: 23 | classesAllowedToBeExtended: 24 | # laravel 25 | - Illuminate\Support\ServiceProvider 26 | - Illuminate\Database\Eloquent\Model 27 | 28 | # php exceptions 29 | - LogicException 30 | - RuntimeException 31 | - UnderflowException 32 | - UnexpectedValueException 33 | - InvalidArgumentException 34 | noParameterWithContainerTypeDeclaration: 35 | interfacesImplementedByContainers: 36 | - Psr\Container\ContainerInterface 37 | paths: 38 | - src/ -------------------------------------------------------------------------------- /phpstan.tests.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan.common.neon 3 | - phpstan.tests.baseline.neon 4 | 5 | parameters: 6 | level: 9 7 | ignoreErrors: 8 | - '#^Offset \d+ does not exist on.+Collection.+int.+$#' 9 | fileExtensions: 10 | - php 11 | ergebnis: 12 | noParameterWithNullableTypeDeclaration: 13 | enabled: false 14 | noNullableReturnTypeDeclaration: 15 | enabled: false 16 | noParameterWithNullDefaultValue: 17 | enabled: false 18 | final: 19 | allowAbstractClasses: false 20 | classesNotRequiredToBeAbstractOrFinal: 21 | - Bavix\Wallet\Models\Wallet 22 | - Bavix\Wallet\Models\Transfer 23 | - Bavix\Wallet\Models\Transaction 24 | noExtends: 25 | classesAllowedToBeExtended: 26 | # laravel 27 | - Illuminate\Support\ServiceProvider 28 | - Illuminate\Database\Eloquent\Model 29 | - Illuminate\Database\Migrations\Migration 30 | - Illuminate\Database\Eloquent\Factories\Factory 31 | 32 | # php exceptions 33 | - RuntimeException 34 | - InvalidArgumentException 35 | 36 | # phpunit 37 | - Orchestra\Testbench\TestCase 38 | - Bavix\Wallet\Test\Infra\TestCase 39 | 40 | # wallet 41 | - Bavix\Wallet\Models\Wallet 42 | - Bavix\Wallet\Models\Transfer 43 | - Bavix\Wallet\Models\Transaction 44 | 45 | noParameterWithContainerTypeDeclaration: 46 | interfacesImplementedByContainers: 47 | - Psr\Container\ContainerInterface 48 | paths: 49 | - tests/ -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ./src/ 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | parallel(); 14 | $config->paths([ 15 | __DIR__ . '/src', 16 | __DIR__ . '/tests', 17 | ]); 18 | 19 | // remove it in next version 20 | $config->skip([ 21 | MigrateToSimplifiedAttributeRector::class, 22 | MinutesToSecondsInCacheRector::class, 23 | RemoveDeadZeroAndOneOperationRector::class, 24 | ]); 25 | 26 | // Define what rule sets will be applied 27 | $config->import(LaravelLevelSetList::UP_TO_LARAVEL_110); 28 | $config->import(SetList::STRICT_BOOLEANS); 29 | $config->import(SetList::PRIVATIZATION); 30 | $config->import(SetList::EARLY_RETURN); 31 | $config->import(SetList::INSTANCEOF); 32 | $config->import(SetList::CODE_QUALITY); 33 | $config->import(SetList::DEAD_CODE); 34 | $config->import(SetList::PHP_82); 35 | }; 36 | -------------------------------------------------------------------------------- /resources/lang/ar/errors.php: -------------------------------------------------------------------------------- 1 | 'يجب أن يكون السعر موجبًا', 7 | 'product_stock' => 'المنتج غير متوفر', 8 | 'wallet_empty' => 'المحفظة فارغة', 9 | 'insufficient_funds' => 'الأموال غير الكافية', 10 | 'confirmed_invalid' => 'تم التأكيد بالفعل', 11 | 'unconfirmed_invalid' => 'التأكيد تمت إعادة تعيينه بالفعل', 12 | 'owner_invalid' => 'أنت لست مالك المحفظة', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/en/errors.php: -------------------------------------------------------------------------------- 1 | 'The price should be positive', 7 | 'product_stock' => 'The product is out of stock', 8 | 'wallet_empty' => 'Wallet is empty', 9 | 'insufficient_funds' => 'Insufficient funds', 10 | 'confirmed_invalid' => 'The transaction has already been confirmed', 11 | 'unconfirmed_invalid' => 'Confirmation has already been reset', 12 | 'owner_invalid' => 'You are not the owner of the wallet', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/es/errors.php: -------------------------------------------------------------------------------- 1 | 'El precio debe ser un valor positivo', 7 | 'product_stock' => 'El producto no esta disponible en el inventario', 8 | 'wallet_empty' => 'La cartera esta vacía', 9 | 'insufficient_funds' => 'Fondos Insuficientes', 10 | 'confirmed_invalid' => 'La transferencia ya fue confirmada', 11 | 'unconfirmed_invalid' => 'La confirmación ya fue restablecida', 12 | 'owner_invalid' => 'No eres el propietario de esta cartera', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/fa/errors.php: -------------------------------------------------------------------------------- 1 | 'قیمت باید عددی مثبت باشد', 7 | 'product_stock' => 'کالا موجود نیست', 8 | 'wallet_empty' => 'کیف پول خالی است', 9 | 'insufficient_funds' => 'موجودی ناکافی', 10 | 'confirmed_invalid' => 'تراکنش قبلا تایید شده است', 11 | 'unconfirmed_invalid' => 'تاییدیه تراکنش لغو شده است', 12 | 'owner_invalid' => 'این کیف پول متعلق به شما نیست', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/ru/errors.php: -------------------------------------------------------------------------------- 1 | 'Цена должна быть положительной', 7 | 'product_stock' => 'Товар отсутствует', 8 | 'wallet_empty' => 'Кошелёк пуст', 9 | 'insufficient_funds' => 'Недостаточно средств', 10 | 'confirmed_invalid' => 'Перевод уже подтвержден', 11 | 'unconfirmed_invalid' => 'Подтверждение уже сброшено', 12 | 'owner_invalid' => 'Вы не владелец кошелька', 13 | ]; 14 | -------------------------------------------------------------------------------- /resources/lang/zh-CN/errors.php: -------------------------------------------------------------------------------- 1 | '价格不能为负数', 7 | 'product_stock' => '库存不足', 8 | 'wallet_empty' => '钱包为空', 9 | 'insufficient_funds' => '余额不足', 10 | 'confirmed_invalid' => '翻译已经确认', 11 | 'unconfirmed_invalid' => '確認已被重置', 12 | 'owner_invalid' => '你不是钱包的主人', 13 | ]; 14 | -------------------------------------------------------------------------------- /src/Exceptions/AmountInvalid.php: -------------------------------------------------------------------------------- 1 | $objects 21 | * @return non-empty-array 22 | * 23 | * @throws ExceptionInterface 24 | */ 25 | public function apply(array $objects): array; 26 | } 27 | -------------------------------------------------------------------------------- /src/External/Api/TransactionQueryInterface.php: -------------------------------------------------------------------------------- 1 | |null 27 | */ 28 | public function getMeta(): ?array; 29 | 30 | public function isConfirmed(): bool; 31 | 32 | public function getUuid(): ?string; 33 | } 34 | -------------------------------------------------------------------------------- /src/External/Api/TransferFloatQuery.php: -------------------------------------------------------------------------------- 1 | |ExtraDtoInterface|null $meta 18 | */ 19 | public function __construct( 20 | private Wallet $from, 21 | private Wallet $to, 22 | float|int|string $amount, 23 | private array|ExtraDtoInterface|null $meta 24 | ) { 25 | $walletModel = app(CastServiceInterface::class)->getWallet($from); 26 | 27 | $this->amount = app(FormatterServiceInterface::class) 28 | ->intValue($amount, $walletModel->decimal_places); 29 | } 30 | 31 | public function getFrom(): Wallet 32 | { 33 | return $this->from; 34 | } 35 | 36 | public function getTo(): Wallet 37 | { 38 | return $this->to; 39 | } 40 | 41 | public function getAmount(): string 42 | { 43 | return $this->amount; 44 | } 45 | 46 | /** 47 | * @return array|ExtraDtoInterface|null 48 | */ 49 | public function getMeta(): array|ExtraDtoInterface|null 50 | { 51 | return $this->meta; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/External/Api/TransferQuery.php: -------------------------------------------------------------------------------- 1 | |ExtraDtoInterface|null $meta 14 | */ 15 | public function __construct( 16 | private Wallet $from, 17 | private Wallet $to, 18 | private float|int|string $amount, 19 | private array|ExtraDtoInterface|null $meta 20 | ) { 21 | } 22 | 23 | public function getFrom(): Wallet 24 | { 25 | return $this->from; 26 | } 27 | 28 | public function getTo(): Wallet 29 | { 30 | return $this->to; 31 | } 32 | 33 | public function getAmount(): float|int|string 34 | { 35 | return $this->amount; 36 | } 37 | 38 | /** 39 | * @return array|ExtraDtoInterface|null 40 | */ 41 | public function getMeta(): array|ExtraDtoInterface|null 42 | { 43 | return $this->meta; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/External/Api/TransferQueryHandler.php: -------------------------------------------------------------------------------- 1 | assistantService->getWallets( 30 | array_map(static fn (TransferQueryInterface $query): Wallet => $query->getFrom(), $objects), 31 | ); 32 | 33 | $values = array_map( 34 | fn (TransferQueryInterface $query) => $this->prepareService->transferLazy( 35 | $query->getFrom(), 36 | $query->getTo(), 37 | Transfer::STATUS_TRANSFER, 38 | $query->getAmount(), 39 | $query->getMeta(), 40 | ), 41 | $objects 42 | ); 43 | 44 | return $this->atomicService->blocks($wallets, fn () => $this->transferService->apply($values)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/External/Api/TransferQueryHandlerInterface.php: -------------------------------------------------------------------------------- 1 | $objects 21 | * @return non-empty-array 22 | * 23 | * @throws ExceptionInterface 24 | */ 25 | public function apply(array $objects): array; 26 | } 27 | -------------------------------------------------------------------------------- /src/External/Api/TransferQueryInterface.php: -------------------------------------------------------------------------------- 1 | |ExtraDtoInterface|null 20 | */ 21 | public function getMeta(): array|ExtraDtoInterface|null; 22 | } 23 | -------------------------------------------------------------------------------- /src/External/Contracts/ExtraDtoInterface.php: -------------------------------------------------------------------------------- 1 | |null 17 | */ 18 | public function getExtra(): ?array; 19 | } 20 | -------------------------------------------------------------------------------- /src/External/Contracts/OptionDtoInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | public function getMeta(): ?array; 13 | 14 | public function isConfirmed(): bool; 15 | 16 | public function getUuid(): ?string; 17 | } 18 | -------------------------------------------------------------------------------- /src/External/Dto/Extra.php: -------------------------------------------------------------------------------- 1 | |null $deposit 18 | * @param OptionDtoInterface|array|null $withdraw 19 | * @param array|null $extra 20 | */ 21 | public function __construct( 22 | OptionDtoInterface|array|null $deposit, 23 | OptionDtoInterface|array|null $withdraw, 24 | private ?string $uuid = null, 25 | private ?array $extra = null 26 | ) { 27 | $this->deposit = $deposit instanceof OptionDtoInterface ? $deposit : new Option($deposit); 28 | $this->withdraw = $withdraw instanceof OptionDtoInterface ? $withdraw : new Option($withdraw); 29 | } 30 | 31 | public function getDepositOption(): OptionDtoInterface 32 | { 33 | return $this->deposit; 34 | } 35 | 36 | public function getWithdrawOption(): OptionDtoInterface 37 | { 38 | return $this->withdraw; 39 | } 40 | 41 | public function getUuid(): ?string 42 | { 43 | return $this->uuid; 44 | } 45 | 46 | /** 47 | * @return array|null 48 | */ 49 | public function getExtra(): ?array 50 | { 51 | return $this->extra; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/External/Dto/Option.php: -------------------------------------------------------------------------------- 1 | $meta 13 | */ 14 | public function __construct( 15 | private ?array $meta, 16 | private bool $confirmed = true, 17 | private ?string $uuid = null 18 | ) { 19 | } 20 | 21 | /** 22 | * @return null|array 23 | */ 24 | public function getMeta(): ?array 25 | { 26 | return $this->meta; 27 | } 28 | 29 | public function isConfirmed(): bool 30 | { 31 | return $this->confirmed; 32 | } 33 | 34 | public function getUuid(): ?string 35 | { 36 | return $this->uuid; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Interfaces/CartInterface.php: -------------------------------------------------------------------------------- 1 | |null The meta data for the product, or null if there is no meta data. 28 | * 29 | * @example 30 | * The return value should be an array with key-value pairs, for example: 31 | * 32 | * return [ 33 | * 'title' => 'Product Title', 34 | * 'description' => 'Product Description', 35 | * 'images' => [ 36 | * 'https://example.com/image1.jpg', 37 | * 'https://example.com/image2.jpg', 38 | * ], 39 | * ]; 40 | */ 41 | public function getMetaProduct(): ?array; 42 | } 43 | -------------------------------------------------------------------------------- /src/Interfaces/ProductLimitedInterface.php: -------------------------------------------------------------------------------- 1 | getKey(), 23 | $wallet->uuid, 24 | $wallet->getOriginalBalanceAttribute(), 25 | $this->clockService->now() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Assembler/BalanceUpdatedEventAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | optionDtoAssembler->create($data); 24 | 25 | return new Extra($option, $option, null); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Internal/Assembler/ExtraDtoAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | |null $data 15 | * The data to create ExtraDto from. Can be either ExtraDtoInterface object, array or null. 16 | * @return ExtraDtoInterface 17 | * The created ExtraDto. 18 | */ 19 | public function create(ExtraDtoInterface|array|null $data): ExtraDtoInterface; 20 | } 21 | -------------------------------------------------------------------------------- /src/Internal/Assembler/OptionDtoAssembler.php: -------------------------------------------------------------------------------- 1 | |null $data The data to create the OptionDto from. 15 | * This can be null, in which case an empty 16 | * OptionDto object will be created. 17 | * @return OptionDtoInterface The created OptionDto object. 18 | */ 19 | public function create(array|null $data): OptionDtoInterface; 20 | } 21 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransactionCreatedEventAssembler.php: -------------------------------------------------------------------------------- 1 | getKey(), 23 | $transaction->type, 24 | $transaction->wallet_id, 25 | $this->clockService->now(), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransactionCreatedEventAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | identifierFactoryService->generate(), 32 | $payable->getMorphClass(), 33 | $payable->getKey(), 34 | $walletId, 35 | $type, 36 | $amount, 37 | $confirmed, 38 | $meta, 39 | $this->clockService->now(), 40 | $this->clockService->now(), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransactionDtoAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | $meta 16 | */ 17 | public function create( 18 | Model $payable, 19 | int $walletId, 20 | string $type, 21 | float|int|string $amount, 22 | bool $confirmed, 23 | ?array $meta, 24 | ?string $uuid 25 | ): TransactionDtoInterface; 26 | } 27 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransactionQueryAssembler.php: -------------------------------------------------------------------------------- 1 | $uuids 14 | */ 15 | public function create(array $uuids): TransactionQueryInterface 16 | { 17 | return new TransactionQuery($uuids); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransactionQueryAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | $uuids The uuids of the transactions. 15 | * @return TransactionQueryInterface The transaction query. 16 | */ 17 | public function create(array $uuids): TransactionQueryInterface; 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransferDtoAssembler.php: -------------------------------------------------------------------------------- 1 | |null $extra 23 | */ 24 | public function create( 25 | int $depositId, 26 | int $withdrawId, 27 | string $status, 28 | Model $fromModel, 29 | Model $toModel, 30 | int $discount, 31 | string $fee, 32 | ?string $uuid, 33 | ?array $extra, 34 | ): TransferDtoInterface { 35 | return new TransferDto( 36 | $uuid ?? $this->identifierFactoryService->generate(), 37 | $depositId, 38 | $withdrawId, 39 | $status, 40 | $fromModel->getKey(), 41 | $toModel->getKey(), 42 | $discount, 43 | $fee, 44 | $extra, 45 | $this->clockService->now(), 46 | $this->clockService->now(), 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransferDtoAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | |null $extra Extra data of transfer 24 | */ 25 | public function create( 26 | int $depositId, 27 | int $withdrawId, 28 | string $status, 29 | Model $fromModel, 30 | Model $toModel, 31 | int $discount, 32 | string $fee, 33 | ?string $uuid, 34 | ?array $extra, 35 | ): TransferDtoInterface; 36 | } 37 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransferLazyDtoAssembler.php: -------------------------------------------------------------------------------- 1 | |null $extra 16 | */ 17 | public function create( 18 | Wallet $fromWallet, 19 | Wallet $toWallet, 20 | int $discount, 21 | string $fee, 22 | TransactionDtoInterface $withdrawDto, 23 | TransactionDtoInterface $depositDto, 24 | string $status, 25 | ?string $uuid, 26 | ?array $extra, 27 | ): TransferLazyDtoInterface { 28 | return new TransferLazyDto( 29 | $fromWallet, 30 | $toWallet, 31 | $discount, 32 | $fee, 33 | $withdrawDto, 34 | $depositDto, 35 | $status, 36 | $uuid, 37 | $extra, 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransferLazyDtoAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | |null $extra The extra data. 25 | * @return TransferLazyDtoInterface The transfer lazy DTO. 26 | */ 27 | public function create( 28 | Wallet $fromWallet, 29 | Wallet $toWallet, 30 | int $discount, 31 | string $fee, 32 | TransactionDtoInterface $withdrawDto, 33 | TransactionDtoInterface $depositDto, 34 | string $status, 35 | ?string $uuid, 36 | ?array $extra, 37 | ): TransferLazyDtoInterface; 38 | } 39 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransferQueryAssembler.php: -------------------------------------------------------------------------------- 1 | $uuids 14 | */ 15 | public function create(array $uuids): TransferQueryInterface 16 | { 17 | return new TransferQuery($uuids); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Internal/Assembler/TransferQueryAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | $uuids The UUIDs of the transfers. 15 | * @return TransferQueryInterface The newly created TransferQuery object. 16 | */ 17 | public function create(array $uuids): TransferQueryInterface; 18 | } 19 | -------------------------------------------------------------------------------- /src/Internal/Assembler/WalletCreatedEventAssembler.php: -------------------------------------------------------------------------------- 1 | holder_type, 23 | $wallet->holder_id, 24 | $wallet->uuid, 25 | $wallet->getKey(), 26 | $this->clockService->now() 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Assembler/WalletCreatedEventAssemblerInterface.php: -------------------------------------------------------------------------------- 1 | basketDto; 22 | } 23 | 24 | public function getCustomer(): Customer 25 | { 26 | return $this->customer; 27 | } 28 | 29 | public function isForce(): bool 30 | { 31 | return $this->force; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Internal/Dto/AvailabilityDtoInterface.php: -------------------------------------------------------------------------------- 1 | $items 14 | * @param array $meta 15 | * @param array|null $extra 16 | */ 17 | public function __construct( 18 | private array $items, 19 | private array $meta, 20 | private ?array $extra, 21 | ) { 22 | } 23 | 24 | public function meta(): array 25 | { 26 | return $this->meta; 27 | } 28 | 29 | public function count(): int 30 | { 31 | return count($this->items); 32 | } 33 | 34 | public function total(): int 35 | { 36 | return iterator_count($this->cursor()); 37 | } 38 | 39 | /** 40 | * @return Generator 41 | */ 42 | public function cursor(): Generator 43 | { 44 | foreach ($this->items as $item) { 45 | foreach ($item->getItems() as $product) { 46 | yield $product; 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * @return non-empty-array 53 | */ 54 | public function items(): array 55 | { 56 | return $this->items; 57 | } 58 | 59 | /** 60 | * @return array|null 61 | */ 62 | public function extra(): ?array 63 | { 64 | return $this->extra; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Internal/Dto/BasketDtoInterface.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function meta(): array; 24 | 25 | /** 26 | * Retrieve the extra data of the basket. 27 | * 28 | * @return array|null 29 | */ 30 | public function extra(): ?array; 31 | 32 | /** 33 | * Retrieve the items of the basket. 34 | * 35 | * @return non-empty-array 36 | */ 37 | public function items(): array; 38 | 39 | /** 40 | * Retrieve the generator for the items of the basket. 41 | * 42 | * @return Generator 43 | */ 44 | public function cursor(): Generator; 45 | } 46 | -------------------------------------------------------------------------------- /src/Internal/Dto/ItemDto.php: -------------------------------------------------------------------------------- 1 | quantity, $this->product); 27 | } 28 | 29 | public function getPricePerItem(): int|string|null 30 | { 31 | return $this->pricePerItem; 32 | } 33 | 34 | public function getProduct(): ProductInterface 35 | { 36 | return $this->product; 37 | } 38 | 39 | public function count(): int 40 | { 41 | return $this->quantity; 42 | } 43 | 44 | public function getReceiving(): ?Wallet 45 | { 46 | return $this->receiving; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Internal/Dto/ItemDtoInterface.php: -------------------------------------------------------------------------------- 1 | 56 | */ 57 | public function getMeta(): ?array; 58 | 59 | /** 60 | * Get the created at timestamp of the transaction. 61 | */ 62 | public function getCreatedAt(): DateTimeImmutable; 63 | 64 | /** 65 | * Get the updated at timestamp of the transaction. 66 | */ 67 | public function getUpdatedAt(): DateTimeImmutable; 68 | } 69 | -------------------------------------------------------------------------------- /src/Internal/Dto/TransferDtoInterface.php: -------------------------------------------------------------------------------- 1 | |null 59 | */ 60 | public function getExtra(): ?array; 61 | 62 | /** 63 | * Get the created at timestamp of the transfer. 64 | */ 65 | public function getCreatedAt(): DateTimeImmutable; 66 | 67 | /** 68 | * Get the updated at timestamp of the transfer. 69 | */ 70 | public function getUpdatedAt(): DateTimeImmutable; 71 | } 72 | -------------------------------------------------------------------------------- /src/Internal/Dto/TransferLazyDto.php: -------------------------------------------------------------------------------- 1 | |null $extra 14 | */ 15 | public function __construct( 16 | private Wallet $fromWallet, 17 | private Wallet $toWallet, 18 | private int $discount, 19 | private string $fee, 20 | private TransactionDtoInterface $withdrawDto, 21 | private TransactionDtoInterface $depositDto, 22 | private string $status, 23 | private ?string $uuid, 24 | private ?array $extra, 25 | ) { 26 | } 27 | 28 | public function getFromWallet(): Wallet 29 | { 30 | return $this->fromWallet; 31 | } 32 | 33 | public function getToWallet(): Wallet 34 | { 35 | return $this->toWallet; 36 | } 37 | 38 | public function getDiscount(): int 39 | { 40 | return $this->discount; 41 | } 42 | 43 | public function getFee(): string 44 | { 45 | return $this->fee; 46 | } 47 | 48 | public function getWithdrawDto(): TransactionDtoInterface 49 | { 50 | return $this->withdrawDto; 51 | } 52 | 53 | public function getDepositDto(): TransactionDtoInterface 54 | { 55 | return $this->depositDto; 56 | } 57 | 58 | public function getStatus(): string 59 | { 60 | return $this->status; 61 | } 62 | 63 | public function getUuid(): ?string 64 | { 65 | return $this->uuid; 66 | } 67 | 68 | /** 69 | * @return array|null 70 | */ 71 | public function getExtra(): ?array 72 | { 73 | return $this->extra; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Internal/Dto/TransferLazyDtoInterface.php: -------------------------------------------------------------------------------- 1 | |null 59 | */ 60 | public function getExtra(): ?array; 61 | } 62 | -------------------------------------------------------------------------------- /src/Internal/Events/BalanceUpdatedEvent.php: -------------------------------------------------------------------------------- 1 | walletId; 22 | } 23 | 24 | public function getWalletUuid(): string 25 | { 26 | return $this->walletUuid; 27 | } 28 | 29 | public function getBalance(): string 30 | { 31 | return $this->balance; 32 | } 33 | 34 | public function getUpdatedAt(): DateTimeImmutable 35 | { 36 | return $this->updatedAt; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Internal/Events/BalanceUpdatedEventInterface.php: -------------------------------------------------------------------------------- 1 | id; 22 | } 23 | 24 | public function getType(): string 25 | { 26 | return $this->type; 27 | } 28 | 29 | public function getWalletId(): int 30 | { 31 | return $this->walletId; 32 | } 33 | 34 | public function getCreatedAt(): DateTimeImmutable 35 | { 36 | return $this->createdAt; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Internal/Events/TransactionCreatedEventInterface.php: -------------------------------------------------------------------------------- 1 | holderType; 23 | } 24 | 25 | public function getHolderId(): int|string 26 | { 27 | return $this->holderId; 28 | } 29 | 30 | public function getWalletUuid(): string 31 | { 32 | return $this->walletUuid; 33 | } 34 | 35 | public function getWalletId(): int 36 | { 37 | return $this->walletId; 38 | } 39 | 40 | public function getCreatedAt(): DateTimeImmutable 41 | { 42 | return $this->createdAt; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Internal/Events/WalletCreatedEventInterface.php: -------------------------------------------------------------------------------- 1 | $missingKeys 13 | */ 14 | public function __construct( 15 | string $message, 16 | int $code, 17 | private readonly array $missingKeys 18 | ) { 19 | parent::__construct($message, $code); 20 | } 21 | 22 | /** 23 | * @return non-empty-array 24 | */ 25 | public function getMissingKeys(): array 26 | { 27 | return $this->missingKeys; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Exceptions/RuntimeExceptionInterface.php: -------------------------------------------------------------------------------- 1 | result; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Internal/Exceptions/UnderflowExceptionInterface.php: -------------------------------------------------------------------------------- 1 | get()->transactionLevel(); 20 | 21 | // If the transaction level is 1, it means it is the top level of a transaction 22 | if ($transactionLevel === 1) { 23 | // Get the regulator service instance 24 | /** @var RegulatorServiceInterface $regulatorService */ 25 | $regulatorService = app(RegulatorServiceInterface::class); 26 | 27 | // Purge all transactions and transfers 28 | // This method is called to ensure that all changes made to the database within the transaction 29 | // are reflected in the wallet's balance. It is important to note that this action is not reversible 30 | // and data loss is possible. 31 | $regulatorService->purge(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Internal/Query/TransactionQuery.php: -------------------------------------------------------------------------------- 1 | $uuids 16 | */ 17 | public function __construct( 18 | private array $uuids 19 | ) { 20 | } 21 | 22 | /** 23 | * @return non-empty-array 24 | */ 25 | public function getUuids(): array 26 | { 27 | return $this->uuids; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Query/TransactionQueryInterface.php: -------------------------------------------------------------------------------- 1 | An array of transaction UUIDs. 15 | */ 16 | public function getUuids(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Query/TransferQuery.php: -------------------------------------------------------------------------------- 1 | $uuids 16 | */ 17 | public function __construct( 18 | private array $uuids 19 | ) { 20 | } 21 | 22 | /** 23 | * @return non-empty-array 24 | */ 25 | public function getUuids(): array 26 | { 27 | return $this->uuids; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Internal/Query/TransferQueryInterface.php: -------------------------------------------------------------------------------- 1 | An array of transfer UUIDs. 15 | */ 16 | public function getUuids(): array; 17 | } 18 | -------------------------------------------------------------------------------- /src/Internal/Repository/TransactionRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | $objects The array of transaction objects to insert. 17 | */ 18 | public function insert(array $objects): void; 19 | 20 | /** 21 | * Inserts a single transaction into the repository. 22 | * 23 | * @param TransactionDtoInterface $dto The transaction object to insert. 24 | * @return Transaction The inserted transaction object. 25 | */ 26 | public function insertOne(TransactionDtoInterface $dto): Transaction; 27 | 28 | /** 29 | * Retrieves transactions from the repository based on the given query. 30 | * 31 | * @param TransactionQueryInterface $query The query to filter the transactions. 32 | * @return Transaction[] An array of transactions that match the query. 33 | */ 34 | public function findBy(TransactionQueryInterface $query): array; 35 | } 36 | -------------------------------------------------------------------------------- /src/Internal/Service/ClockService.php: -------------------------------------------------------------------------------- 1 | connection = $connectionResolver->connection(config('wallet.database.connection')); 20 | } 21 | 22 | public function get(): ConnectionInterface 23 | { 24 | return $this->connection; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Internal/Service/ConnectionServiceInterface.php: -------------------------------------------------------------------------------- 1 | get(); 30 | * 31 | * // Run a query on the "users" table. 32 | * * $connection->table('users')->where('id', 1)->update(['name' => 'Jane']); 33 | * 34 | * // Start a database transaction. 35 | * $connection->beginTransaction(); 36 | * 37 | * try { 38 | * // Run queries... 39 | * 40 | * // Commit the transaction. 41 | * $connection->commit(); 42 | * } catch (Exception $e) { 43 | * // Rollback the transaction. 44 | * $connection->rollback(); 45 | * 46 | * // Rethrow the exception. 47 | * throw $e; 48 | * } 49 | */ 50 | public function get(): ConnectionInterface; 51 | } 52 | -------------------------------------------------------------------------------- /src/Internal/Service/DatabaseServiceInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | private array $events = []; 16 | 17 | public function __construct( 18 | private readonly Dispatcher $dispatcher, 19 | private readonly ConnectionServiceInterface $connectionService 20 | ) { 21 | } 22 | 23 | public function dispatch(EventInterface $event): void 24 | { 25 | $this->events[$event::class] = true; 26 | $this->dispatcher->push($event::class, [$event]); 27 | } 28 | 29 | public function flush(): void 30 | { 31 | foreach (array_keys($this->events) as $event) { 32 | $this->dispatcher->flush($event); 33 | } 34 | 35 | $this->dispatcher->forgetPushed(); 36 | $this->events = []; 37 | } 38 | 39 | public function forgot(): void 40 | { 41 | foreach (array_keys($this->events) as $event) { 42 | $this->dispatcher->forget($event); 43 | } 44 | 45 | $this->events = []; 46 | } 47 | 48 | public function lazyFlush(): void 49 | { 50 | if ($this->connectionService->get()->transactionLevel() === 0) { 51 | $this->flush(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Internal/Service/DispatcherServiceInterface.php: -------------------------------------------------------------------------------- 1 | uuidFactory->uuid7($this->clockService->now()) 41 | ->toString(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Internal/Service/IdentifierFactoryServiceInterface.php: -------------------------------------------------------------------------------- 1 | |null $data The data to encode. If null, returns null. 13 | * @return string|null The JSON encoded string, or null if the input is null. 14 | * 15 | * @note The input data is expected to be an array of mixed type data. 16 | * If the input is not an array, it will be converted to an array 17 | * before being encoded. 18 | * 19 | * @see https://www.php.net/manual/en/function.json-encode.php 20 | * @see https://www.php.net/manual/en/json.constants.php 21 | * @see https://www.php.net/manual/en/jsonserializable.jsonserialize.php 22 | */ 23 | public function encode(?array $data): ?string; 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Service/LockServiceInterface.php: -------------------------------------------------------------------------------- 1 | translator->get($key); 19 | assert(is_string($value)); 20 | 21 | return $value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Internal/Service/TranslatorServiceInterface.php: -------------------------------------------------------------------------------- 1 | uuidFactory->uuid4() 22 | ->toString(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Internal/Service/UuidFactoryServiceInterface.php: -------------------------------------------------------------------------------- 1 | $dto->getUuid(), 15 | 'payable_type' => $dto->getPayableType(), 16 | 'payable_id' => $dto->getPayableId(), 17 | 'wallet_id' => $dto->getWalletId(), 18 | 'type' => $dto->getType(), 19 | 'amount' => $dto->getAmount(), 20 | 'confirmed' => $dto->isConfirmed(), 21 | 'meta' => $dto->getMeta(), 22 | 'created_at' => $dto->getCreatedAt(), 23 | 'updated_at' => $dto->getUpdatedAt(), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Internal/Transform/TransactionDtoTransformerInterface.php: -------------------------------------------------------------------------------- 1 | |null, 22 | * created_at: DateTimeImmutable, 23 | * updated_at: DateTimeImmutable, 24 | * } 25 | */ 26 | public function extract(TransactionDtoInterface $dto): array; 27 | } 28 | -------------------------------------------------------------------------------- /src/Internal/Transform/TransferDtoTransformer.php: -------------------------------------------------------------------------------- 1 | $dto->getUuid(), 15 | 'deposit_id' => $dto->getDepositId(), 16 | 'withdraw_id' => $dto->getWithdrawId(), 17 | 'status' => $dto->getStatus(), 18 | 'from_id' => $dto->getFromId(), 19 | 'to_id' => $dto->getToId(), 20 | 'discount' => $dto->getDiscount(), 21 | 'fee' => $dto->getFee(), 22 | 'extra' => $dto->getExtra(), 23 | 'created_at' => $dto->getCreatedAt(), 24 | 'updated_at' => $dto->getUpdatedAt(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Internal/Transform/TransferDtoTransformerInterface.php: -------------------------------------------------------------------------------- 1 | |null, 23 | * created_at: DateTimeImmutable, 24 | * updated_at: DateTimeImmutable, 25 | * } 26 | */ 27 | public function extract(TransferDtoInterface $dto): array; 28 | } 29 | -------------------------------------------------------------------------------- /src/Services/BasketService.php: -------------------------------------------------------------------------------- 1 | getBasketDto(); 18 | $customer = $availabilityDto->getCustomer(); 19 | foreach ($basketDto->items() as $itemDto) { 20 | $product = $itemDto->getProduct(); 21 | if ($product instanceof ProductLimitedInterface && ! $product->canBuy( 22 | $customer, 23 | $itemDto->count(), 24 | $availabilityDto->isForce() 25 | )) { 26 | return false; 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Services/BasketServiceInterface.php: -------------------------------------------------------------------------------- 1 | getPersonalDiscount($customer); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/DiscountServiceInterface.php: -------------------------------------------------------------------------------- 1 | power($decimalPlaces) 19 | ->multipliedBy(BigDecimal::of($amount)) 20 | ->toScale(0, RoundingMode::DOWN); 21 | } 22 | 23 | public function floatValue(string|int|float $amount, int $decimalPlaces): string 24 | { 25 | return (string) BigDecimal::ofUnscaledValue($amount, $decimalPlaces); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Services/FormatterServiceInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class BuyerFactory extends Factory 14 | { 15 | protected $model = Buyer::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemDiscountFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemDiscountFactory extends Factory 14 | { 15 | protected $model = ItemDiscount::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(200, 700), 23 | 'quantity' => random_int(10, 100), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemDiscountTaxFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemDiscountTaxFactory extends Factory 14 | { 15 | protected $model = ItemDiscountTax::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => 250, 23 | 'quantity' => 90, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemFactory extends Factory 14 | { 15 | protected $model = Item::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(1, 100), 23 | 'quantity' => random_int(0, 10), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemMaxTaxFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemMaxTaxFactory extends Factory 14 | { 15 | protected $model = ItemMaxTax::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(1, 100), 23 | 'quantity' => random_int(0, 10), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemMetaFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemMetaFactory extends Factory 14 | { 15 | protected $model = ItemMeta::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(1, 100), 23 | 'quantity' => random_int(0, 10), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemMinTaxFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemMinTaxFactory extends Factory 14 | { 15 | protected $model = ItemMinTax::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(1, 100), 23 | 'quantity' => random_int(0, 10), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemMultiPriceFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemMultiPriceFactory extends Factory 14 | { 15 | protected $model = ItemMultiPrice::class; 16 | 17 | public function definition(): array 18 | { 19 | $priceUsd = random_int(100, 700); 20 | 21 | return [ 22 | 'name' => fake() 23 | ->domainName, 24 | 'price' => -1, 25 | 'quantity' => random_int(10, 100), 26 | 'prices' => [ 27 | 'USD' => $priceUsd, 28 | ], 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemTaxFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemTaxFactory extends Factory 14 | { 15 | protected $model = ItemTax::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(1, 100), 23 | 'quantity' => random_int(0, 10), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ItemWalletFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ItemWalletFactory extends Factory 14 | { 15 | protected $model = ItemWallet::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->domainName, 22 | 'price' => random_int(1, 100), 23 | 'quantity' => random_int(0, 10), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Factories/ManagerFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ManagerFactory extends Factory 15 | { 16 | protected $model = Manager::class; 17 | 18 | public function definition(): array 19 | { 20 | return [ 21 | 'id' => Uuid::uuid4()->toString(), 22 | 'name' => fake() 23 | ->name, 24 | 'email' => fake() 25 | ->unique() 26 | ->safeEmail, 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Infra/Factories/UserCashierFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class UserCashierFactory extends Factory 14 | { 15 | protected $model = UserCashier::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Factories/UserConfirmFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class UserConfirmFactory extends Factory 14 | { 15 | protected $model = UserConfirm::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Factories/UserDynamicFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class UserDynamicFactory extends Factory 14 | { 15 | protected $model = UserDynamic::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class UserFactory extends Factory 14 | { 15 | protected $model = User::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Factories/UserFloatFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class UserFloatFactory extends Factory 14 | { 15 | protected $model = UserFloat::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Factories/UserMultiFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class UserMultiFactory extends Factory 14 | { 15 | protected $model = UserMulti::class; 16 | 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake() 21 | ->name, 22 | 'email' => fake() 23 | ->unique() 24 | ->safeEmail, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/Infra/Helpers/Config.php: -------------------------------------------------------------------------------- 1 | getUpdatedAt() 17 | ->format(DateTimeInterface::ATOM), 18 | (int) $balanceChangedEvent->getBalance() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Infra/Listeners/BalanceUpdatedThrowIdListener.php: -------------------------------------------------------------------------------- 1 | getWalletUuid(), 16 | (int) $balanceChangedEvent->getBalance() 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Infra/Listeners/BalanceUpdatedThrowUuidListener.php: -------------------------------------------------------------------------------- 1 | getWalletUuid(), 16 | ((int) $balanceChangedEvent->getBalance()) + $balanceChangedEvent->getWalletId(), 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Infra/Listeners/TransactionCreatedThrowListener.php: -------------------------------------------------------------------------------- 1 | getType(); 15 | $createdAt = $transactionCreatedEvent->getCreatedAt() 16 | ->format(\DateTimeInterface::ATOM); 17 | 18 | $message = hash('sha256', $type.$createdAt); 19 | 20 | throw new UnknownEventException($message, $transactionCreatedEvent->getId()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Infra/Listeners/WalletCreatedThrowListener.php: -------------------------------------------------------------------------------- 1 | getHolderType(); 16 | $uuid = $walletCreatedEvent->getWalletUuid(); 17 | $createdAt = $walletCreatedEvent->getCreatedAt() 18 | ->format(DateTimeInterface::ATOM); 19 | 20 | $message = hash('sha256', $holderType.$uuid.$createdAt); 21 | $code = $walletCreatedEvent->getWalletId() + (int) $walletCreatedEvent->getHolderId(); 22 | assert($code > 1); 23 | 24 | throw new UnknownEventException($message, $code); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Infra/Models/Buyer.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected $fillable = ['name', 'quantity', 'price']; 31 | 32 | public function getTable(): string 33 | { 34 | return 'items'; 35 | } 36 | 37 | public function canBuy(Customer $customer, int $quantity = 1, bool $force = false): bool 38 | { 39 | $result = $this->quantity >= $quantity; 40 | 41 | if ($force) { 42 | return $result; 43 | } 44 | 45 | return $result && ! $customer->paid($this) instanceof \Bavix\Wallet\Models\Transfer; 46 | } 47 | 48 | public function getAmountProduct(Customer $customer): int 49 | { 50 | /** @var Wallet $wallet */ 51 | $wallet = app(CastService::class)->getWallet($customer); 52 | 53 | return $this->price + (int) $wallet->holder_id; 54 | } 55 | 56 | /** 57 | * @return array{name: string, price: int} 58 | */ 59 | public function getMetaProduct(): ?array 60 | { 61 | return [ 62 | 'name' => $this->name, 63 | 'price' => $this->price, 64 | ]; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/Infra/Models/ItemWallet.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | protected $fillable = ['name', 'quantity', 'price']; 31 | 32 | public function getTable(): string 33 | { 34 | return 'items'; 35 | } 36 | 37 | public function canBuy(Customer $customer, int $quantity = 1, bool $force = false): bool 38 | { 39 | $result = $this->quantity >= $quantity; 40 | 41 | if ($force) { 42 | return $result; 43 | } 44 | 45 | return $result && ! $customer->paid($this) instanceof \Bavix\Wallet\Models\Transfer; 46 | } 47 | 48 | public function getAmountProduct(Customer $customer): int 49 | { 50 | /** @var Wallet $wallet */ 51 | $wallet = app(CastService::class)->getWallet($customer); 52 | 53 | return $this->price + (int) $wallet->holder_id; 54 | } 55 | 56 | public function getMetaProduct(): ?array 57 | { 58 | return null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Infra/Models/Manager.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected $fillable = ['name', 'email']; 25 | } 26 | -------------------------------------------------------------------------------- /tests/Infra/Models/User.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected $fillable = ['name', 'email']; 25 | } 26 | -------------------------------------------------------------------------------- /tests/Infra/Models/UserCashier.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | protected $fillable = ['name', 'email']; 28 | 29 | public function getTable(): string 30 | { 31 | return 'users'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Infra/Models/UserDynamic.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | protected $fillable = ['name', 'email']; 25 | 26 | public function getTable(): string 27 | { 28 | return 'users'; 29 | } 30 | 31 | /** 32 | * @return non-empty-string 33 | */ 34 | public function getDynamicDefaultSlug(): string 35 | { 36 | return 'default-'.$this->email; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Infra/Models/UserFloat.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | protected $fillable = ['name', 'email']; 26 | 27 | public function getTable(): string 28 | { 29 | return 'users'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Infra/Models/UserMulti.php: -------------------------------------------------------------------------------- 1 | fillable, ['bank_method']); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Infra/PackageModels/TransactionMoney.php: -------------------------------------------------------------------------------- 1 | currency ??= new Money($this->amount, $this->meta['currency'] ?? 'USD'); 21 | 22 | return $this->currency; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Infra/PackageModels/Transfer.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | private array $rates = [ 16 | 'USD' => [ 17 | 'BTC' => 0.004636, 18 | ], 19 | ]; 20 | 21 | public function __construct( 22 | private readonly MathServiceInterface $mathService 23 | ) { 24 | } 25 | 26 | public function convertTo(string $fromCurrency, string $toCurrency, float|int|string $amount): string 27 | { 28 | return $this->mathService->mul($amount, $this->rates[$fromCurrency][$toCurrency] ?? 1.); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Infra/Services/MyExchangeService.php: -------------------------------------------------------------------------------- 1 | > 14 | */ 15 | private array $rates = [ 16 | 'USD' => [ 17 | 'RUB' => 67.61, 18 | ], 19 | ]; 20 | 21 | public function __construct( 22 | private readonly MathServiceInterface $mathService 23 | ) { 24 | foreach ($this->rates as $from => $rates) { 25 | foreach ($rates as $to => $rate) { 26 | $this->rates[$to][$from] ??= $this->mathService->div(1, $rate); 27 | } 28 | } 29 | } 30 | 31 | public function convertTo(string $fromCurrency, string $toCurrency, float|int|string $amount): string 32 | { 33 | return $this->mathService->mul($amount, $this->rates[$fromCurrency][$toCurrency] ?? 1.); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Infra/TestServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadMigrationsFrom([dirname(__DIR__).'/migrations']); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Infra/Transform/TransactionDtoTransformerCustom.php: -------------------------------------------------------------------------------- 1 | getMeta() !== null) { 22 | $bankMethod = $dto->getMeta()['bank_method'] ?? null; 23 | } 24 | 25 | return array_merge($this->transactionDtoTransformer->extract($dto), [ 26 | 'bank_method' => $bankMethod, 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Infra/Values/Money.php: -------------------------------------------------------------------------------- 1 | create(); 22 | self::assertSame('wallet', $user->wallet->getTable()); 23 | } 24 | 25 | public function testTransactionTableName(): void 26 | { 27 | /** @var User $user */ 28 | $user = UserFactory::new()->create(); 29 | $transaction = $user->deposit(100); 30 | self::assertSame('transaction', $transaction->getTable()); 31 | } 32 | 33 | public function testTransferTableName(): void 34 | { 35 | /** 36 | * @var User $user1 37 | * @var User $user2 38 | */ 39 | [$user1, $user2] = UserFactory::times(2)->create(); 40 | $user1->deposit(1000); 41 | $transfer = $user1->transfer($user2, 1000); 42 | self::assertSame('transfer', $transfer->getTable()); 43 | 44 | /** @var Manager $manager */ 45 | $manager = ManagerFactory::new()->create(); 46 | $user2->transfer($manager, 1000); 47 | self::assertSame(1000, $manager->balanceInt); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Units/Expand/WalletTest.php: -------------------------------------------------------------------------------- 1 | MyWallet::class, 21 | ]); 22 | 23 | /** @var Buyer $buyer */ 24 | $buyer = BuyerFactory::new()->create(); 25 | 26 | /** @var MyWallet $wallet */ 27 | $wallet = $buyer->wallet; 28 | 29 | self::assertSame('hello world', $wallet->helloWorld()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Units/Service/DatabaseTest.php: -------------------------------------------------------------------------------- 1 | expectException(TransactionFailedException::class); 23 | $this->expectExceptionCode(ExceptionInterface::TRANSACTION_FAILED); 24 | $this->expectExceptionMessage('Transaction failed. Message: hello'); 25 | 26 | app(DatabaseServiceInterface::class)->transaction(static function (): never { 27 | throw new \RuntimeException('hello'); 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Units/Service/DeferrableTest.php: -------------------------------------------------------------------------------- 1 | resolveProvider(WalletServiceProvider::class); 20 | 21 | self::assertInstanceOf(DeferrableProvider::class, $walletServiceProvider); 22 | self::assertNotEmpty($walletServiceProvider->provides()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Units/Service/FormatterTest.php: -------------------------------------------------------------------------------- 1 | floatValue('12345', 3); 22 | 23 | self::assertSame('12.345', $result); 24 | } 25 | 26 | /** 27 | * @throws ExceptionInterface 28 | */ 29 | public function testFloatValueDP2(): void 30 | { 31 | $result = app(FormatterServiceInterface::class)->floatValue('12345', 2); 32 | 33 | self::assertSame('123.45', $result); 34 | } 35 | 36 | /** 37 | * @throws ExceptionInterface 38 | */ 39 | public function testIntValueDP3(): void 40 | { 41 | $result = app(FormatterServiceInterface::class)->intValue('12.345', 3); 42 | 43 | self::assertSame('12345', $result); 44 | } 45 | 46 | /** 47 | * @throws ExceptionInterface 48 | */ 49 | public function testIntValueDP2(): void 50 | { 51 | $result = app(FormatterServiceInterface::class)->intValue('123.45', 2); 52 | 53 | self::assertSame('12345', $result); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/Units/Service/JsonServiceTest.php: -------------------------------------------------------------------------------- 1 | encode(null)); 19 | self::assertJson((string) $jsonService->encode([1])); 20 | } 21 | 22 | public function testJsonEncodeFailed(): void 23 | { 24 | $jsonService = app(JsonService::class); 25 | $array = [1]; 26 | $array[] = &$array; 27 | 28 | self::assertNull($jsonService->encode($array)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Units/Service/WalletConfigureTest.php: -------------------------------------------------------------------------------- 1 | increments('id'); 15 | $table->string('name'); 16 | $table->string('email'); 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down(): void 22 | { 23 | Schema::dropIfExists('users'); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/migrations/2018_11_08_214421_create_items_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 15 | $table->string('name'); 16 | $table->integer('price'); 17 | $table->json('prices') 18 | ->nullable(); 19 | $table->unsignedSmallInteger('quantity'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | public function down(): void 25 | { 26 | Schema::dropIfExists('items'); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /tests/migrations/2019_09_19_191432_alter_column_transaction_table.php: -------------------------------------------------------------------------------- 1 | getTable(), static function (Blueprint $table) { 15 | $table->string('bank_method') 16 | ->nullable(); 17 | }); 18 | } 19 | 20 | public function down(): void 21 | { 22 | Schema::table((new Transaction())->getTable(), static function (Blueprint $table) { 23 | $table->dropColumn('bank_method'); 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /tests/migrations/2022_01_26_185311_create_managers_table.php: -------------------------------------------------------------------------------- 1 | uuid('id') 15 | ->primary(); 16 | $table->string('name'); 17 | $table->string('email'); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | public function down(): void 23 | { 24 | Schema::dropIfExists('managers'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /tests/migrations/2023_12_13_190445_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key') 18 | ->primary(); 19 | $table->mediumText('value'); 20 | $table->integer('expiration'); 21 | }); 22 | 23 | Schema::create('cache_locks', static function (Blueprint $table) { 24 | $table->string('key') 25 | ->primary(); 26 | $table->string('owner'); 27 | $table->integer('expiration'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | */ 34 | public function down(): void 35 | { 36 | Schema::dropIfExists('cache'); 37 | Schema::dropIfExists('cache_locks'); 38 | } 39 | }; 40 | --------------------------------------------------------------------------------