Skip to content

Commit

Permalink
denormalize amounts in invoice table
Browse files Browse the repository at this point in the history
  • Loading branch information
QuentinGab committed Nov 29, 2023
1 parent ac619d9 commit e6180dc
Show file tree
Hide file tree
Showing 11 changed files with 199 additions and 43 deletions.
40 changes: 32 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,15 @@
},
"require-dev": {
"laravel/pint": "^1.0",
"nunomaduro/collision": "^7.0",
"nunomaduro/collision": "^7.8",
"nunomaduro/larastan": "^2.0.1",
"orchestra/testbench": "^8.0",
"orchestra/testbench": "^8.8",
"pestphp/pest": "^2.0",
"pestphp/pest-plugin-arch": "^2.0",
"pestphp/pest-plugin-laravel": "^2.0",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^10.0",
"spatie/laravel-ray": "^1.26"
"phpstan/phpstan-phpunit": "^1.0"
},
"autoload": {
"psr-4": {
Expand All @@ -44,14 +43,39 @@
},
"autoload-dev": {
"psr-4": {
"Finller\\Invoice\\Tests\\": "tests"
"Finller\\Invoice\\Tests\\": "tests",
"Workbench\\App\\": "workbench/app/",
"Workbench\\Database\\Factories\\": "workbench/database/factories/",
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
}
},
"scripts": {
"post-autoload-dump": [
"@clear",
"@prepare",
"@composer run prepare"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi",
"start": [
"Composer\\Config::disableProcessTimeout",
"@composer run build",
"@php vendor/bin/testbench serve"
],
"analyse": "vendor/bin/phpstan analyse",
"test": "vendor/bin/pest",
"test-coverage": "vendor/bin/pest --coverage",
"format": "vendor/bin/pint"
"format": "vendor/bin/pint",
"serve": [
"Composer\\Config::disableProcessTimeout",
"@build",
"@php vendor/bin/testbench serve"
],
"lint": [
"@php vendor/bin/pint",
"@php vendor/bin/phpstan analyse"
]
},
"config": {
"sort-packages": true,
Expand All @@ -72,4 +96,4 @@
},
"minimum-stability": "dev",
"prefer-stable": true
}
}
10 changes: 6 additions & 4 deletions database/factories/InvoiceItemFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Finller\Invoice\Database\Factories;

use Brick\Money\Money;
use Finller\Invoice\InvoiceItem;
use Illuminate\Database\Eloquent\Factories\Factory;

Expand All @@ -11,17 +12,18 @@ class InvoiceItemFactory extends Factory

