Skip to content

Commit f35bd23

Browse files
committed
Implement merchant fee deduction logic in PrepareService and HasGift trait
- Added support for deducting fees from merchant payouts in PrepareService and HasGift trait. - Introduced MerchantFeeDeductible interface to determine fee handling based on the product type. - Updated transaction calculations to ensure correct amounts for both customer withdrawals and merchant deposits. - Enhanced code clarity with comments explaining the fee deduction logic.
1 parent fbe3f19 commit f35bd23

File tree

6 files changed

+473
-6
lines changed

6 files changed

+473
-6
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bavix\Wallet\Interfaces;
6+
7+
/**
8+
* Interface for wallets that deduct fees from merchant's payout instead of adding to customer's payment.
9+
*
10+
* When a wallet implements this interface, the fee is deducted from the merchant's deposit
11+
* instead of being added to the customer's withdrawal. This allows customers to pay only
12+
* the listed product price, while merchants receive the product price minus the fee.
13+
*
14+
* This interface extends Taxable to reuse the fee calculation logic (getFeePercent).
15+
* It can be used alongside MinimalTaxable and MaximalTaxable interfaces.
16+
*
17+
* @example
18+
*
19+
* Without MerchantFeeDeductible (current Taxable behavior):
20+
* - Product price: $100
21+
* - Fee: 5%
22+
* - Customer pays: $105 ($100 + $5 fee)
23+
* - Merchant receives: $100
24+
*
25+
* With MerchantFeeDeductible:
26+
* - Product price: $100
27+
* - Fee: 5%
28+
* - Customer pays: $100
29+
* - Merchant receives: $95 ($100 - $5 fee)
30+
*/
31+
interface MerchantFeeDeductible extends Taxable
32+
{
33+
}

