diff --git a/README.md b/README.md index 416fa99..08c2585 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,46 @@ class MyProjectableModel extends Model ]; } ``` +If you want to use a different date field from your Model instead of created_at then do the following : +1) Make sure the field is casted to Carbon + +```php +use App\Models\Projections\MyProjection; +use TimothePearce\TimeSeries\Projectable; + +class MyProjectableModel extends Model +{ + use Projectable; + + protected $casts = [ + 'other_date_time' => 'datetime:Y-m-d H:00', + ]; + + protected array $projections = [ + MyProjection::class, + ]; +} +``` +2) Add the dateColumn field in your Projection +```php +namespace App\Models\Projections; + +use Illuminate\Database\Eloquent\Model; +use TimothePearce\TimeSeries\Contracts\ProjectionContract; +use TimothePearce\TimeSeries\Models\Projection; + +class MyProjection extends Projection implements ProjectionContract +{ + /** + * The projected periods. + */ + public array $periods = []; + + public string $dateColumn = 'other_date_time'; +.... + +``` + ### Implement a Projection diff --git a/composer.json b/composer.json index b39c9ec..80cbd15 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "php": "^8.0" }, "require-dev": { + "brianium/paratest": "^6.11", "nunomaduro/collision": "^6.0", "nunomaduro/larastan": "^2.0.1", "orchestra/testbench": "^7.0", diff --git a/src/Projector.php b/src/Projector.php index 51bfc5d..362be9c 100644 --- a/src/Projector.php +++ b/src/Projector.php @@ -9,11 +9,17 @@ class Projector { + public string $dateColumn = 'created_at'; + public function __construct( protected Model $projectedModel, protected string $projectionName, protected string $eventName, ) { + $projection = (new $this->projectionName()); + if (isset($projection->dateColumn)) { + $this->dateColumn = $projection->dateColumn; + } } /** @@ -104,7 +110,7 @@ private function findProjection(string $period): Projection|null ['projection_name', $this->projectionName], ['key', $this->hasKey() ? $this->key() : null], ['period', $period], - ['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->created_at, $period)], + ['start_date', app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->dateColumn}, $period)], ]); } @@ -117,7 +123,7 @@ private function createProjection(string $period): void 'projection_name' => $this->projectionName, 'key' => $this->hasKey() ? $this->key() : null, 'period' => $period, - 'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->created_at, $period), + 'start_date' => app(TimeSeries::class)->resolveFloorDate($this->projectedModel->{$this->dateColumn}, $period), 'content' => $this->mergeProjectedContent((new $this->projectionName())->defaultContent(), $period), ]); } @@ -193,7 +199,7 @@ private function resolveCallableMethod(array $content, string $period): array */ private function resolveStartDate(string $periodType, int $quantity): Carbon { - $startDate = $this->projectedModel->created_at->floorUnit($periodType, $quantity); + $startDate = $this->projectedModel->{$this->dateColumn}->floorUnit($periodType, $quantity); if (in_array($periodType, ['week', 'weeks'])) { $startDate->startOfWeek(config('time-series.beginning_of_the_week')); diff --git a/tests/Models/Projections/TableReservationPerDiningDayProjection.php b/tests/Models/Projections/TableReservationPerDiningDayProjection.php new file mode 100644 index 0000000..b0c93d4 --- /dev/null +++ b/tests/Models/Projections/TableReservationPerDiningDayProjection.php @@ -0,0 +1,50 @@ + 0, + 'number_reservations' => 0, + ]; + } + + /** + * Computes the content when a projectable model is created. + */ + public function projectableCreated(array $content, Model $model): array + { + return [ + 'total_people' => $content['total_people'] += $model->number_people, + 'number_reservations' => $content['number_reservations'] + 1, + ]; + } + + /** + * Computes the content when a projectable model is deleted. + */ + public function projectableDeleted(array $content, Model $model): array + { + return [ + 'total_people' => $content['total_people'] -= $model->number_people, + 'number_reservations' => $content['number_reservations'] - 1, + ]; + } +} diff --git a/tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php b/tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php new file mode 100644 index 0000000..3720d75 --- /dev/null +++ b/tests/Models/Projections/TableReservationPerDiningDayProjectionWithKey.php @@ -0,0 +1,18 @@ +table_id; + } +} diff --git a/tests/Models/Projections/TableReservationPerMadeDayProjection.php b/tests/Models/Projections/TableReservationPerMadeDayProjection.php new file mode 100644 index 0000000..5cf54c1 --- /dev/null +++ b/tests/Models/Projections/TableReservationPerMadeDayProjection.php @@ -0,0 +1,14 @@ +dateColumn = 'reservation_made_date'; + } +} diff --git a/tests/Models/TableReservation.php b/tests/Models/TableReservation.php new file mode 100644 index 0000000..9f1190e --- /dev/null +++ b/tests/Models/TableReservation.php @@ -0,0 +1,29 @@ + 'datetime:Y-m-d', + 'reservation_made_date' => 'datetime:Y-m-d H:00', + ]; + /** + * The projections list. + */ + protected array $projections = [ + TableReservationPerMadeDayProjection::class, + TableReservationPerDiningDayProjection::class, + ]; +} diff --git a/tests/ProjectionWithConfigurableDateTest.php b/tests/ProjectionWithConfigurableDateTest.php new file mode 100644 index 0000000..9fd8ada --- /dev/null +++ b/tests/ProjectionWithConfigurableDateTest.php @@ -0,0 +1,187 @@ +travelTo(Carbon::today()); + } + + /** @test */ + public function it_gets_a_custom_collection() + { + TableReservation::factory()->count(2)->create(); + + $collection = Projection::all(); + + $this->assertInstanceOf(ProjectionCollection::class, $collection); + } + + /** @test */ + public function it_has_a_relationship_with_the_model() + { + TableReservation::factory()->create(); + $projection = Projection::first(); + + $this->assertNotEmpty($projection->from(TableReservation::class)->get()); + } + + /** @test */ + public function it_gets_the_projections_from_projection_name() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); + + $numberOfProjections = Projection::name(TableReservationPerDiningDayProjection::class)->count(); + + $this->assertEquals(1, $numberOfProjections); + } + + /** @test */ + public function it_gets_the_projections_from_a_single_period() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 + $this->travel(5)->days(); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 2 + + $numberOfProjections = Projection::period('1 day')->count(); + + $this->assertEquals(2, $numberOfProjections); + } + + /** @test */ + public function it_raises_an_exception_when_using_the_between_scope_without_a_period() + { + $this->expectException(MissingProjectionNameException::class); + + Projection::between(now()->subMinute(), now()); + } + + /** @test */ + public function it_raises_an_exception_when_using_the_between_scope_without_the_projection_name() + { + $this->expectException(MissingProjectionPeriodException::class); + + Projection::name(TableReservationPerDiningDayProjection::class)->between(now()->subMinute(), now()); + } + + /** @test */ + public function it_gets_the_projections_between_the_given_dates_for_made_date() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be excluded + $this->travel(5)->days(); + $tablereservation = $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be included + $this->travel(5)->days(); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerMadeDayProjection::class]); // 1 // Should be excluded + + $this->travelBack(); + + $betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(5), + Carbon::today()->addDays(10) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals($betweenProjections->first()->id, $tablereservation->firstProjection(TableReservationPerMadeDayProjection::class)->id); + $this->assertEquals($betweenProjections->first()->start_date, Carbon::today()->addDays(5)); + + } + + /** @test */ + public function it_gets_the_projections_between_the_given_dates_for_dining_date() + { + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be excluded + $this->travel(5)->days(); + $tablereservation = $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be included + $this->travel(5)->days(); + $this->createModelWithProjections(TableReservation::class, [TableReservationPerDiningDayProjection::class]); // 1 // Should be excluded + + $this->travelBack(); + + /* Here we test based on the dining date of the reservation and it should be made_date + 10 days */ + $betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(15), + Carbon::today()->addDays(20) + )->get(); + + $this->assertCount(1, $betweenProjections); + $this->assertEquals($betweenProjections->first()->id, $tablereservation->firstProjection(TableReservationPerDiningDayProjection::class)->id); + $this->assertEquals($betweenProjections->first()->start_date, Carbon::today()->addDays(15)); // dining_date is set 10 day from creation date + } + + /** @test */ + public function it_gets_the_projections_between_the_given_dates_for_all_projections() + { + $tablereservation1 = TableReservation::factory()->create(); + // 1 made_date = created_at = now(), reservation_date = now()+10 days, total_people = 2, number_reservation = 1 + $this->travel(15)->minutes(); + $tablereservation2 = TableReservation::factory()->create(); + // 2 made_date = created_at = now(), reservation_date = now()+10, total_people = 2+2, number_reservation = 1+1 + $this->travel(2)->days(); + $tablereservation3 = TableReservation::factory()->create(); + // 3 made_date = created_at = now()+2, reservation_date = now()+2+10, total_people = 2, number_reservation = 1 + + $this->travelBack(); // reset the Carbon:date back to today + + /* Here we test based on the dining date of the reservation and it should be made_date + 10 days */ + $betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(10), + Carbon::today()->addDays(11) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(4, $betweenProjections->first()->content['total_people']); + $this->assertEquals(2, $betweenProjections->first()->content['number_reservations']); + + $betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class) + ->period('1 day') + ->between( + Carbon::today(), + Carbon::today()->addDays(2) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(4, $betweenProjections->first()->content['total_people']); + $this->assertEquals(2, $betweenProjections->first()->content['number_reservations']); + + $betweenProjections = Projection::name(TableReservationPerDiningDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(12), + Carbon::today()->addDays(13) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(2, $betweenProjections->first()->content['total_people']); + $this->assertEquals(1, $betweenProjections->first()->content['number_reservations']); + + $betweenProjections = Projection::name(TableReservationPerMadeDayProjection::class) + ->period('1 day') + ->between( + Carbon::today()->addDays(2), + Carbon::today()->addDays(3) + )->get(); + $this->assertCount(1, $betweenProjections); + $this->assertEquals(2, $betweenProjections->first()->content['total_people']); + $this->assertEquals(1, $betweenProjections->first()->content['number_reservations']); + } +} diff --git a/tests/database/factories/TableReservationFactory.php b/tests/database/factories/TableReservationFactory.php new file mode 100644 index 0000000..4d80290 --- /dev/null +++ b/tests/database/factories/TableReservationFactory.php @@ -0,0 +1,24 @@ + 1, //$this->random_int(1, 10), + 'customer_name' => $this->faker->name, + 'reservation_date' => today()->addDays(10)->format('Y-m-d'), + 'reservation_made_date' => today()->format('Y-m-d H:00'), + 'number_people' => 2, //$this->random_int(1, 10), + ]; + } +} + + diff --git a/tests/database/migrations/create_tablereservations_table.php b/tests/database/migrations/create_tablereservations_table.php new file mode 100644 index 0000000..3db582b --- /dev/null +++ b/tests/database/migrations/create_tablereservations_table.php @@ -0,0 +1,26 @@ +id(); + $table->integer('table_id'); + $table->string('customer_name'); + $table->dateTime('reservation_date'); + $table->dateTime('reservation_made_date'); + $table->integer('number_people'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down() + { + Schema::dropIfExists('table_reservations'); + } +};