diff --git a/app/Http/Controllers/Accounting/Account/DeleteController.php b/app/Http/Controllers/Accounting/Account/DeleteController.php new file mode 100644 index 0000000..9a18436 --- /dev/null +++ b/app/Http/Controllers/Accounting/Account/DeleteController.php @@ -0,0 +1,24 @@ +delete($account); + return response()->json([ + "success" => true + ]); + } +} diff --git a/app/Http/Controllers/Accounting/Account/ListController.php b/app/Http/Controllers/Accounting/Account/ListController.php new file mode 100644 index 0000000..c9280e7 --- /dev/null +++ b/app/Http/Controllers/Accounting/Account/ListController.php @@ -0,0 +1,22 @@ +validated(); + $data = $repository->list($params); + return ListResource::collection($data); + } +} diff --git a/app/Http/Controllers/Accounting/Account/StoreController.php b/app/Http/Controllers/Accounting/Account/StoreController.php new file mode 100644 index 0000000..fe378d1 --- /dev/null +++ b/app/Http/Controllers/Accounting/Account/StoreController.php @@ -0,0 +1,22 @@ +validated(); + $data = $repository->create($params); + return RowResource::make($data); + } +} diff --git a/app/Http/Controllers/Accounting/Account/UpdateController.php b/app/Http/Controllers/Accounting/Account/UpdateController.php new file mode 100644 index 0000000..1211d04 --- /dev/null +++ b/app/Http/Controllers/Accounting/Account/UpdateController.php @@ -0,0 +1,23 @@ +validated(); + $data = $repository->update($account, $params); + return RowResource::make($data); + } +} diff --git a/app/Http/Requests/Accounting/Account/DeleteRequest.php b/app/Http/Requests/Accounting/Account/DeleteRequest.php new file mode 100644 index 0000000..970ac54 --- /dev/null +++ b/app/Http/Requests/Accounting/Account/DeleteRequest.php @@ -0,0 +1,27 @@ +user()->checkPermission("accounting.account:delete"); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + ]; + } +} diff --git a/app/Http/Requests/Accounting/Account/ListRequest.php b/app/Http/Requests/Accounting/Account/ListRequest.php new file mode 100644 index 0000000..b62e57e --- /dev/null +++ b/app/Http/Requests/Accounting/Account/ListRequest.php @@ -0,0 +1,39 @@ +user()->checkPermission("accounting.account:read"); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'limit' => 'nullable', + 'offset' => 'nullable', + 'search' => 'nullable', + + 'filter' => 'nullable|array', + 'filter.*.column' => 'required|in:name', + 'filter.*.operator' => 'nullable|in:eq,in', + 'filter.*.query' => 'required', + + 'sort' => 'nullable|array', + 'sort.column' => 'nullable|in:name', + 'sort.dir' => 'nullable', + ]; + } +} diff --git a/app/Http/Requests/Accounting/Account/StoreRequest.php b/app/Http/Requests/Accounting/Account/StoreRequest.php new file mode 100644 index 0000000..a2658a6 --- /dev/null +++ b/app/Http/Requests/Accounting/Account/StoreRequest.php @@ -0,0 +1,34 @@ +user()->checkPermission("accounting.account:create"); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string', + 'code' => 'required|string|unique:accounts', + 'sheet' => 'required|string', + 'type' => 'required|string', + 'category' => 'required|string', + 'subcategory' => 'nullable|string', + 'structure' => 'required|string', + ]; + } +} diff --git a/app/Http/Requests/Accounting/Account/UpdateRequest.php b/app/Http/Requests/Accounting/Account/UpdateRequest.php new file mode 100644 index 0000000..e0264c6 --- /dev/null +++ b/app/Http/Requests/Accounting/Account/UpdateRequest.php @@ -0,0 +1,34 @@ +user()->checkPermission("accounting.account:update"); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string', + 'code' => 'required|string|unique:accounts,code,'.$this->account->id, + 'sheet' => 'required|string', + 'type' => 'required|string', + 'category' => 'required|string', + 'subcategory' => 'nullable|string', + 'structure' => 'required|string', + ]; + } +} diff --git a/app/Http/Resources/Accounting/Account/ListResource.php b/app/Http/Resources/Accounting/Account/ListResource.php new file mode 100644 index 0000000..358d5e1 --- /dev/null +++ b/app/Http/Resources/Accounting/Account/ListResource.php @@ -0,0 +1,28 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->id, + "name" => $this->name, + "code" => $this->code, + "sheet" => $this->sheet, + "type" => $this->type, + "category" => $this->category, + "subcategory" => $this->subcategory, + "structure" => $this->structure + ]; + } +} diff --git a/app/Http/Resources/Accounting/Account/SimpleResource.php b/app/Http/Resources/Accounting/Account/SimpleResource.php new file mode 100644 index 0000000..a6c388b --- /dev/null +++ b/app/Http/Resources/Accounting/Account/SimpleResource.php @@ -0,0 +1,22 @@ + + */ + public function toArray(Request $request): array + { + return [ + "id" => $this->id, + "name" => $this->name, + ]; + } +} diff --git a/app/Models/Account.php b/app/Models/Account.php new file mode 100644 index 0000000..d33514a --- /dev/null +++ b/app/Models/Account.php @@ -0,0 +1,23 @@ +belongsToMany(Permission::class,"role_permission"); } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults(); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 3ace172..90ae061 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,9 +9,13 @@ use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; use Cache; +use Spatie\Activitylog\Traits\LogsActivity; +use Spatie\Activitylog\LogOptions; + class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; + use LogsActivity; /** * The attributes that are mass assignable. @@ -44,6 +48,11 @@ class User extends Authenticatable 'password' => 'hashed', ]; + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults(); + } + public function roles(){ return $this->belongsToMany(Role::class,'user_role')->with("permissions"); } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 55b12e0..c45d385 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -36,6 +36,11 @@ class RouteServiceProvider extends ServiceProvider ->namespace("{$this->apiNamespace}\Auth") ->group(base_path('routes/auth.php')); + Route::middleware('api') + ->prefix('accounting') + ->namespace("{$this->apiNamespace}\Accounting") + ->group(base_path('routes/accounting.php')); + Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); diff --git a/app/Repositories/Accounting/AccountRepository.php b/app/Repositories/Accounting/AccountRepository.php new file mode 100644 index 0000000..3fa04e8 --- /dev/null +++ b/app/Repositories/Accounting/AccountRepository.php @@ -0,0 +1,51 @@ +orderBy($sortColumn, $sortDir) + + ->when(@$params["filter"], function ($query) use ($params) { + foreach ($params["filter"] as $filter) { + $query->where($filter["column"], $filter["query"]); + } + }) + ->paginate($limit); + } + + public function create($params){ + + $params["password"] = "-"; + $model = Account::create($params); + if (@$params["permissions"]){ + $model->permissions()->sync($params["permissions"]); + } + return $model; + } + + public function update($model, $params){ + + $model->update($params); + if (@$params["permissions"]){ + $model->permissions()->sync($params["permissions"]); + } + return $model; + } + + public function delete($model){ + + $model->delete(); + } +} diff --git a/composer.json b/composer.json index 8a4cf9b..6b563bd 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,8 @@ "laravel/envoy": "^2.10", "laravel/framework": "^10.10", "laravel/sanctum": "^3.3", - "laravel/tinker": "^2.8" + "laravel/tinker": "^2.8", + "spatie/laravel-activitylog": "^4.10" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index ef98185..2f2de47 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "215e4584bd80d9667c89109f18cd6fbf", + "content-hash": "2feee7537e769f923d55b248c506fe92", "packages": [ { "name": "brick/math", @@ -3244,6 +3244,157 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "spatie/laravel-activitylog", + "version": "4.10.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/466f30f7245fe3a6e328ad5e6812bd43b4bddea5", + "reference": "466f30f7245fe3a6e328ad5e6812bd43b4bddea5", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.1" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-10T15:38:25+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.19.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", + "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-02-06T14:58:20+00:00" + }, { "name": "symfony/console", "version": "v6.4.17", diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php new file mode 100644 index 0000000..a4770d1 --- /dev/null +++ b/database/factories/AccountFactory.php @@ -0,0 +1,29 @@ + + */ +class AccountFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'code' => fake()->name(), + 'sheet' => fake()->randomElement(["balance","income"]), + 'type' => fake()->randomElement(["header","posting","begin-total","end-total"]), + 'category' => fake()->name(), + 'subcategory' => fake()->name(), + 'structure' => fake()->name(), + ]; + } +} diff --git a/database/migrations/2025_02_12_061947_create_accounts_table.php b/database/migrations/2025_02_12_061947_create_accounts_table.php new file mode 100644 index 0000000..7e6556c --- /dev/null +++ b/database/migrations/2025_02_12_061947_create_accounts_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('code')->unique(); + $table->string('name'); + $table->string('sheet'); // Balance/Income + $table->string('category'); // Assets, Equity, Liabilities, Income, Expense, Cost of Goods + $table->string('subcategory'); + $table->string('type'); // Header, Begin-Total, Posting, End-Total + $table->string('structure'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('accounts'); + } +}; diff --git a/database/migrations/2025_02_12_062740_create_ltree_extension.php b/database/migrations/2025_02_12_062740_create_ltree_extension.php new file mode 100644 index 0000000..c4f5245 --- /dev/null +++ b/database/migrations/2025_02_12_062740_create_ltree_extension.php @@ -0,0 +1,26 @@ + "Account", "prefix" => "account", "middleware" => "auth:sanctum"], function () { + Route::get('/', 'ListController')->middleware("auth:sanctum"); + Route::post('/', 'StoreController')->middleware("auth:sanctum"); + Route::post('/{account}', 'UpdateController')->middleware("auth:sanctum"); + Route::post('/{account}/delete', 'DeleteController')->middleware("auth:sanctum"); +}); diff --git a/tests/Feature/Accounting/Account/DeleteTest.php b/tests/Feature/Accounting/Account/DeleteTest.php new file mode 100644 index 0000000..bada065 --- /dev/null +++ b/tests/Feature/Accounting/Account/DeleteTest.php @@ -0,0 +1,42 @@ +first(); + $role = Role::factory()->create(); + $role->permissions()->attach($permission->id); + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + Sanctum::actingAs($user); + + $data = Account::factory()->create(); + + $response = $this->post('/accounting/account/'.$data->id.'/delete'); + + $response->assertStatus(200); + } +} diff --git a/tests/Feature/Accounting/Account/ListTest.php b/tests/Feature/Accounting/Account/ListTest.php new file mode 100644 index 0000000..a918e3b --- /dev/null +++ b/tests/Feature/Accounting/Account/ListTest.php @@ -0,0 +1,49 @@ +first(); + $role = Role::factory()->create(); + $role->permissions()->attach($permission->id); + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + Sanctum::actingAs($user); + $data = Account::factory()->create(); + + $response = $this->get('/accounting/account/'); + + $response->assertStatus(200); + $response->assertJson([ + "data" => [ + [ + "id" => $data->id, + "name" => $data->name + ] + ] + ]); + } +} diff --git a/tests/Feature/Accounting/Account/StoreTest.php b/tests/Feature/Accounting/Account/StoreTest.php new file mode 100644 index 0000000..14a7307 --- /dev/null +++ b/tests/Feature/Accounting/Account/StoreTest.php @@ -0,0 +1,89 @@ +first(); + $role = Role::factory()->create(); + $role->permissions()->attach($permission->id); + + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + Sanctum::actingAs($user); + + $response = $this->post('/accounting/account/',[ + "name" => "new role", + "code" => "code", + "sheet" => "sheet", + "structure" => "structure", + "type" => "type", + "category" => "category", + "subcategory" => "subcategory", + ]); + + $response->assertStatus(201); + $response->assertJson([ + "data" => [ + "name" => "new role" + ] + ]); + } + + + /** + * A basic feature test example. + */ + public function test_with_roles_success(): void + { + $permission = Permission::where("code","accounting.account:create")->first(); + $role = Role::factory()->create(); + $role->permissions()->attach($permission->id); + + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + $role2 = Role::factory()->create(); + + + Sanctum::actingAs($user); + + $response = $this->post('/accounting/account/',[ + "name" => "new role", + "code" => "code", + "sheet" => "sheet", + "structure" => "structure", + "type" => "type", + "category" => "category", + "subcategory" => "subcategory", + ]); + + $response->assertStatus(201); + $response->assertJson([ + "data" => [ + "name" => "new role" + ] + ]); + } +} diff --git a/tests/Feature/Accounting/Account/UpdateTest.php b/tests/Feature/Accounting/Account/UpdateTest.php new file mode 100644 index 0000000..32cf4dc --- /dev/null +++ b/tests/Feature/Accounting/Account/UpdateTest.php @@ -0,0 +1,92 @@ +first(); + $role = Role::factory()->create(); + $role->permissions()->attach($permission->id); + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + Sanctum::actingAs($user); + + $data = Account::factory()->create(); + + $response = $this->post('/accounting/account/'.$data->id,[ + "name" => "update role", + "code" => "code", + "sheet" => "sheet", + "structure" => "structure", + "type" => "type", + "category" => "category", + "subcategory" => "subcategory", + ]); + + $response->assertStatus(200); + $response->assertJson([ + "data" => [ + "name" => "update role", + ] + ]); + } + + + /** + * A basic feature test example. + */ + public function test_with_roles_success(): void + { + $permission = Permission::where("code","accounting.account:update")->first(); + $role = Role::factory()->create(); + $role->permissions()->attach($permission->id); + + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + Sanctum::actingAs($user); + + $data = Account::factory()->create(); + $role2 = Role::factory()->create(); + + $response = $this->post('/accounting/account/'.$data->id,[ + + "name" => "update role", + "code" => "code", + "sheet" => "sheet", + "structure" => "structure", + "type" => "type", + "category" => "category", + "subcategory" => "subcategory", + ]); + + $response->assertStatus(200); + $response->assertJson([ + "data" => [ + "name" => "update role" + ] + ]); + } +}