src/Services/PrepareService.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Bavix\Wallet\Exceptions\AmountInvalid;
88
use Bavix\Wallet\External\Contracts\ExtraDtoInterface;
9+
use Bavix\Wallet\Interfaces\MerchantFeeDeductible;
910
use Bavix\Wallet\Interfaces\Wallet;
1011
use Bavix\Wallet\Internal\Assembler\ExtraDtoAssemblerInterface;
1112
use Bavix\Wallet\Internal\Assembler\TransactionDtoAssemblerInterface;
@@ -112,11 +113,30 @@ public function transferExtraLazy(
112113
ExtraDtoInterface|array|null $meta = null
113114
): TransferLazyDtoInterface {
114115
$discount = $this->personalDiscountService->getDiscount($from, $to);
116+
/** @var non-empty-string $fee */
115117
$fee = $this->taxService->getFee($to, $amount);
116118

117119
$amountWithoutDiscount = $this->mathService->sub($amount, $discount, $toWallet->decimal_places);
118120
$depositAmount = $this->mathService->compare($amountWithoutDiscount, 0) === -1 ? '0' : $amountWithoutDiscount;
119-
$withdrawAmount = $this->mathService->add($depositAmount, $fee, $fromWallet->decimal_places);
121+
122+
// Check if fee should be deducted from merchant's payout instead of added to customer's payment
123+
// This follows the exact same pattern as TaxService::getFee() which checks $wallet instanceof Taxable
124+
// The $to parameter is the product model that implements Wallet through HasWallet trait
125+
// We can check $to directly since it's the model that implements the interface
126+
$isMerchantFeeDeductible = $to instanceof MerchantFeeDeductible;
127+
128+
if ($isMerchantFeeDeductible) {
129+
// Fee is deducted from merchant's deposit
130+
$withdrawAmount = $depositAmount;
131+
$merchantDepositAmount = $this->mathService->sub($depositAmount, $fee, $toWallet->decimal_places);
132+
// Ensure merchant deposit amount is not negative
133+
$merchantDepositAmount = $this->mathService->compare($merchantDepositAmount, 0) === -1 ? '0' : $merchantDepositAmount;
134+
} else {
135+
// Fee is added to customer's withdrawal (current behavior)
136+
$withdrawAmount = $this->mathService->add($depositAmount, $fee, $fromWallet->decimal_places);
137+
$merchantDepositAmount = $depositAmount;
138+
}
139+
120140
$extra = $this->extraDtoAssembler->create($meta);
121141
$withdrawOption = $extra->getWithdrawOption();
122142
$depositOption = $extra->getDepositOption();
@@ -131,7 +151,7 @@ public function transferExtraLazy(
131151

132152
$deposit = $this->deposit(
133153
$toWallet,
134-
$depositAmount,
154+
$merchantDepositAmount,
135155
$depositOption->getMeta(),
136156
$depositOption->isConfirmed(),
137157
$depositOption->getUuid(),

src/Traits/HasGift.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use function app;
88
use Bavix\Wallet\Exceptions\BalanceIsEmpty;
99
use Bavix\Wallet\Exceptions\InsufficientFunds;
10+
use Bavix\Wallet\Interfaces\MerchantFeeDeductible;
1011
use Bavix\Wallet\Interfaces\ProductInterface;
1112
use Bavix\Wallet\Interfaces\Wallet;
1213
use Bavix\Wallet\Internal\Assembler\TransferDtoAssemblerInterface;
@@ -106,25 +107,44 @@ public function gift(Wallet $to, ProductInterface $product, bool $force = false)
106107
$amount = $mathService->sub($product->getAmountProduct($this), $discount);
107108

108109
// Get the fee for the transaction
110+
/** @var non-empty-string $fee */
109111
$fee = $taxService->getFee($product, $amount);
110112

113+
// Check if fee should be deducted from merchant's payout instead of added to customer's payment
114+
$isMerchantFeeDeductible = $product instanceof MerchantFeeDeductible;
115+
111116
// Check if the gift can be forced without checking the balance
112117
if (! $force) {
113-
// Check the consistency of the potential transaction
114-
$consistencyService->checkPotential($this, $mathService->add($amount, $fee));
118+
// If merchant fee is deductible, customer only needs to pay the amount (no fee)
119+
// Otherwise, customer needs to pay amount + fee
120+
$requiredAmount = $isMerchantFeeDeductible ? $amount : $mathService->add($amount, $fee);
121+
$consistencyService->checkPotential($this, $requiredAmount);
122+
}
123+
124+
// Calculate withdraw and deposit amounts based on fee deduction type
125+
if ($isMerchantFeeDeductible) {
126+
// Fee is deducted from merchant's deposit
127+
$withdrawAmount = $amount;
128+
$merchantDepositAmount = $mathService->sub($amount, $fee);
129+
// Ensure merchant deposit amount is not negative
130+
$merchantDepositAmount = $mathService->compare($merchantDepositAmount, 0) === -1 ? '0' : $merchantDepositAmount;
131+
} else {
132+
// Fee is added to customer's withdrawal (current behavior)
133+
$withdrawAmount = $mathService->add($amount, $fee);
134+
$merchantDepositAmount = $amount;
115135
}
116136

117137
// Create withdraw and deposit transactions
118138
$withdraw = $transactionService->makeOne(
119139
$this,
120140
Transaction::TYPE_WITHDRAW,
121-
$mathService->add($amount, $fee),
141+
$withdrawAmount,
122142
$product->getMetaProduct()
123143
);
124144
$deposit = $transactionService->makeOne(
125145
$product,
126146
Transaction::TYPE_DEPOSIT,
127-
$amount,
147+
$merchantDepositAmount,
128148
$product->getMetaProduct()
129149
);
130150

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bavix\Wallet\Test\Infra\Factories;
6+
7+
use Bavix\Wallet\Test\Infra\Models\ItemMerchantFeeDeductible;
8+
use Illuminate\Database\Eloquent\Factories\Factory;
9+
10+
/**
11+
* @extends Factory<ItemMerchantFeeDeductible>
12+
*/
13+
final class ItemMerchantFeeDeductibleFactory extends Factory
14+
{
15+
protected $model = ItemMerchantFeeDeductible::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+
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bavix\Wallet\Test\Infra\Models;
6+
7+
use Bavix\Wallet\Interfaces\Customer;
8+
use Bavix\Wallet\Interfaces\MerchantFeeDeductible;
9+
use Bavix\Wallet\Interfaces\ProductLimitedInterface;
10+
use Bavix\Wallet\Models\Wallet;
11+
use Bavix\Wallet\Services\CastService;
12+
use Bavix\Wallet\Traits\HasWallet;
13+
use Illuminate\Database\Eloquent\Model;
14+
15+
/**
16+
* @property string $name
17+
* @property int $quantity
18+
* @property int $price
19+
*
20+
* @method int getKey()
21+
*/
22+
final class ItemMerchantFeeDeductible extends Model implements ProductLimitedInterface, MerchantFeeDeductible
23+
{
24+
use HasWallet;
25+
26+
/**
27+
* @var array<int, string>
28+
*/
29+
protected $fillable = ['name', 'quantity', 'price'];
30+
31+
#[\Override]
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+
/**
62+
* Specify the percentage of the amount. For example, the product costs $100, the fee is 5%.
63+
* With MerchantFeeDeductible, customer pays $100, merchant receives $95.
64+
*
65+
* Minimum 0; Maximum 100 Example: return 5.0; // 5%
66+
*/
67+
public function getFeePercent(): float
68+
{
69+
return 5.0;
70+
}
71+
}
72+

0 commit comments

Comments
 (0)