diff --git a/database/factories/ItemFactory.php b/database/factories/ItemFactory.php index 6c8fe2df..9a496a2a 100644 --- a/database/factories/ItemFactory.php +++ b/database/factories/ItemFactory.php @@ -33,7 +33,6 @@ public function modelName(): string public function definition(): array { return [ - 'tax' => mt_rand(10, 100), 'name' => $this->faker->company(), 'price' => mt_rand(10, 100), 'quantity' => mt_rand(1, 10), diff --git a/database/factories/ShippingFactory.php b/database/factories/ShippingFactory.php index 7a151035..5148f22f 100644 --- a/database/factories/ShippingFactory.php +++ b/database/factories/ShippingFactory.php @@ -34,8 +34,7 @@ public function modelName(): string public function definition(): array { return [ - 'tax' => mt_rand(0, 20), - 'cost' => mt_rand(0, 100), + 'fee' => mt_rand(0, 100), 'driver' => Manager::getDefaultDriver(), ]; } diff --git a/database/factories/TaxFactory.php b/database/factories/TaxFactory.php new file mode 100644 index 00000000..e69de29b diff --git a/database/factories/TaxRateFactory.php b/database/factories/TaxRateFactory.php new file mode 100644 index 00000000..e69de29b diff --git a/database/migrations/2020_01_01_000300_create_bazar_items_table.php b/database/migrations/2020_01_01_000300_create_bazar_items_table.php index 51ce38c9..bfa0654c 100644 --- a/database/migrations/2020_01_01_000300_create_bazar_items_table.php +++ b/database/migrations/2020_01_01_000300_create_bazar_items_table.php @@ -17,7 +17,6 @@ public function up(): void $table->nullableMorphs('buyable'); $table->string('name'); $table->float('price')->unsigned(); - $table->float('tax')->unsigned()->default(0); $table->float('quantity')->unsigned(); $table->json('properties')->nullable(); $table->timestamps(); diff --git a/database/migrations/2020_01_01_001000_create_bazar_shippings_table.php b/database/migrations/2020_01_01_001000_create_bazar_shippings_table.php index 97843534..442c8711 100644 --- a/database/migrations/2020_01_01_001000_create_bazar_shippings_table.php +++ b/database/migrations/2020_01_01_001000_create_bazar_shippings_table.php @@ -15,8 +15,7 @@ public function up(): void $table->id(); $table->morphs('shippable'); $table->string('driver'); - $table->float('cost')->unsigned()->default(0); - $table->float('tax')->unsigned()->default(0); + $table->float('fee')->unsigned()->default(0); $table->timestamps(); }); } diff --git a/database/migrations/2024_07_28_082227_create_bazar_tax_rates_table.php b/database/migrations/2024_07_28_082227_create_bazar_tax_rates_table.php index 1c12f5ab..6c583cb1 100644 --- a/database/migrations/2024_07_28_082227_create_bazar_tax_rates_table.php +++ b/database/migrations/2024_07_28_082227_create_bazar_tax_rates_table.php @@ -13,6 +13,8 @@ public function up(): void { Schema::create('bazar_tax_rates', static function (Blueprint $table): void { $table->id(); + $table->integer('value')->unsigned(); + $table->boolean('shipping')->default(false); $table->timestamps(); }); } diff --git a/database/migrations/2024_07_28_082252_create_bazar_buyable_tax_rate_table.php b/database/migrations/2024_07_28_082252_create_bazar_buyable_tax_rate_table.php index eaddfa89..4f776f00 100644 --- a/database/migrations/2024_07_28_082252_create_bazar_buyable_tax_rate_table.php +++ b/database/migrations/2024_07_28_082252_create_bazar_buyable_tax_rate_table.php @@ -13,6 +13,8 @@ public function up(): void { Schema::create('bazar_buyable_tax_rate', static function (Blueprint $table): void { $table->id(); + $table->foreignId('tax_rate_id')->constrained('bazar_tax_rates')->cascadeOnDelete(); + $table->morphs('buyable'); $table->timestamps(); }); } diff --git a/database/migrations/2024_07_28_082302_create_bazar_taxes_table.php b/database/migrations/2024_07_28_082302_create_bazar_taxes_table.php index 4c5bf918..3e26a248 100644 --- a/database/migrations/2024_07_28_082302_create_bazar_taxes_table.php +++ b/database/migrations/2024_07_28_082302_create_bazar_taxes_table.php @@ -13,7 +13,12 @@ public function up(): void { Schema::create('bazar_taxes', static function (Blueprint $table): void { $table->id(); + $table->foreignId('tax_rate_id')->nullable()->constrained('bazar_tax_rates')->nullOnDelete(); + $table->morphs('taxable'); + $table->float('value')->unsigned(); $table->timestamps(); + + $table->unique(['taxable_id', 'taxable_type', 'tax_rate_id']); }); } diff --git a/src/BazarServiceProvider.php b/src/BazarServiceProvider.php index 0cd4185a..4dc731f7 100644 --- a/src/BazarServiceProvider.php +++ b/src/BazarServiceProvider.php @@ -22,6 +22,8 @@ class BazarServiceProvider extends ServiceProvider Interfaces\Models\Property::class => Models\Property::class, Interfaces\Models\PropertyValue::class => Models\PropertyValue::class, Interfaces\Models\Shipping::class => Models\Shipping::class, + Interfaces\Models\Tax::class => Models\Tax::class, + Interfaces\Models\TaxRate::class => Models\TaxRate::class, Interfaces\Models\Transaction::class => Models\Transaction::class, Interfaces\Models\Variant::class => Models\Variant::class, ]; @@ -33,7 +35,6 @@ class BazarServiceProvider extends ServiceProvider Interfaces\Cart\Manager::class => Cart\Manager::class, Interfaces\Gateway\Manager::class => Gateway\Manager::class, Interfaces\Repositories\DiscountRepository::class => Repositories\DiscountRepository::class, - Interfaces\Repositories\TaxRepository::class => Repositories\TaxRepository::class, Interfaces\Shipping\Manager::class => Shipping\Manager::class, ]; diff --git a/src/Cart/Driver.php b/src/Cart/Driver.php index 1bb1d5aa..d2ba0a91 100644 --- a/src/Cart/Driver.php +++ b/src/Cart/Driver.php @@ -69,8 +69,8 @@ public function getModel(): Cart if (! $cart->locked && $cart->currency !== Bazar::getCurrency()) { $cart->setAttribute('currency', Bazar::getCurrency()); $cart->syncItems(); - $cart->shipping->calculateCost(false); - $cart->shipping->calculateTax(); + $cart->shipping->calculateFee(); + $cart->shipping->calculateTaxes(); $cart->calculateDiscount(); } }); @@ -147,7 +147,7 @@ public function removeItems(array $ids): void public function updateItem(string $id, array $properties = []): void { if ($item = $this->getItem($id)) { - $item->fill($properties)->calculateTax(); + $item->fill($properties)->calculateTaxes(); $this->sync(); } @@ -161,7 +161,7 @@ public function updateItems(array $data): void $items = $this->getItems()->whereIn('id', array_keys($data)); $items->each(static function (Item $item) use ($data): void { - $item->fill($data[$item->getKey()])->calculateTax(); + $item->fill($data[$item->getKey()])->calculateTaxes(); }); if ($items->isNotEmpty()) { @@ -229,7 +229,7 @@ public function empty(): void $this->getModel()->items()->delete(); $this->getModel()->setRelation('items', $this->getModel()->items()->getRelated()->newCollection()); - $this->getShipping()->update(['tax' => 0, 'cost' => 0]); + $this->getShipping()->update(['tax' => 0, 'fee' => 0]); $this->getModel()->calculateDiscount(); } @@ -273,8 +273,10 @@ public function isNotEmpty(): bool */ public function sync(): void { - $this->getShipping()->calculateCost(false); - $this->getShipping()->calculateTax(); + $this->getShipping()->calculateFee(); + $this->getShipping()->save(); + + $this->getShipping()->calculateTaxes(); $this->getModel()->calculateDiscount(); } diff --git a/src/Interfaces/Buyable.php b/src/Interfaces/Buyable.php index ecab40ea..0bd8eaae 100644 --- a/src/Interfaces/Buyable.php +++ b/src/Interfaces/Buyable.php @@ -3,6 +3,7 @@ namespace Cone\Bazar\Interfaces; use Cone\Bazar\Models\Item; +use Illuminate\Database\Eloquent\Relations\MorphToMany; interface Buyable { @@ -11,6 +12,11 @@ interface Buyable */ public function buyable(Checkoutable $checkoutable): bool; + /** + * Get the tax rates for the variant. + */ + public function taxRates(): MorphToMany; + /** * Get the item representation of the buyable instance. */ diff --git a/src/Interfaces/Checkoutable.php b/src/Interfaces/Checkoutable.php index 09e786a6..b5f09920 100644 --- a/src/Interfaces/Checkoutable.php +++ b/src/Interfaces/Checkoutable.php @@ -66,7 +66,7 @@ public function getFormattedTax(): string; /** * Calculate the tax. */ - public function calculateTax(bool $update = true): float; + public function calculateTax(): float; /** * Find an item by its attributes or make a new instance. diff --git a/src/Interfaces/Models/Shipping.php b/src/Interfaces/Models/Shipping.php index c568b1b7..dddae7b2 100644 --- a/src/Interfaces/Models/Shipping.php +++ b/src/Interfaces/Models/Shipping.php @@ -13,7 +13,7 @@ interface Shipping extends LineItem public function shippable(): MorphTo; /** - * Calculate the cost. + * Calculate the fee. */ - public function calculateCost(bool $update = true): float; + public function calculateFee(): float; } diff --git a/src/Interfaces/Models/Tax.php b/src/Interfaces/Models/Tax.php index 4d0f57cb..a59a5a26 100644 --- a/src/Interfaces/Models/Tax.php +++ b/src/Interfaces/Models/Tax.php @@ -2,7 +2,23 @@ namespace Cone\Bazar\Interfaces\Models; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphTo; + interface Tax { - // + /** + * Get the taxable model for the model. + */ + public function taxable(): MorphTo; + + /** + * Get the tax rate for the model. + */ + public function taxRate(): BelongsTo; + + /** + * Get the formatted tax. + */ + public function format(): string; } diff --git a/src/Interfaces/Models/Variant.php b/src/Interfaces/Models/Variant.php index 28f1afba..9b690f77 100644 --- a/src/Interfaces/Models/Variant.php +++ b/src/Interfaces/Models/Variant.php @@ -11,22 +11,22 @@ interface Variant extends Buyable, Stockable { /** - * Get the items for the product. + * Get the items for the variant. */ public function items(): MorphMany; /** - * Get the orders for the product. + * Get the orders for the variant. */ public function orders(): HasManyThrough; /** - * Get the carts for the product. + * Get the carts for the variant. */ public function carts(): HasManyThrough; /** - * Get the product for the transaction. + * Get the product for the variant. */ public function product(): BelongsTo; } diff --git a/src/Interfaces/Tax.php b/src/Interfaces/Tax.php deleted file mode 100644 index 4d112088..00000000 --- a/src/Interfaces/Tax.php +++ /dev/null @@ -1,11 +0,0 @@ -<?php - -namespace Cone\Bazar\Interfaces; - -interface Tax -{ - /** - * Calculate the tax for the given model. - */ - public function __invoke(Taxable $model): float; -} diff --git a/src/Interfaces/Taxable.php b/src/Interfaces/Taxable.php index de32c88a..d745674b 100644 --- a/src/Interfaces/Taxable.php +++ b/src/Interfaces/Taxable.php @@ -2,30 +2,32 @@ namespace Cone\Bazar\Interfaces; +use Illuminate\Database\Eloquent\Relations\MorphMany; + interface Taxable { /** - * Get the tax. + * Get the taxes for the model. */ - public function getTax(): float; + public function taxes(): MorphMany; /** - * Get the formatted tax. + * Get the tax base. */ - public function getFormattedTax(): string; + public function getTaxBase(): float; /** - * Get the tax rate. + * Get the tax total. */ - public function getTaxRate(): float; + public function getTaxTotal(): float; /** - * Get the formatted tax rate. + * Get the formatted tax. */ - public function getFormattedTaxRate(): string; + public function getFormattedTaxTotal(): string; /** - * Calculate the tax. + * Calculate the taxes. */ - public function calculateTax(bool $update = true): float; + public function calculateTaxes(): float; } diff --git a/src/Models/Item.php b/src/Models/Item.php index c7a1b587..6bc267c5 100644 --- a/src/Models/Item.php +++ b/src/Models/Item.php @@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; -use Illuminate\Support\Number; class Item extends Model implements Contract { @@ -42,7 +41,6 @@ class Item extends Model implements Contract 'price' => 0, 'properties' => '[]', 'quantity' => 1, - 'tax' => 0, ]; /** @@ -54,7 +52,6 @@ class Item extends Model implements Contract 'price' => 'float', 'properties' => 'json', 'quantity' => 'float', - 'tax' => 'float', ]; /** @@ -69,7 +66,6 @@ class Item extends Model implements Contract 'price', 'properties', 'quantity', - 'tax', ]; /** @@ -148,7 +144,7 @@ protected function formattedPrice(): Attribute protected function total(): Attribute { return new Attribute( - get: fn (): float => $this->getTotal(), + get: fn (): float => $this->getTotal() ); } @@ -160,7 +156,7 @@ protected function total(): Attribute protected function formattedTotal(): Attribute { return new Attribute( - get: fn (): string => $this->getFormattedTotal(), + get: fn (): string => $this->getFormattedTotal() ); } @@ -172,7 +168,7 @@ protected function formattedTotal(): Attribute protected function subtotal(): Attribute { return new Attribute( - get: fn (): float => $this->getSubtotal(), + get: fn (): float => $this->getSubtotal() ); } @@ -184,7 +180,7 @@ protected function subtotal(): Attribute protected function formattedSubtotal(): Attribute { return new Attribute( - get: fn (): string => $this->getFormattedSubtotal(), + get: fn (): string => $this->getFormattedSubtotal() ); } @@ -217,7 +213,7 @@ public function getFormattedPrice(): string */ public function getTotal(): float { - return ($this->getPrice() + $this->getTax()) * $this->getQuantity(); + return ($this->getPrice() + $this->getTaxTotal()) * $this->getQuantity(); } /** @@ -245,19 +241,19 @@ public function getFormattedSubtotal(): string } /** - * Get the tax rate. + * Get the tax base. */ - public function getTaxRate(): float + public function getTaxBase(): float { - return $this->getPrice() > 0 ? ($this->getTax() / $this->getPrice()) * 100 : 0; + return $this->price; } /** - * Get the formatted tax rate. + * Get the formatted tax. */ - public function getFormattedTaxRate(): string + public function getFormattedTaxTotal(): string { - return Number::percentage($this->getTaxRate()); + return (new Currency($this->getTaxTotal(), $this->checkoutable->getCurrency()))->format(); } /** @@ -283,4 +279,16 @@ public function isFee(): bool { return ! $this->isLineItem(); } + + /** + * Calculate the taxes. + */ + public function calculateTaxes(): float + { + $this->buyable->taxRates->each(function (TaxRate $taxRate): void { + $taxRate->calculate($this); + }); + + return $this->getTaxTotal(); + } } diff --git a/src/Models/Product.php b/src/Models/Product.php index 7bb1d5ed..12175bdb 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -91,10 +92,18 @@ public function variants(): HasMany return $this->hasMany(Variant::getProxiedClass()); } + /** + * Get the tax rates. + */ + public function taxRates(): MorphToMany + { + return $this->morphToMany(TaxRate::getProxiedClass(), 'buyable', 'bazar_buyable_tax_rate'); + } + /** * Determine whether the buyable object is available for the checkoutable instance. */ - public function buyable(Checkoutable $checkoutable): bool + public function buyable(Checkoutable $model): bool { return true; } diff --git a/src/Models/Shipping.php b/src/Models/Shipping.php index 18986af5..9ca254e6 100644 --- a/src/Models/Shipping.php +++ b/src/Models/Shipping.php @@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; -use Illuminate\Support\Number; use Throwable; class Shipping extends Model implements Contract @@ -30,8 +29,7 @@ class Shipping extends Model implements Contract * @var array<string, mixed> */ protected $attributes = [ - 'cost' => 0, - 'tax' => 0, + 'fee' => 0, ]; /** @@ -40,8 +38,7 @@ class Shipping extends Model implements Contract * @var array<string, string> */ protected $casts = [ - 'cost' => 'float', - 'tax' => 'float', + 'fee' => 'float', ]; /** @@ -50,9 +47,8 @@ class Shipping extends Model implements Contract * @var list<string> */ protected $fillable = [ - 'cost', + 'fee', 'driver', - 'tax', ]; /** @@ -202,12 +198,20 @@ public function getName(): string return $this->driverName; } + /** + * Get the tax base. + */ + public function getTaxBase(): float + { + return $this->fee; + } + /** * Get the price. */ public function getPrice(): float { - return $this->cost; + return $this->fee; } /** @@ -223,7 +227,7 @@ public function getFormattedPrice(): string */ public function getTotal(): float { - return $this->getPrice() + $this->getTax(); + return $this->getPrice() + $this->getTaxTotal(); } /** @@ -251,19 +255,11 @@ public function getFormattedSubtotal(): string } /** - * Get the tax rate. - */ - public function getTaxRate(): float - { - return $this->getPrice() > 0 ? ($this->getTax() / $this->getPrice()) * 100 : 0; - } - - /** - * Get the formatted tax rate. + * Get the formatted tax total. */ - public function getFormattedTaxRate(): string + public function getFormattedTaxTotal(): string { - return Number::percentage($this->getTaxRate()); + return (new Currency($this->getTaxTotal(), $this->shippable->getCurrency()))->format(); } /** @@ -275,20 +271,30 @@ public function getQuantity(): float } /** - * Calculate the cost. + * Calculate the fee. */ - public function calculateCost(bool $update = true): float + public function calculateFee(): float { try { - $this->cost = Manager::driver($this->driver)->calculate($this->shippable); - - if ($update) { - $this->save(); - } + $this->update([ + 'fee' => Manager::driver($this->driver)->calculate($this->shippable), + ]); } catch (Throwable $exception) { // - } finally { - return $this->cost; } + + return $this->fee; + } + + /** + * Calculate the taxes. + */ + public function calculateTaxes(): float + { + TaxRate::proxy()->newQuery()->applicableForShipping()->cursor()->each(function (TaxRate $taxRate): void { + $taxRate->calculate($this); + }); + + return $this->getTaxTotal(); } } diff --git a/src/Models/Tax.php b/src/Models/Tax.php index 5ab7ab68..28a99057 100644 --- a/src/Models/Tax.php +++ b/src/Models/Tax.php @@ -3,15 +3,53 @@ namespace Cone\Bazar\Models; use Cone\Bazar\Interfaces\Models\Tax as Contract; +use Cone\Bazar\Support\Currency; use Cone\Root\Traits\InteractsWithProxy; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphTo; class Tax extends Model implements Contract { use HasFactory; use InteractsWithProxy; + /** + * The attributes that should have default values. + * + * @var array<string, mixed> + */ + protected $attributes = [ + 'value' => null, + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array<string, string> + */ + protected $casts = [ + 'value' => 'float', + ]; + + /** + * The attributes that are mass assignable. + * + * @var list<string> + */ + protected $fillable = [ + 'value', + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'bazar_taxes'; + /** * Get the proxied interface. */ @@ -19,4 +57,38 @@ public static function getProxiedInterface(): string { return Contract::class; } + + /** + * Get the taxable model for the model. + */ + public function taxable(): MorphTo + { + return $this->morphTo(); + } + + /** + * Get the tax rate for the model. + */ + public function taxRate(): BelongsTo + { + return $this->belongsTo(TaxRate::getProxiedClass()); + } + + /** + * Get the formatted value attribute. + */ + protected function formattedValue(): Attribute + { + return new Attribute( + get: fn (): string => $this->format() + ); + } + + /** + * Get the formatted tax. + */ + public function format(): string + { + return (new Currency($this->value, $this->taxable?->checkoutable?->getCurrency()))->format(); + } } diff --git a/src/Models/TaxRate.php b/src/Models/TaxRate.php index 7a76093a..fdc2fea9 100644 --- a/src/Models/TaxRate.php +++ b/src/Models/TaxRate.php @@ -3,7 +3,9 @@ namespace Cone\Bazar\Models; use Cone\Bazar\Interfaces\Models\TaxRate as Contract; +use Cone\Bazar\Interfaces\Taxable; use Cone\Root\Traits\InteractsWithProxy; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -12,6 +14,13 @@ class TaxRate extends Model implements Contract use HasFactory; use InteractsWithProxy; + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'bazar_tax_rates'; + /** * Get the proxied interface. */ @@ -19,4 +28,25 @@ public static function getProxiedInterface(): string { return Contract::class; } + + /** + * Calculate the tax for the taxable model. + */ + public function calculate(Taxable $taxable): Tax + { + $value = round($taxable->getTaxBase() * $this->rate, 2); + + return $taxable->taxes()->updateOrCreate( + ['tax_rate_id' => $this->getKey()], + ['value' => $value] + ); + } + + /** + * Scope the query for the results that are applicable for shipping. + */ + public function scopeApplicableForShipping(Builder $query): Builder + { + return $query->where($query->qualifyColumn('shipping'), true); + } } diff --git a/src/Models/Variant.php b/src/Models/Variant.php index 3d2cb18b..c4599a77 100644 --- a/src/Models/Variant.php +++ b/src/Models/Variant.php @@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\MorphToMany; use Illuminate\Database\Eloquent\SoftDeletes; class Variant extends Model implements Contract @@ -83,6 +84,14 @@ public function product(): BelongsTo ->withDefault(); } + /** + * Get the tax rates. + */ + public function taxRates(): MorphToMany + { + return $this->morphToMany(TaxRate::getProxiedClass(), 'buyable', 'bazar_buyable_tax_rate'); + } + /** * Get the name attribute. * diff --git a/src/Repositories/TaxRepository.php b/src/Repositories/TaxRepository.php deleted file mode 100644 index 956b98ad..00000000 --- a/src/Repositories/TaxRepository.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -namespace Cone\Bazar\Repositories; - -use Closure; -use Cone\Bazar\Interfaces\Repositories\TaxRepository as Contract; -use Cone\Bazar\Interfaces\Tax; -use Cone\Bazar\Interfaces\Taxable; - -class TaxRepository extends Repository implements Contract -{ - /** - * Determine if the taxes are disabled. - */ - protected bool $disabled = false; - - /** - * Register a new tax. - */ - public function register(string $name, int|float|Closure|Tax $tax): void - { - $this->items->put($name, $tax); - } - - /** - * Disable the tax calculation. - */ - public function disable(): void - { - $this->disabled = true; - } - - /** - * Enable the tax calculation. - */ - public function enable(): void - { - $this->disabled = false; - } - - /** - * Calculate tax for the given item. - */ - public function calculate(Taxable $model): float - { - return $this->disabled - ? $model->tax - : $this->items->sum(function (int|float|Closure|Tax $tax) use ($model): float { - return $this->process($model, $tax); - }); - } - - /** - * Process the calculation. - */ - protected function process(Taxable $model, int|float|Closure|Tax $tax): float - { - if (is_numeric($tax)) { - return $tax; - } - - if ($tax instanceof Closure) { - return call_user_func_array($tax, [$model]); - } - - if ($tax instanceof Tax) { - return call_user_func_array([$tax, '__invoke'], [$model]); - } - - return 0; - } -} diff --git a/src/Shipping/Driver.php b/src/Shipping/Driver.php index b0e6f2cd..46a82276 100644 --- a/src/Shipping/Driver.php +++ b/src/Shipping/Driver.php @@ -8,7 +8,7 @@ abstract class Driver extends BaseDriver { /** - * Calculate the shipping cost. + * Calculate the shipping fee. */ abstract public function calculate(Shippable $model): float; } diff --git a/src/Shipping/LocalPickupDriver.php b/src/Shipping/LocalPickupDriver.php index 4e78318b..4361208e 100644 --- a/src/Shipping/LocalPickupDriver.php +++ b/src/Shipping/LocalPickupDriver.php @@ -7,7 +7,7 @@ class LocalPickupDriver extends Driver { /** - * Calculate the shipping cost. + * Calculate the shipping fee. */ public function calculate(Shippable $model): float { diff --git a/src/Support/Facades/Tax.php b/src/Support/Facades/Tax.php deleted file mode 100644 index 8bf30fd9..00000000 --- a/src/Support/Facades/Tax.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -namespace Cone\Bazar\Support\Facades; - -use Cone\Bazar\Interfaces\Repositories\TaxRepository; -use Illuminate\Support\Facades\Facade; - -/** - * @method static void register(string $name, int|callable $tax) - * @method static void remove(string $name) - * @method static void disable() - * @method static void enable() - * @method static float calculate(\Cone\Bazar\Interfaces\Taxable $model) - * - * @see \Cone\Bazar\Interfaces\Repositories\TaxRepository - */ -class Tax extends Facade -{ - /** - * Get the registered name of the component. - */ - protected static function getFacadeAccessor(): string - { - return TaxRepository::class; - } -} diff --git a/src/Traits/InteractsWithItems.php b/src/Traits/InteractsWithItems.php index 70edbff9..9c9e20d1 100644 --- a/src/Traits/InteractsWithItems.php +++ b/src/Traits/InteractsWithItems.php @@ -290,7 +290,7 @@ public function getFormattedFeeTotal(): string public function getTax(): float { $value = $this->taxables->sum(static function (LineItem $item): float { - return $item->getTax() * $item->getQuantity(); + return $item->getTaxTotal() * $item->getQuantity(); }); return round($value, 2); @@ -307,10 +307,10 @@ public function getFormattedTax(): string /** * Calculate the tax. */ - public function calculateTax(bool $update = true): float + public function calculateTax(): float { - return $this->taxables->sum(static function (LineItem $item) use ($update): float { - return $item->calculateTax($update) * $item->getQuantity(); + return $this->taxables->sum(static function (LineItem $item): float { + return $item->calculateTaxes() * $item->getQuantity(); }); } @@ -365,7 +365,7 @@ public function syncItems(): void if ($item->isLineItem() && ! is_null($item->checkoutable)) { $data = $item->buyable->toItem($item->checkoutable, $item->only('properties'))->only('price'); - $item->fill($data)->calculateTax(); + $item->fill($data)->calculateTaxes(); } }); } diff --git a/src/Traits/InteractsWithTaxes.php b/src/Traits/InteractsWithTaxes.php index d146028b..989c626b 100644 --- a/src/Traits/InteractsWithTaxes.php +++ b/src/Traits/InteractsWithTaxes.php @@ -2,64 +2,49 @@ namespace Cone\Bazar\Traits; -use Cone\Bazar\Bazar; -use Cone\Bazar\Models\Item; -use Cone\Bazar\Models\Shipping; -use Cone\Bazar\Support\Currency; -use Cone\Bazar\Support\Facades\Tax; +use Cone\Bazar\Models\Tax; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Relations\MorphMany; trait InteractsWithTaxes { /** - * Get the formatted tax attribute. - * - * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never> + * Get the taxes for the model. */ - protected function formattedTax(): Attribute + public function taxes(): MorphMany { - return new Attribute( - get: fn (): string => $this->getFormattedTax() - ); + return $this->morphMany(Tax::getProxiedClass(), 'taxable'); } /** - * Get the tax. + * Get the tax total. + * + * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never> */ - public function getTax(): float + protected function taxTotal(): Attribute { - return $this->tax; + return new Attribute( + get: fn (): float => $this->getTaxTotal() + ); } /** - * Get the formatted tax. + * Get the formatted tax attribute. + * + * @return \Illuminate\Database\Eloquent\Casts\Attribute<string, never> */ - public function getFormattedTax(): string + protected function formattedTaxTotal(): Attribute { - $currency = Bazar::getCurrency(); - - if ($this instanceof Item) { - $currency = $this->checkoutable->currency; - } - - if ($this instanceof Shipping) { - $currency = $this->shippable->currency; - } - - return (new Currency($this->getTax(), $currency))->format(); + return new Attribute( + get: fn (): string => $this->getFormattedTaxTotal() + ); } /** - * Calculate the tax. + * Get the tax total. */ - public function calculateTax(bool $update = true): float + public function getTaxTotal(): float { - $this->tax = Tax::calculate($this); - - if ($update) { - $this->save(); - } - - return $this->tax; + return $this->taxes->sum('value'); } } diff --git a/tests/Models/ItemTest.php b/tests/Models/ItemTest.php index 708de7ed..a8fd1a73 100644 --- a/tests/Models/ItemTest.php +++ b/tests/Models/ItemTest.php @@ -7,7 +7,6 @@ use Cone\Bazar\Models\Item; use Cone\Bazar\Models\Product; use Cone\Bazar\Support\Currency; -use Cone\Bazar\Support\Facades\Tax; use Cone\Bazar\Tests\TestCase; class ItemTest extends TestCase @@ -18,10 +17,6 @@ public function setUp(): void { parent::setUp(); - Tax::register('fix-10%', function (Taxable $item) { - return $item->price * 0.1; - }); - $cart = Cart::factory()->create(); $product = Product::factory()->create(); @@ -36,10 +31,10 @@ public function test_item_is_taxable(): void { $this->assertInstanceOf(Taxable::class, $this->item); $this->assertSame( - (new Currency($this->item->tax, $this->item->checkoutable->currency))->format(), - $this->item->getFormattedTax() + (new Currency($this->item->getTaxTotal(), $this->item->checkoutable->currency))->format(), + $this->item->getFormattedTaxTotal() ); - $this->assertSame($this->item->getFormattedTax(), $this->item->formattedTax); + $this->assertSame($this->item->getFormattedTaxTotal(), $this->item->formattedTaxTotal); } public function test_item_has_price_attribute(): void diff --git a/tests/Models/ProductTest.php b/tests/Models/ProductTest.php index 65554bff..8276fb15 100644 --- a/tests/Models/ProductTest.php +++ b/tests/Models/ProductTest.php @@ -35,7 +35,6 @@ public function test_product_has_many_orders_through_items(): void $item = Item::factory()->make([ 'price' => 100, - 'tax' => 0, 'quantity' => 3, 'name' => $this->product->name, ])->checkoutable()->associate($order); @@ -51,7 +50,6 @@ public function test_product_belongs_to_carts(): void $item = Item::factory()->make([ 'price' => 100, - 'tax' => 0, 'quantity' => 3, 'name' => $this->product->name, ])->checkoutable()->associate($cart); diff --git a/tests/Models/ShippingTest.php b/tests/Models/ShippingTest.php index c72d8956..9524b063 100644 --- a/tests/Models/ShippingTest.php +++ b/tests/Models/ShippingTest.php @@ -2,14 +2,12 @@ namespace Cone\Bazar\Tests\Models; -use Cone\Bazar\Interfaces\Taxable; use Cone\Bazar\Models\Address; use Cone\Bazar\Models\Cart; use Cone\Bazar\Models\Order; use Cone\Bazar\Models\Shipping; use Cone\Bazar\Support\Currency; use Cone\Bazar\Support\Facades\Shipping as ShippingManager; -use Cone\Bazar\Support\Facades\Tax; use Cone\Bazar\Tests\TestCase; use Cone\Bazar\Tests\User; @@ -25,10 +23,6 @@ public function setUp(): void { parent::setUp(); - Tax::register('fix-10%', function (Taxable $item) { - return $item->price * 0.1; - }); - $this->user = User::factory()->create(); $this->cart = Cart::factory()->create(); $this->shipping = Shipping::factory()->make(); @@ -75,29 +69,21 @@ public function test_shipping_has_address(): void $this->assertSame($address->id, $shipping->address->id); } - public function testt_can_calculate_calculateCost(): void + public function test_shipping_can_calculate_fee(): void { - $cost = $this->shipping->calculateCost(); - $this->assertSame($cost, $this->shipping->cost); + $fee = $this->shipping->calculateFee(); + $this->assertSame($fee, $this->shipping->fee); } - public function testt_is_taxable(): void + public function test_shipping_is_taxable(): void { - $this->shipping->calculateTax(); - - $this->assertInstanceOf(Taxable::class, $this->shipping); - $this->assertSame($this->shipping->price * 0.1, $this->shipping->tax); - $this->assertSame( - (new Currency($this->shipping->tax, $this->shipping->shippable->currency))->format(), - $this->shipping->getFormattedTax() - ); - $this->assertSame($this->shipping->getFormattedTax(), $this->shipping->formattedTax); + $this->assertSame($this->shipping->getFormattedTaxTotal(), $this->shipping->formattedTaxTotal); } public function testt_has_total_attribute(): void { $this->assertSame( - $this->shipping->cost + $this->shipping->tax, + $this->shipping->fee + $this->shipping->tax, $this->shipping->getTotal() ); $this->assertSame($this->shipping->getTotal(), $this->shipping->total); @@ -106,7 +92,7 @@ public function testt_has_total_attribute(): void $this->shipping->getFormattedTotal() ); $this->assertSame($this->shipping->getFormattedTotal(), $this->shipping->formattedTotal); - $this->assertSame($this->shipping->cost, $this->shipping->getSubtotal()); + $this->assertSame($this->shipping->fee, $this->shipping->getSubtotal()); $this->assertSame($this->shipping->getSubtotal(), $this->shipping->subtotal); $this->assertSame( (new Currency($this->shipping->subtotal, $this->shipping->shippable->currency))->format(), diff --git a/tests/Repositories/TaxTest.php b/tests/Repositories/TaxTest.php deleted file mode 100644 index d0b1d414..00000000 --- a/tests/Repositories/TaxTest.php +++ /dev/null @@ -1,76 +0,0 @@ -<?php - -namespace Cone\Bazar\Tests\Repositories; - -use Cone\Bazar\Interfaces\Tax as Contract; -use Cone\Bazar\Interfaces\Taxable; -use Cone\Bazar\Models\Cart; -use Cone\Bazar\Models\Product; -use Cone\Bazar\Models\Shipping; -use Cone\Bazar\Support\Facades\Tax; -use Cone\Bazar\Tests\TestCase; - -class TaxTest extends TestCase -{ - protected Cart $cart; - - public function setUp(): void - { - parent::setUp(); - - $this->cart = Cart::factory()->create(); - - Product::factory()->count(2)->create()->each(function ($product) { - $this->cart->items()->create([ - 'buyable_id' => $product->id, - 'buyable_type' => Product::class, - 'price' => $product->price, - 'tax' => 0, - 'quantity' => 1, - 'name' => $product->name, - ]); - }); - - Tax::register('custom-30', 30); - } - - public function test_tax_repository_calculates_taxes(): void - { - Tax::register('custom-object', new CustomTax); - Tax::register('custom-closure', function (Taxable $model) { - return $model instanceof Shipping ? 20 : 30; - }); - - $this->assertEquals(470, $this->cart->calculateTax()); - } - - public function test_tax_repository_removes_taxes(): void - { - Tax::remove('custom-30'); - - $this->assertEquals(0, $this->cart->calculateTax()); - } - - public function test_tax_repository_disables_taxes(): void - { - $this->assertEquals(90, $this->cart->calculateTax()); - - Tax::disable(); - - Tax::register('custom-10', 10); - - $this->assertEquals(90, $this->cart->calculateTax()); - - Tax::enable(); - - $this->assertEquals(120, $this->cart->calculateTax()); - } -} - -class CustomTax implements Contract -{ - public function __invoke(Taxable $model): float - { - return 100; - } -}