public function definition()
{
$price = fake()->numberBetween(100.00, 100000.00);
$price = Money::ofMinor(fake()->numberBetween(1000, 100000), config('invoices.default_currency'));
$unit_tax = Money::ofMinor(fake()->numberBetween(0, $price->getAmount()->toFloat()), config('invoices.default_currency'));

$useTaxPercentage = fake()->boolean();

return [
'label' => fake()->sentence(),
'description' => fake()->sentence(),
'unit_price' => $price,
'unit_tax' => ! $useTaxPercentage ? fake()->numberBetween(0, $price) : null,
'unit_price' => $price->getMinorAmount()->toInt(),
'currency' => $price->getCurrency()->getCurrencyCode(),
'unit_tax' => ! $useTaxPercentage ? $unit_tax : null,
'tax_percentage' => $useTaxPercentage ? fake()->numberBetween(0, 100) : null,
'currency' => fake()->currencyCode(),
'quantity' => fake()->numberBetween(1, 10),
];
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->bigInteger('subtotal_amount')->nullable();
$table->bigInteger('discount_amount')->nullable();
$table->bigInteger('tax_amount')->nullable();
$table->bigInteger('total_amount')->nullable();
$table->string('currency')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('invoices', function (Blueprint $table) {
$table->dropColumn('subtotal_amount');
$table->dropColumn('discount_amount');
$table->dropColumn('tax_amount');
$table->dropColumn('total_amount');
$table->dropColumn('currency');
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ return new class extends Migration
{
Schema::table('invoices', function (Blueprint $table) {
$table->string('type')->default('invoice');
$table->foreignId('parent_id')->nullable()
$table->foreignId('parent_id')->nullable();
});
}

Expand Down
44 changes: 44 additions & 0 deletions src/Commands/DenormalizeInvoicesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Finller\Invoice;

use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;

class DenormalizeInvoicesCommand extends Command
{
public $signature = 'invoices:denormalize {ids?*}';

public $description = 'Denormalize amount, tax and discounts to the invoice table';

public function handle(): int
{
$ids = $this->argument('ids');

$model = config('invoices.model_invoice');

/** @var Builder $query */
$query = $model::query();
$query
->with(['items'])
->when($ids, fn (Builder $q) => $q->whereIn('id', $ids));

/** @var int */
$total = $query->count();

$bar = $this->output->createProgressBar($total);

$query
->chunk(200, function (Collection $invoices) use ($bar) {
$invoices->each(function (Invoice $invoice) use ($bar) {
$invoice->denormalize()->saveQuietly();
$bar->advance();
});
});

$bar->finish();

return self::SUCCESS;
}
}
29 changes: 28 additions & 1 deletion src/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

namespace Finller\Invoice;

use Brick\Money\Money;
use Carbon\Carbon;
use Finller\Invoice\Casts\Discounts;
use Finller\Money\MoneyCast;
use Illuminate\Contracts\Mail\Attachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\ArrayObject;
Expand All @@ -24,7 +26,7 @@
* @property ?Invoice $credit
* @property InvoiceType $type
* @property string $serial_number
* @property ArrayObject $serial_number_details
* @property ?ArrayObject $serial_number_details
* @property string $description
* @property ?ArrayObject $seller_information
* @property ?ArrayObject $buyer_information
Expand All @@ -47,6 +49,11 @@
* @property Carbon $updated_at
* @property null|InvoiceDiscount[] $discounts
* @property ?ArrayObject $metadata
* @property ?Money $subtotal_amount
* @property ?Money $discount_amount
* @property ?Money $tax_amount
* @property ?Money $total_amount
* @property ?string $currency
*/
class Invoice extends Model implements Attachable
{
Expand All @@ -68,6 +75,10 @@ class Invoice extends Model implements Attachable
'metadata' => AsArrayObject::class,
'discounts' => Discounts::class,
'serial_number_details' => AsArrayObject::class,
'total_amount' => MoneyCast::class.':currency',
'subtotal_amount' => MoneyCast::class.':currency',
'discount' => MoneyCast::class.':currency',
'tax' => MoneyCast::class.':currency',
];

public static function booted()
Expand Down Expand Up @@ -283,6 +294,22 @@ public function getDiscounts(): ?array
return $this->discounts;
}

/**
* Denormalize amounts computed from items to the invoice table
* Allowing easier query
*/
public function denormalize(): static
{
$pdfInvoice = $this->toPdfInvoice();
$this->currency = $pdfInvoice->getCurrency();
$this->subtotal_amount = $pdfInvoice->subTotalAmount();
$this->discount_amount = $pdfInvoice->totalDiscountAmount();
$this->tax_amount = $pdfInvoice->totalTaxAmount();
$this->total_amount = $pdfInvoice->totalAmount();

return $this;
}

public function scopePaid(Builder $query): Builder
{
return $query->where('state', InvoiceState::Paid);
Expand Down
3 changes: 2 additions & 1 deletion src/InvoiceServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function configurePackage(Package $package): void
->hasMigration('create_invoices_table')
->hasMigration('create_invoice_items_table')
->hasMigration('add_discounts_column_to_invoices_table')
->hasMigration('add_type_column_to_invoices_table');
->hasMigration('add_type_column_to_invoices_table')
->hasMigration('add_denormalized_columns_to_invoices_table');
}
}
40 changes: 12 additions & 28 deletions src/PdfInvoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ public function getFilename(): string
return $this->filename ?? $this->generateFilename();
}

