Skip to content

Commit

Permalink
feature: add gift domain
Browse files Browse the repository at this point in the history
  • Loading branch information
mascam97 committed Jan 13, 2024
1 parent 5e43b71 commit ea37be1
Show file tree
Hide file tree
Showing 14 changed files with 417 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ sail artisan scribe:generate
- [Queueable actions in Laravel](https://github.com/spatie/laravel-queueable-action)
- [Laravel Model State](https://spatie.be/docs/laravel-model-states/v2/01-introduction) - Advanced state support for Laravel models
- [PEST PHP](https://pestphp.com/) - An elegant PHP Testing Framework
- [MoneyPHP](https://moneyphp.org/) - PHP implementation of Fowler's Money pattern.
- [Scribe](https://scribe.knuckles.wtf/laravel/) - Generate API documentation for humans from your Laravel codebase.

### Authors
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"laravel/vapor-core": "^2.33",
"league/flysystem-aws-s3-v3": "3.0",
"maatwebsite/excel": "^3.1",
"moneyphp/money": "^4.3",
"rebing/graphql-laravel": "^9.1",
"spatie/laravel-activitylog": "^4.7",
"spatie/laravel-data": "^2.1",
Expand Down
90 changes: 89 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions database/factories/DBGiftFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Database\Factories;

use Domain\Gifts\Models\Gift;
use Illuminate\Database\Eloquent\Factories\Factory;

class DBGiftFactory extends Factory
{
protected $model = Gift::class;

public function definition(): array
{
return [
'note' => $this->faker->text,
'amount' => $this->faker->numberBetween(1, 1000),
'currency' => 'USD',
];
}
}
32 changes: 32 additions & 0 deletions database/migrations/2023_12_18_013819_create_gifts_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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::create('gifts', function (Blueprint $table) {
$table->id();
$table->string('note', 255)->nullable();
$table->bigInteger('amount');
$table->char('currency', 3);
$table->foreignId('user_id')->constrained('users');
$table->foreignId('sender_user_id')->constrained('users');
// TODO: Evaluate if add or not pocket_id field, it can be gotten from the user now
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('gifts');
}
};
45 changes: 45 additions & 0 deletions src/Domain/Gifts/Actions/StoreGiftAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace Domain\Gifts\Actions;

use Domain\Gifts\Data\StoreGiftData;
use Domain\Gifts\Models\Gift;
use Domain\Pockets\Models\Pocket;
use Domain\Users\Models\User;
use Money\Currency;
use Money\Money;

class StoreGiftAction
{
public function __invoke(StoreGiftData $data, User $senderUser, User $user): Gift
{
/** @var Pocket $userPocket */
$userPocket = Pocket::query()->whereId($user->pocket_id)
->select(['id', 'balance', 'currency'])
->first();

// TODO: Throw an error when there is no user pocket

// TODO: Support conversion between currencies
if ($userPocket->currency !== $data->currency) {
// TODO: Create a custom exception to be handed in the controller
throw new \DomainException('User pocket currency does not match gift currency');
}

$userMoney = new Money($userPocket->balance, new Currency($userPocket->currency)); /* @phpstan-ignore-line */
$giftMoney = new Money($data->amount, new Currency($data->currency)); /* @phpstan-ignore-line */

$userPocket->balance = (int) $userMoney->add($giftMoney)->getAmount();
$userPocket->update();

$gift = new Gift();
$gift->note = $data->note;
$gift->amount = $data->amount;
$gift->currency = $data->currency;
$gift->senderUser()->associate($senderUser);
$gift->user()->associate($user);
$gift->save();

return $gift;
}
}
26 changes: 26 additions & 0 deletions src/Domain/Gifts/Data/StoreGiftData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Domain\Gifts\Data;

use Spatie\LaravelData\Data;

class StoreGiftData extends Data
{
public function __construct(
public ?string $note,
public int $amount,
public string $currency,
) {
}

public static function rules(): array
{
return [
'note' => ['required', 'string', 'min:3', 'max:255'],
// TODO: Add a logic about payment method, where the money is supposed to come from
'amount' => ['required', 'integer', 'min:100'],
// TODO: Add validated currency rule
'currency' => ['required', 'string'],
];
}
}
57 changes: 57 additions & 0 deletions src/Domain/Gifts/Models/Gift.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Domain\Gifts\Models;

use Database\Factories\DBGiftFactory;
use Domain\Quotes\QueryBuilders\QuoteQueryBuilder;
use Domain\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;

/**
* @property-read int $id
*
* @property ?string $note
* @property int $amount
* @property string $currency
* @property int $user_id
* @property int $sender_user_id
* @property Carbon $created_at
* @property Carbon $updated_at
*
* @property User $user
* @property User $senderUser
*
* @method static DBGiftFactory factory(...$parameters)
* @method static QuoteQueryBuilder query()
*/
class Gift extends Model
{
use HasFactory;

public function newEloquentBuilder($query): QuoteQueryBuilder
{
return new QuoteQueryBuilder($query);
}

public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

public function senderUser(): BelongsTo
{
return $this->belongsTo(User::class, 'sender_user_id');
}

/**
* Create a new factory instance for the model.
*/
protected static function newFactory(): Factory
{
return DBGiftFactory::new();
}
}
30 changes: 30 additions & 0 deletions src/Domain/Gifts/QueryBuilders/GiftQueryBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Domain\Gifts\QueryBuilders;

use Domain\Gifts\Models\Gift;
use Domain\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;

/**
* @method select($columns = ['*'])
* @method count()
* @method Gift firstOrFail($columns = ['*'])
*/
class GiftQueryBuilder extends Builder
{
public function whereId(int $id): self
{
return $this->where('id', $id);
}

public function whereUser(User $user): self
{
return $this->where('user_id', $user->getKey());
}

public function whereSenderUser(User $user): self
{
return $this->where('sender_user_id', $user->getKey());
}
}
16 changes: 14 additions & 2 deletions src/Domain/Users/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Domain\Users\Models;

use Database\Factories\DBUserFactory;
use Domain\Gifts\Models\Gift;
use Domain\Pockets\Models\Pocket;
use Domain\Quotes\Models\Quote;
use Domain\Rating\Contracts\Rates;
Expand All @@ -14,7 +15,6 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
Expand Down Expand Up @@ -42,7 +42,9 @@
* @property ?int $roles_count
*
* @property-read HasMany $quotes
* @property-read HasOne $pocket
* @property-read HasMany $gifts
* @property-read HasMany $sentGifts
* @property-read Pocket $pocket
*
* @method static DBUserFactory factory(...$parameters)
* @method static UserQueryBuilder query()
Expand Down Expand Up @@ -90,6 +92,16 @@ public function pocket(): BelongsTo
return $this->belongsTo(Pocket::class);
}

public function gifts(): HasMany
{
return $this->hasMany(Gift::class);
}

public function sentGifts(): HasMany
{
return $this->hasMany(Gift::class, 'sender_user_id');
}

/**
* Create a new factory instance for the model.
*/
Expand Down
2 changes: 1 addition & 1 deletion tests/Feature/ApiAdmin/Users/IndexUserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@
fn ($query) => $query->toContain('select `roles`.*, `role_has_permissions`.`permission_id` as `pivot_permission_id`, `role_has_permissions`.`role_id` as `pivot_role_id` from `roles` inner join `role_has_permissions` on `roles`.`id` = `role_has_permissions`.`role_id` where `role_has_permissions`.`permission_id` in'),
fn ($query) => $query->toBe('select `permissions`.*, `model_has_permissions`.`model_id` as `pivot_model_id`, `model_has_permissions`.`permission_id` as `pivot_permission_id`, `model_has_permissions`.`model_type` as `pivot_model_type` from `permissions` inner join `model_has_permissions` on `permissions`.`id` = `model_has_permissions`.`permission_id` where `model_has_permissions`.`model_id` = ? and `model_has_permissions`.`model_type` = ?'),
fn ($query) => $query->toBe('select count(*) as aggregate from `users` where `users`.`deleted_at` is null'),
fn ($query) => $query->toBe('select `id`, `name`, `email`, `deleted_at`, `created_at`, `updated_at` from `users` where `users`.`deleted_at` is null order by `created_at` asc limit 20 offset 0'),
fn ($query) => $query->toBe('select `id`, `name`, `email`, `pocket_id`, `deleted_at`, `created_at`, `updated_at` from `users` where `users`.`deleted_at` is null order by `created_at` asc limit 20 offset 0'),
);

DB::disableQueryLog();
Expand Down
Loading

0 comments on commit ea37be1

Please sign in to comment.