public function getCurrency(): string
{
/** @var ?PdfInvoiceItem $firstItem */
$firstItem = Arr::first($this->items);

return $firstItem?->currency ?? config('invoices.default_currency');
}

public function getLogo(): string
{
$type = pathinfo($this->logo, PATHINFO_EXTENSION);
Expand All @@ -62,42 +70,26 @@ public function getLogo(): string
*/
public function subTotalAmount(): Money
{
if (empty($this->items)) {
return Money::ofMinor(0, config('invoices.default_currency'));
}

$firstItem = Arr::first($this->items);

$currency = $firstItem->currency;

return array_reduce(
$this->items,
fn (Money $total, PdfInvoiceItem $item) => $total->plus($item->subTotalAmount()),
Money::of(0, $currency)
Money::of(0, $this->getCurrency())
);
}

public function totalTaxAmount(): Money
{
if (empty($this->items)) {
return Money::ofMinor(0, config('invoices.default_currency'));
}

$firstItem = Arr::first($this->items);

$currency = $firstItem->currency;

return array_reduce(
$this->items,
fn (Money $total, PdfInvoiceItem $item) => $total->plus($item->totalTaxAmount()),
Money::of(0, $currency)
Money::of(0, $this->getCurrency())
);
}

public function totalDiscountAmount(): Money
{
if (! $this->discounts) {
return Money::of(0, $this->subTotalAmount()->getCurrency());
return Money::of(0, $this->getCurrency());
}

$subtotal = $this->subTotalAmount();
Expand All @@ -109,18 +101,10 @@ public function totalDiscountAmount(): Money

public function totalAmount(): Money
{
if (empty($this->items)) {
return Money::ofMinor(0, config('invoices.default_currency'));
}

$firstItem = Arr::first($this->items);

$currency = $firstItem->currency;

$total = array_reduce(
$this->items,
fn (Money $total, PdfInvoiceItem $item) => $total->plus($item->totalAmount()),
Money::of(0, $currency)
Money::of(0, $this->getCurrency())
);

return $total->minus($this->totalDiscountAmount());
Expand Down
5 changes: 5 additions & 0 deletions tests/ArchTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

it('will not use debugging functions')
->expect(['dd', 'dump', 'ray'])
->each->not->toBeUsed();
24 changes: 24 additions & 0 deletions tests/Feature/InvoiceTest.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php

use Finller\Invoice\Invoice;
use Finller\Invoice\InvoiceItem;

it('can create and generate unique serial numbers', function () {
$prefix = 'INV';
Expand Down Expand Up @@ -72,3 +73,26 @@
'count' => 1,
]);
});

it('denormalize amounts in invoice', function () {
/** @var Invoice */
$invoice = Invoice::factory()->make();
$invoice->save();

$invoice->items()->saveMany(InvoiceItem::factory(2)->make());

$invoice->denormalize()->save();
$pdfInvoice = $invoice->toPdfInvoice();

expect($invoice->subtotal_amount->getAmount()->toFloat())
->toEqual($pdfInvoice->subTotalAmount()->getAmount()->toFloat());

expect($invoice->discount_amount->getAmount()->toFloat())
->toEqual($pdfInvoice->totalDiscountAmount()->getAmount()->toFloat());

expect($invoice->tax_amount->getAmount()->toFloat())
->toEqual($pdfInvoice->totalTaxAmount()->getAmount()->toFloat());

expect($invoice->total_amount->getAmount()->toFloat())
->toEqual($pdfInvoice->totalAmount()->getAmount()->toFloat());
});
Loading

0 comments on commit e6180dc

Please sign in to comment.