Browse Source

feat: introduce Academic domain with models, policies, DTOs, and actions to support curriculum, calendar, and intake management

master
Syarif Ubaidillah 2 months ago
parent
commit
b2e90078bb
  1. 75
      README.md
  2. 2
      app/Console/Commands/DomainNewCommand.php
  3. 21
      app/Domains/Academic/Actions/Calendar/InitializeAcademicTerm.php
  4. 17
      app/Domains/Academic/Actions/Calendar/ModifyTerm.php
  5. 17
      app/Domains/Academic/Actions/Calendar/ModifyTermDate.php
  6. 17
      app/Domains/Academic/Actions/Curriculum/AdjustStudyProgramDetails.php
  7. 18
      app/Domains/Academic/Actions/Curriculum/EstablishFaculty.php
  8. 17
      app/Domains/Academic/Actions/Curriculum/ModifyFacultyDetails.php
  9. 21
      app/Domains/Academic/Actions/Curriculum/RegisterStudyProgram.php
  10. 17
      app/Domains/Academic/DTOs/Calendar/InitializeAcademicTermDTO.php
  11. 13
      app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php
  12. 13
      app/Domains/Academic/DTOs/Calendar/ModifyTermDateDTO.php
  13. 13
      app/Domains/Academic/DTOs/Curriculum/AdjustStudyProgramDTO.php
  14. 12
      app/Domains/Academic/DTOs/Curriculum/EstablishFacultyDTO.php
  15. 11
      app/Domains/Academic/DTOs/Curriculum/ModifyFacultyDetailsDTO.php
  16. 17
      app/Domains/Academic/DTOs/Curriculum/RegisterStudyProgramDTO.php
  17. 35
      app/Domains/Academic/Models/Faculty.php
  18. 48
      app/Domains/Academic/Models/StudyProgram.php
  19. 30
      app/Domains/Academic/Models/Term.php
  20. 49
      app/Domains/Academic/Policies/FacultyPolicy.php
  21. 49
      app/Domains/Academic/Policies/StudyProgramPolicy.php
  22. 49
      app/Domains/Academic/Policies/TermPolicy.php
  23. 23
      app/Domains/Academic/Providers/AcademicServiceProvider.php
  24. 54
      app/Domains/Academic/Providers/RelationshipServiceProvider.php
  25. 21
      app/Domains/Account/Enums/GenderOption.php
  26. 17
      app/Domains/Admission/Actions/AdmissionPricing/DefineFeeType.php
  27. 11
      app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php
  28. 19
      app/Domains/Admission/Actions/IntakeScheduling/AdjustAdmissionScheduleDuration.php
  29. 22
      app/Domains/Admission/Actions/IntakeScheduling/PublishAdmissionSchedule.php
  30. 17
      app/Domains/Admission/Actions/IntakeSourcing/InitializeAdmissionTrack.php
  31. 18
      app/Domains/Admission/Actions/StudyModeProvisioning/DefineProgramTrack.php
  32. 14
      app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php
  33. 11
      app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php
  34. 15
      app/Domains/Admission/DTOs/IntakeScheduling/AdjustAdmissionScheduleDurationDTO.php
  35. 19
      app/Domains/Admission/DTOs/IntakeScheduling/PublishAdmissionScheduleDTO.php
  36. 13
      app/Domains/Admission/DTOs/IntakeSourcing/InitializeAdmissionTrackDTO.php
  37. 14
      app/Domains/Admission/DTOs/StudyModeProvisioning/DefineProgramTrackDTO.php
  38. 27
      app/Domains/Admission/Enums/AdmissionStatus.php
  39. 18
      app/Domains/Admission/Enums/BillingCycle.php
  40. 53
      app/Domains/Admission/Models/AdmissionSchedule.php
  41. 42
      app/Domains/Admission/Models/AdmissionTrack.php
  42. 52
      app/Domains/Admission/Models/FeeRate.php
  43. 35
      app/Domains/Admission/Models/FeeType.php
  44. 35
      app/Domains/Admission/Models/ProgramTrack.php
  45. 49
      app/Domains/Admission/Policies/AdmissionSchedulePolicy.php
  46. 49
      app/Domains/Admission/Policies/AdmissionTrackPolicy.php
  47. 49
      app/Domains/Admission/Policies/FeeRatePolicy.php
  48. 49
      app/Domains/Admission/Policies/FeeTypePolicy.php
  49. 49
      app/Domains/Admission/Policies/ProgramTrackPolicy.php
  50. 23
      app/Domains/Admission/Providers/AdmissionServiceProvider.php
  51. 31
      app/Domains/Admission/Providers/RelationshipServiceProvider.php
  52. 13
      app/Domains/Channel/Actions/AppProvisioning/DeauthorizConsumerApp.php
  53. 13
      app/Domains/Channel/Actions/AppProvisioning/DeauthorizeConsumerApp.php
  54. 19
      app/Domains/Channel/Actions/AppProvisioning/ProvisionConsumerApp.php
  55. 14
      app/Domains/Channel/Actions/AppProvisioning/RotateSecretKey.php
  56. 12
      app/Domains/Channel/DTOs/AppProvisioning/ProvisionConsumerAppDTO.php
  57. 34
      app/Domains/Channel/Models/Client.php
  58. 49
      app/Domains/Channel/Policies/ClientPolicy.php
  59. 23
      app/Domains/Channel/Providers/ChannelServiceProvider.php
  60. 51
      app/Domains/Channel/Providers/RelationshipServiceProvider.php
  61. 11
      app/Domains/Finance/Actions/DisbursementProcessing/DisburseFunds.php
  62. 11
      app/Domains/Finance/Actions/DisbursementProcessing/RecordVendorBill.php
  63. 19
      app/Domains/Finance/Actions/LedgerConfiguration/AdjustAccountClassification.php
  64. 20
      app/Domains/Finance/Actions/LedgerConfiguration/RegisterChartOfAccount.php
  65. 16
      app/Domains/Finance/Actions/LedgerConfiguration/SuspendAccountPostings.php
  66. 11
      app/Domains/Finance/Actions/PaymentProcessing/InitializeInvoice.php
  67. 11
      app/Domains/Finance/Actions/PaymentProcessing/ProcessIncomingPaymentWebhook.php
  68. 11
      app/Domains/Finance/Actions/ProductConfiguration/CreateProductMapping.php
  69. 11
      app/Domains/Finance/Actions/ProductConfiguration/RegisterProductMapping.php
  70. 37
      app/Domains/Finance/Casts/MoneyCurrency.php
  71. 10
      app/Domains/Finance/DTOs/DisbursementProcessing/DisburseFundsDTO.php
  72. 10
      app/Domains/Finance/DTOs/DisbursementProcessing/VendorBillDTO.php
  73. 15
      app/Domains/Finance/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php
  74. 17
      app/Domains/Finance/DTOs/LedgerConfiguration/RegisterChartOfAccountDTO.php
  75. 17
      app/Domains/Finance/DTOs/PaymentProcessing/BankWebhookPayloadDTO.php
  76. 15
      app/Domains/Finance/DTOs/PaymentProcessing/IssueInvoiceDTO.php
  77. 12
      app/Domains/Finance/DTOs/ProductConfiguration/RegisterProductMappingDTO.php
  78. 19
      app/Domains/Finance/Enums/AccountClassification.php
  79. 18
      app/Domains/Finance/Enums/InvoiceStatus.php
  80. 17
      app/Domains/Finance/Enums/InvoiceType.php
  81. 16
      app/Domains/Finance/Enums/PaymentDirection.php
  82. 18
      app/Domains/Finance/Enums/PaymentStatus.php
  83. 43
      app/Domains/Finance/Models/ChartOfAccount.php
  84. 60
      app/Domains/Finance/Models/Invoice.php
  85. 42
      app/Domains/Finance/Models/InvoiceReport.php
  86. 54
      app/Domains/Finance/Models/Payment.php
  87. 24
      app/Domains/Finance/Models/ProductMapping.php
  88. 47
      app/Domains/Finance/Models/StudentInvoiceReport.php
  89. 49
      app/Domains/Finance/Policies/ChartOfAccountPolicy.php
  90. 49
      app/Domains/Finance/Policies/InvoicePolicy.php
  91. 49
      app/Domains/Finance/Policies/InvoiceReportPolicy.php
  92. 49
      app/Domains/Finance/Policies/PaymentPolicy.php
  93. 49
      app/Domains/Finance/Policies/ProductMappingPolicy.php
  94. 49
      app/Domains/Finance/Policies/StudentInvoiceReportPolicy.php
  95. 23
      app/Domains/Finance/Providers/FinanceServiceProvider.php
  96. 31
      app/Domains/Finance/Providers/RelationshipServiceProvider.php
  97. 35
      app/Domains/Finance/Support/ValueObjects/Money.php
  98. 26
      app/Domains/Identity/Actions/AccessControl/DefineSystemRole.php
  99. 26
      app/Domains/Identity/Actions/AccessControl/DeleteSystemRole.php
  100. 22
      app/Domains/Identity/Actions/Governance/DeleteUser.php

75
README.md

@ -40,6 +40,7 @@ cd laravel-base
# 2. Run the automated setup # 2. Run the automated setup
# This installs PHP/JS deps, creates .env, generates key, and runs migrations # This installs PHP/JS deps, creates .env, generates key, and runs migrations
composer setup composer setup
``` ```
--- ---
@ -50,6 +51,7 @@ Use the pre-configured development command which runs the server, queue listener
```bash ```bash
composer dev composer dev
``` ```
The application will be available at `http://localhost:8000`. The application will be available at `http://localhost:8000`.
@ -59,14 +61,16 @@ The application will be available at `http://localhost:8000`.
## 5. Scripts & Commands ## 5. Scripts & Commands
Available via Composer: Available via Composer:
- `composer setup`: Initial project bootstrap.
- `composer dev`: Start development environment (Server + Queue + Logs + Vite). * `composer setup`: Initial project bootstrap.
- `composer test`: Run the test suite. * `composer dev`: Start development environment (Server + Queue + Logs + Vite).
- `php artisan domain:make`: Custom generator for the DDD architecture (see Section 12). * `composer test`: Run the test suite.
* `php artisan domain:make`: Custom generator for the DDD architecture (see Section 12).
Available via NPM: Available via NPM:
- `npm run dev`: Start Vite dev server.
- `npm run build`: Build assets for production. * `npm run dev`: Start Vite dev server.
* `npm run build`: Build assets for production.
--- ---
@ -93,6 +97,7 @@ Available via NPM:
├── tests/ <-- Pest test suite ├── tests/ <-- Pest test suite
├── storage/ <-- Logs, file uploads, and cache ├── storage/ <-- Logs, file uploads, and cache
└── vite.config.js <-- Vite configuration └── vite.config.js <-- Vite configuration
``` ```
--- ---
@ -181,6 +186,7 @@ app/
├── Traits/ <-- Domain-specific traits (HasFile) ├── Traits/ <-- Domain-specific traits (HasFile)
└── ... └── ...
``` ```
--- ---
@ -222,7 +228,7 @@ This architecture uses a strict, intentional naming language. Every name must co
Domain names are **Business Concepts**, not technical layers. They must be a singular noun that describes a bounded context. Domain names are **Business Concepts**, not technical layers. They must be a singular noun that describes a bounded context.
| ✅ Correct | ❌ Wrong | Why | | ✅ Correct | ❌ Wrong | Why |
|---|---|---| | --- | --- | --- |
| `Identity` | `Users` | Identity covers auth, roles, and user lifecycle — not just a table | | `Identity` | `Users` | Identity covers auth, roles, and user lifecycle — not just a table |
| `Account` | `Profile` | Account owns the full user account surface, not one model | | `Account` | `Profile` | Account owns the full user account surface, not one model |
| `System` | `Utils` / `Helpers` | System is a real bounded context for cross-cutting infrastructure | | `System` | `Utils` / `Helpers` | System is a real bounded context for cross-cutting infrastructure |
@ -232,7 +238,7 @@ Domain names are **Business Concepts**, not technical layers. They must be a sin
Subdirectories inside `Actions/`, `DTOs/`, `Events/`, and `Listeners/` must be named after **Business Capabilities**, not database nouns. Subdirectories inside `Actions/`, `DTOs/`, `Events/`, and `Listeners/` must be named after **Business Capabilities**, not database nouns.
| ✅ Correct | ❌ Wrong | Why | | ✅ Correct | ❌ Wrong | Why |
|---|---|---| | --- | --- | --- |
| `Onboarding/` | `Users/` | Describes the lifecycle stage, not the DB table | | `Onboarding/` | `Users/` | Describes the lifecycle stage, not the DB table |
| `AccessControl/` | `Roles/` | Describes the capability, not the resource | | `AccessControl/` | `Roles/` | Describes the capability, not the resource |
| `Governance/` | `Admin/` | Describes the compliance intent | | `Governance/` | `Admin/` | Describes the compliance intent |
@ -246,7 +252,7 @@ Subdirectories inside `Actions/`, `DTOs/`, `Events/`, and `Listeners/` must be n
Actions must be named after the **specific Business Intent** they fulfill. Use an active verb + business noun pattern. Actions must be named after the **specific Business Intent** they fulfill. Use an active verb + business noun pattern.
| ✅ Correct | ❌ Wrong | Why | | ✅ Correct | ❌ Wrong | Why |
|---|---|---| | --- | --- | --- |
| `ProvisionNewUser` | `CreateUser` | Describes *who* triggers it and *why* | | `ProvisionNewUser` | `CreateUser` | Describes *who* triggers it and *why* |
| `SuspendUser` | `DeleteUser` | Reveals the business consequence (soft revoke, not destroy) | | `SuspendUser` | `DeleteUser` | Reveals the business consequence (soft revoke, not destroy) |
| `UpdateUserRole` | `SaveRole` | Explicit about the subject and property being changed | | `UpdateUserRole` | `SaveRole` | Explicit about the subject and property being changed |
@ -255,22 +261,38 @@ Actions must be named after the **specific Business Intent** they fulfill. Use a
CRUD names (`CreateCategory`, `UpdateSetting`) are only acceptable for trivial lookup tables with **no side effects**. CRUD names (`CreateCategory`, `UpdateSetting`) are only acceptable for trivial lookup tables with **no side effects**.
### 10.4 DTO Class Names ### 10.4 Mutation Verbs Selection Matrix (The Cheat Sheet)
To keep domain actions aligned with authentic business intent rather than technical database scripts, choose the execution prefix according to this operational lifecycle matrix:
| Verb Prefix | Intent Scope | Real-World Context Example |
| --- | --- | --- |
| **`Initialize`** / **`Register`** | Declares the setup of an operational business track or engine. | `InitializeAdmissionTrack`, `RegisterStudyProgram` |
| **`Draft`** | Spawns a new entity row but locks it out of live visibility as a pending draft. | `DraftAdmissionSchedule` |
| **`Publish`** / **`Activate`** | Handles state transition to shift an existing record to a live production state. | `PublishAdmissionSchedule` |
| **`Define`** | Configures static lookups or structural reference dictionary elements. | `DefineProgramTrack`, `DefineFeeType` |
| **`Adjust`** / **`Modify`** | Performs precise partial modifications or data tuning on active records. | `AdjustAdmissionScheduleDuration` |
| **`Replace`** / **`Overwrite`** | Performs a full destructive replacement of an entry's total data layout. | `ReplaceTrackSpecification` |
| **`Synchronize`** | Forces full state matching with an authoritative external system registry. | `SynchronizeFacultyData` |
| **`Suspend`** / **`Pause`** | Halts access temporarily while leaving underlying structures fully intact. | `SuspendUser`, `PauseAdmissionSchedule` |
| **`Archive`** | Executes soft-deletion, shifting records permanently to history ledgers. | `ArchiveStudyProgram` |
### 10.5 DTO Class Names
DTOs are named after the Action they serve, with a `DTO` suffix. DTOs are named after the Action they serve, with a `DTO` suffix.
| Action | DTO | | Action | DTO |
|---|---| | --- | --- |
| `ProvisionNewUser` | `ProvisionUserDTO` | | `ProvisionNewUser` | `ProvisionUserDTO` |
| `UpdateUser` | `UpdateUserDTO` | | `UpdateUser` | `UpdateUserDTO` |
| `CreateSystemRole` | `CreateRoleDTO` | | `CreateSystemRole` | `CreateRoleDTO` |
### 10.5 Event Class Names ### 10.6 Event Class Names
Events are **past-tense facts** about something that already happened in the domain. The class name must be grammatically a completed truth. Events are **past-tense facts** about something that already happened in the domain. The class name must be grammatically a completed truth.
| ✅ Correct | ❌ Wrong | Why | | ✅ Correct | ❌ Wrong | Why |
|---|---|---| | --- | --- | --- |
| `UserWasProvisioned` | `UserProvisioned` | Explicit past-tense removes ambiguity | | `UserWasProvisioned` | `UserProvisioned` | Explicit past-tense removes ambiguity |
| `UserWasSuspended` | `UserSuspended` | Reads as a state, not a completed fact | | `UserWasSuspended` | `UserSuspended` | Reads as a state, not a completed fact |
| `UserLoggedIn` | `LoginEvent` | Noun + verb pattern; avoids the `Event` suffix | | `UserLoggedIn` | `LoginEvent` | Noun + verb pattern; avoids the `Event` suffix |
@ -278,12 +300,12 @@ Events are **past-tense facts** about something that already happened in the dom
**Rule:** Never suffix Events with `Event` (e.g., `UserRegisteredEvent` is wrong). The namespace `Events\` already communicates the type. **Rule:** Never suffix Events with `Event` (e.g., `UserRegisteredEvent` is wrong). The namespace `Events\` already communicates the type.
### 10.6 Listener Class Names ### 10.7 Listener Class Names
Listeners describe the **active reaction** to an event using an imperative verb phrase. Listeners describe the **active reaction** to an event using an imperative verb phrase.
| ✅ Correct | ❌ Wrong | Why | | ✅ Correct | ❌ Wrong | Why |
|---|---|---| | --- | --- | --- |
| `SendSignInActivityNotification` | `UserLoggedInListener` | Describes what the listener *does*, not what it reacts to | | `SendSignInActivityNotification` | `UserLoggedInListener` | Describes what the listener *does*, not what it reacts to |
| `DispatchWelcomeNotification` | `WelcomeListener` | Imperative verb makes the intent crystal clear | | `DispatchWelcomeNotification` | `WelcomeListener` | Imperative verb makes the intent crystal clear |
@ -341,6 +363,7 @@ You are an autonomous Senior Laravel Architect specializing in Pragmatic Domain-
Write modern PHP 8.4+ code with strict typing. Ensure all PSR-4 namespaces perfectly match the directory structure. Write modern PHP 8.4+ code with strict typing. Ensure all PSR-4 namespaces perfectly match the directory structure.
``` ```
--- ---
@ -356,6 +379,7 @@ To maintain the strict folder structure of the Antigravity architecture, **do no
```bash ```bash
php artisan domain:make {type} {domain} {name} [options] php artisan domain:make {type} {domain} {name} [options]
``` ```
**Arguments:** **Arguments:**
@ -409,6 +433,7 @@ class User extends Model
} }
} }
``` ```
### File Actions (Metadata via DTO) ### File Actions (Metadata via DTO)
@ -434,6 +459,7 @@ $action->execute(
) )
); );
``` ```
### System Asset Helper ### System Asset Helper
@ -486,7 +512,6 @@ The component relies on three collaborating layers:
### How It Works ### How It Works
* **Import:** The user uploads an `.xlsx` file via the FilePond modal. The file is stored to `local/excel/import/{resourceName}` and a new import instance is constructed with a UUID (`$importId`) and the authenticated user's ID (`$initiatorId`) before being dispatched to the queue via `Excel::queueImport()`. The Ingestion class lives in `app/Http/Ingestion/` (Gateway layer) and implements `WithChunkReading` (chunk size: 200 rows) to stay within shared-hosting memory limits. A success toast (`ui.excel.import.success`) is shown immediately upon queuing. * **Import:** The user uploads an `.xlsx` file via the FilePond modal. The file is stored to `local/excel/import/{resourceName}` and a new import instance is constructed with a UUID (`$importId`) and the authenticated user's ID (`$initiatorId`) before being dispatched to the queue via `Excel::queueImport()`. The Ingestion class lives in `app/Http/Ingestion/` (Gateway layer) and implements `WithChunkReading` (chunk size: 200 rows) to stay within shared-hosting memory limits. A success toast (`ui.excel.import.success`) is shown immediately upon queuing.
* **Export:** A Livewire browser event (`export-excel`) — dispatched by the DataTable's Export button — triggers the `export()` method via `#[On('export-excel')]`. The domain Export is wrapped in `StyledExport` and queued via `Excel::queue()`. The job chain appends `NotifyExportReady`, which dispatches `ExportCompleted`, which is handled by `SendExportReportEmail` to send the file as an email attachment to the authenticated user. A success toast (`ui.excel.export.success`) is shown immediately upon queuing. * **Export:** A Livewire browser event (`export-excel`) — dispatched by the DataTable's Export button — triggers the `export()` method via `#[On('export-excel')]`. The domain Export is wrapped in `StyledExport` and queued via `Excel::queue()`. The job chain appends `NotifyExportReady`, which dispatches `ExportCompleted`, which is handled by `SendExportReportEmail` to send the file as an email attachment to the authenticated user. A success toast (`ui.excel.export.success`) is shown immediately upon queuing.
### Component Props ### Component Props
@ -507,6 +532,7 @@ Embed the component in any DataTable page view. All props must be provided as fu
:import-class="\App\Http\Ingestion\Excel\Identity\UserImport::class" :import-class="\App\Http\Ingestion\Excel\Identity\UserImport::class"
resource-name="user" resource-name="user"
/> />
``` ```
The DataTable's Export button should dispatch the `export-excel` Livewire event, and the Import button should open the `#excel-import-modal` Bootstrap modal: The DataTable's Export button should dispatch the `export-excel` Livewire event, and the Import button should open the `#excel-import-modal` Bootstrap modal:
@ -518,6 +544,7 @@ Button::make('excel')
Button::make('excel') Button::make('excel')
->action("$('#excel-import-modal').modal('show')"), ->action("$('#excel-import-modal').modal('show')"),
``` ```
### Generating Export & Mapper Classes ### Generating Export & Mapper Classes
@ -531,6 +558,7 @@ php artisan domain:make export Identity UserExport --model=User
# Generate an Integration Mapper (auto-appends DataMapper suffix) # Generate an Integration Mapper (auto-appends DataMapper suffix)
php artisan domain:make mapper Identity User php artisan domain:make mapper Identity User
# → app/Domains/Identity/Integration/Mappers/UserDataMapper.php # → app/Domains/Identity/Integration/Mappers/UserDataMapper.php
``` ```
Domain Export classes must implement `FromQuery & WithHeadings & WithMapping & WithColumnFormatting`. The `StyledExport` decorator will apply all visual styling automatically at queue time — do **not** implement `WithStyles` directly on domain Exports. Domain Export classes must implement `FromQuery & WithHeadings & WithMapping & WithColumnFormatting`. The `StyledExport` decorator will apply all visual styling automatically at queue time — do **not** implement `WithStyles` directly on domain Exports.
@ -547,6 +575,7 @@ Excel::queue(StyledExport, $path)
└─> ExportCompleted::dispatch (Event) [app/Domains/System/Events/] └─> ExportCompleted::dispatch (Event) [app/Domains/System/Events/]
└─> SendExportReportEmail (Listener) [app/Domains/System/Listeners/] └─> SendExportReportEmail (Listener) [app/Domains/System/Listeners/]
└─> ExcelExportEmail (Mailable) [app/Domains/System/Mail/] └─> ExcelExportEmail (Mailable) [app/Domains/System/Mail/]
``` ```
The import notification is sent by the domain Import class itself upon completion, using `ExcelImportEmail` from the same `App\Domains\System\Mail\` namespace. The import notification is sent by the domain Import class itself upon completion, using `ExcelImportEmail` from the same `App\Domains\System\Mail\` namespace.
@ -581,12 +610,12 @@ We use [Pest PHP](https://pestphp.com) for our test suite. Please refer to [TEST
Key variables used in `.env`: Key variables used in `.env`:
- `APP_NAME`: Name of the application. * `APP_NAME`: Name of the application.
- `APP_ENV`: Application environment (`local`, `production`, etc.). * `APP_ENV`: Application environment (`local`, `production`, etc.).
- `APP_KEY`: Application encryption key. * `APP_KEY`: Application encryption key.
- `DB_CONNECTION`: Database driver (`sqlite`, `mysql`, `pgsql`). * `DB_CONNECTION`: Database driver (`sqlite`, `mysql`, `pgsql`).
- `QUEUE_CONNECTION`: Queue driver (default: `database`). * `QUEUE_CONNECTION`: Queue driver (default: `database`).
- `MAIL_MAILER`: Mail driver (default: `log`). * `MAIL_MAILER`: Mail driver (default: `log`).
See `.env.example` for the full list of available options. See `.env.example` for the full list of available options.
@ -595,5 +624,3 @@ See `.env.example` for the full list of available options.
## 20. License ## 20. License
This project is licensed under the **MIT License**. This project is licensed under the **MIT License**.

2
app/Console/Commands/DomainNewCommand.php

@ -26,7 +26,7 @@ class DomainNewCommand extends Command
if ($this->files->isDirectory($providerDir) && count($this->files->files($providerDir)) > 0) { if ($this->files->isDirectory($providerDir) && count($this->files->files($providerDir)) > 0) {
$this->components->error( $this->components->error(
"Domain [{$domain}] already has providers. ". "Domain [{$domain}] already has providers. ".
"Use [domain:make provider] to add individual providers." 'Use [domain:make provider] to add individual providers.'
); );
return self::FAILURE; return self::FAILURE;

21
app/Domains/Academic/Actions/Calendar/InitializeAcademicTerm.php

@ -0,0 +1,21 @@
<?php
namespace App\Domains\Academic\Actions\Calendar;
use App\Domains\Academic\DTOs\Calendar\InitializeAcademicTermDTO;
use App\Domains\Academic\Models\Term;
class InitializeAcademicTerm
{
public function execute(InitializeAcademicTermDTO $dto): void
{
Term::create([
'name' => $dto->name,
'code' => $dto->code,
'year_study' => $dto->yearStudy,
'semester' => $dto->semester,
'date_start' => $dto->startDate,
'date_end' => $dto->endDate,
]);
}
}

17
app/Domains/Academic/Actions/Calendar/ModifyTerm.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Academic\Actions\Calendar;
use App\Domains\Academic\DTOs\Calendar\ModifyTermDTO;
use App\Domains\Academic\Models\Term;
class ModifyTerm
{
public function execute(Term $term, ModifyTermDTO $dto): void
{
$term->update([
'date_start' => $dto->startDate,
'date_end' => $dto->endDate,
]);
}
}

17
app/Domains/Academic/Actions/Calendar/ModifyTermDate.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Academic\Actions\Calendar;
use App\Domains\Academic\DTOs\Calendar\ModifyTermDateDTO;
use App\Domains\Academic\Models\Term;
class ModifyTermDate
{
public function execute(Term $term, ModifyTermDateDTO $dto): void
{
$term->update([
'date_start' => $dto->startDate,
'date_end' => $dto->endDate,
]);
}
}

17
app/Domains/Academic/Actions/Curriculum/AdjustStudyProgramDetails.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Academic\Actions\Curriculum;
use App\Domains\Academic\DTOs\Curriculum\AdjustStudyProgramDTO;
use App\Domains\Academic\Models\StudyProgram;
class AdjustStudyProgramDetails
{
public function execute(StudyProgram $studyProgram, AdjustStudyProgramDTO $dto): void
{
$studyProgram->update([
'level' => $dto->level,
'status' => $dto->status,
]);
}
}

18
app/Domains/Academic/Actions/Curriculum/EstablishFaculty.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Academic\Actions\Curriculum;
use App\Domains\Academic\DTOs\Curriculum\EstablishFacultyDTO;
use App\Domains\Academic\Models\Faculty;
class EstablishFaculty
{
public function execute(EstablishFacultyDTO $dto): void
{
Faculty::create([
'name' => $dto->name,
'code' => $dto->code,
'external_id' => $dto->externalId,
]);
}
}

17
app/Domains/Academic/Actions/Curriculum/ModifyFacultyDetails.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Academic\Actions\Curriculum;
use App\Domains\Academic\DTOs\Curriculum\ModifyFacultyDetailsDTO;
use App\Domains\Academic\Models\Faculty;
class ModifyFacultyDetails
{
public function execute(Faculty $faculty, ModifyFacultyDetailsDTO $dto): void
{
$faculty->update([
'name' => $dto->name,
'code' => $dto->code,
]);
}
}

21
app/Domains/Academic/Actions/Curriculum/RegisterStudyProgram.php

@ -0,0 +1,21 @@
<?php
namespace App\Domains\Academic\Actions\Curriculum;
use App\Domains\Academic\DTOs\Curriculum\RegisterStudyProgramDTO;
use App\Domains\Academic\Models\StudyProgram;
class RegisterStudyProgram
{
public function execute(RegisterStudyProgramDTO $dto): void
{
StudyProgram::create([
'name' => $dto->name,
'code' => $dto->code,
'level' => $dto->level,
'status' => $dto->status,
'faculty_id' => $dto->facultyId,
'external_id' => $dto->externalId,
]);
}
}

17
app/Domains/Academic/DTOs/Calendar/InitializeAcademicTermDTO.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Academic\DTOs\Calendar;
use Carbon\CarbonImmutable;
readonly class InitializeAcademicTermDTO
{
public function __construct(
public string $name,
public string $code,
public string $yearStudy,
public string $semester,
public CarbonImmutable $startDate,
public CarbonImmutable $endDate,
) {}
}

13
app/Domains/Academic/DTOs/Calendar/ModifyTermDTO.php

@ -0,0 +1,13 @@
<?php
namespace App\Domains\Academic\DTOs\Calendar;
use Carbon\CarbonImmutable;
readonly class ModifyTermDTO
{
public function __construct(
public CarbonImmutable $startDate,
public CarbonImmutable $endDate,
) {}
}

13
app/Domains/Academic/DTOs/Calendar/ModifyTermDateDTO.php

@ -0,0 +1,13 @@
<?php
namespace App\Domains\Academic\DTOs\Calendar;
use Carbon\CarbonImmutable;
readonly class ModifyTermDateDTO
{
public function __construct(
public CarbonImmutable $startDate,
public CarbonImmutable $endDate,
) {}
}

13
app/Domains/Academic/DTOs/Curriculum/AdjustStudyProgramDTO.php

@ -0,0 +1,13 @@
<?php
namespace App\Domains\Academic\DTOs\Curriculum;
use App\Domains\System\Enums\LifecycleStatus;
readonly class AdjustStudyProgramDTO
{
public function __construct(
public string $level,
public LifecycleStatus $status,
) {}
}

12
app/Domains/Academic/DTOs/Curriculum/EstablishFacultyDTO.php

@ -0,0 +1,12 @@
<?php
namespace App\Domains\Academic\DTOs\Curriculum;
readonly class EstablishFacultyDTO
{
public function __construct(
public string $name,
public string $code,
public ?string $externalId = null
) {}
}

11
app/Domains/Academic/DTOs/Curriculum/ModifyFacultyDetailsDTO.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Academic\DTOs\Curriculum;
readonly class ModifyFacultyDetailsDTO
{
public function __construct(
public string $name,
public string $code
) {}
}

17
app/Domains/Academic/DTOs/Curriculum/RegisterStudyProgramDTO.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Academic\DTOs\Curriculum;
use App\Domains\System\Enums\LifecycleStatus;
readonly class RegisterStudyProgramDTO
{
public function __construct(
public string $name,
public string $code,
public string $level,
public LifecycleStatus $status,
public int $facultyId,
public int $externalId,
) {}
}

35
app/Domains/Academic/Models/Faculty.php

@ -0,0 +1,35 @@
<?php
namespace App\Domains\Academic\Models;
use App\Domains\Academic\Policies\FacultyPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use Database\Factories\Academic\FacultyFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'code', 'status', 'external_id'])]
#[UsePolicy(FacultyPolicy::class)]
class Faculty extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'status' => LifecycleStatus::class,
];
protected static function newFactory(): Factory
{
return FacultyFactory::new();
}
public function uniqueIds(): array
{
return ['ulid'];
}
}

48
app/Domains/Academic/Models/StudyProgram.php

@ -0,0 +1,48 @@
<?php
namespace App\Domains\Academic\Models;
use App\Domains\Academic\Policies\StudyProgramPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use Database\Factories\Academic\StudyProgramFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'name',
'code',
'level',
'status',
'faculty_id',
'external_id',
])]
#[UsePolicy(StudyProgramPolicy::class)]
class StudyProgram extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'status' => LifecycleStatus::class,
];
protected static function newFactory(): Factory
{
return StudyProgramFactory::new();
}
public function uniqueIds(): array
{
return ['ulid'];
}
public function faculty(): BelongsTo
{
return $this->belongsTo(Faculty::class);
}
}

30
app/Domains/Academic/Models/Term.php

@ -0,0 +1,30 @@
<?php
namespace App\Domains\Academic\Models;
use App\Domains\Academic\Policies\TermPolicy;
use Database\Factories\Academic\TermFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'code', 'year_study', 'semester', 'date_start', 'date_end'])]
#[UsePolicy(TermPolicy::class)]
class Term extends Model
{
use HasFactory;
use HasUlids;
protected static function newFactory(): Factory
{
return TermFactory::new();
}
public function uniqueIds(): array
{
return ['ulid'];
}
}

49
app/Domains/Academic/Policies/FacultyPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Academic\Policies;
use App\Domains\Academic\Models\Faculty;
use App\Domains\Identity\Models\User;
class FacultyPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('faculty.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Faculty $model): bool
{
return $user->hasPermissionTo('faculty.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('faculty.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Faculty $model): bool
{
return $user->hasPermissionTo('faculty.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Faculty $model): bool
{
return $user->hasPermissionTo('faculty.delete');
}
}

49
app/Domains/Academic/Policies/StudyProgramPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Academic\Policies;
use App\Domains\Academic\Models\StudyProgram;
use App\Domains\Identity\Models\User;
class StudyProgramPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('study-program.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, StudyProgram $model): bool
{
return $user->hasPermissionTo('study-program.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('study-program.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, StudyProgram $model): bool
{
return $user->hasPermissionTo('study-program.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, StudyProgram $model): bool
{
return $user->hasPermissionTo('study-program.delete');
}
}

49
app/Domains/Academic/Policies/TermPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Academic\Policies;
use App\Domains\Academic\Models\Term;
use App\Domains\Identity\Models\User;
class TermPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('term.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Term $model): bool
{
return $user->hasPermissionTo('term.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('term.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Term $model): bool
{
return $user->hasPermissionTo('term.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Term $model): bool
{
return $user->hasPermissionTo('term.delete');
}
}

23
app/Domains/Academic/Providers/AcademicServiceProvider.php

@ -0,0 +1,23 @@
<?php
namespace App\Domains\Academic\Providers;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
class AcademicServiceProvider extends ServiceProvider
{
use RegistersDomainEvents;
protected array $listen = [];
public function register(): void
{
$this->app->register(RelationshipServiceProvider::class);
}
public function boot(): void
{
$this->registerEvents();
}
}

54
app/Domains/Academic/Providers/RelationshipServiceProvider.php

@ -0,0 +1,54 @@
<?php
namespace App\Domains\Academic\Providers;
use App\Domains\Academic\Models\StudyProgram;
use App\Domains\Academic\Models\Term;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\Admission\Models\FeeRate;
use Illuminate\Support\ServiceProvider;
class RelationshipServiceProvider extends ServiceProvider
{
/**
* Boot cross-domain runtime relationships.
*
* This provider is strictly dedicated to runtime, decoupled cross-domain
* relationship definitions. Use Model::resolveRelationUsing() here to wire
* explicit polymorphic or foreign-key macros between models that live in
* different domains — without introducing a hard compile-time dependency
* between those domains.
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.
*
* Example:
*
* Order::resolveRelationUsing('customer', fn (Order $order) =>
* $order->belongsTo(Customer::class, 'customer_id')
* );
*/
public function boot(): void
{
// Register cross-domain relationships below.
AdmissionSchedule::resolveRelationUsing('term', fn (AdmissionSchedule $admissionSchedule) => $admissionSchedule->belongsTo(
related: Term::class,
foreignKey: 'term_id'
));
Term::resolveRelationUsing('admission_schedules', fn (Term $term) => $term->hasMany(
related: AdmissionSchedule::class,
foreignKey: 'term_id',
localKey: 'id'
));
FeeRate::resolveRelationUsing('study_program', fn (FeeRate $programTrack) => $programTrack->belongsTo(
related: StudyProgram::class,
foreignKey: 'study_program_id'
));
StudyProgram::resolveRelationUsing('fee_rates', fn (StudyProgram $studyProgram) => $studyProgram->hasMany(
related: FeeRate::class,
foreignKey: 'study_program_id',
localKey: 'id'
));
}
}

21
app/Domains/Account/Enums/GenderOption.php

@ -2,16 +2,17 @@
namespace App\Domains\Account\Enums; namespace App\Domains\Account\Enums;
enum GenderOption: string use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasUiBadge;
enum GenderOption: string implements HasLabel, HasUiBadge
{ {
use InteractsWithLabels;
case MALE = 'male'; case MALE = 'male';
case FEMALE = 'female'; case FEMALE = 'female';
public function label(): string
{
return __("domains/account/enum.gender.{$this->value}");
}
public static function fromLabel($value): self public static function fromLabel($value): self
{ {
return match ($value) { return match ($value) {
@ -19,4 +20,12 @@ enum GenderOption: string
self::FEMALE->label() => self::FEMALE, self::FEMALE->label() => self::FEMALE,
}; };
} }
public function variant(): string
{
return match ($this) {
self::MALE => 'primary',
self::FEMALE => 'secondary',
};
}
} }

17
app/Domains/Admission/Actions/AdmissionPricing/DefineFeeType.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Admission\Actions\AdmissionPricing;
use App\Domains\Admission\DTOs\AdmissionPricing\FeeTypeDTO;
use App\Domains\Admission\Models\FeeType;
class DefineFeeType
{
public function execute(FeeTypeDTO $dto): void
{
FeeType::create([
'name' => $dto->name,
'billing_cycle' => $dto->billingCycle,
]);
}
}

11
app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Admission\Actions\AdmissionPricing;
class SetUpAdmissionFeeRates
{
public function execute(): void
{
//
}
}

19
app/Domains/Admission/Actions/IntakeScheduling/AdjustAdmissionScheduleDuration.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Admission\Actions\IntakeScheduling;
use App\Domains\Admission\DTOs\IntakeScheduling\AdjustAdmissionScheduleDurationDTO;
use App\Domains\Admission\Models\AdmissionSchedule;
class AdjustAdmissionScheduleDuration
{
public function execute(AdmissionSchedule $admissionSchedule, AdjustAdmissionScheduleDurationDTO $dto): void
{
$admissionSchedule->update([
'date_start' => $dto->startData,
'date_end' => $dto->endData,
're_registration_date_start' => $dto->reRegisterStartData,
're_registration_date_end' => $dto->reRegisterEndData,
]);
}
}

22
app/Domains/Admission/Actions/IntakeScheduling/PublishAdmissionSchedule.php

@ -0,0 +1,22 @@
<?php
namespace App\Domains\Admission\Actions\IntakeScheduling;
use App\Domains\Admission\DTOs\IntakeScheduling\PublishAdmissionScheduleDTO;
use App\Domains\Admission\Models\AdmissionSchedule;
class PublishAdmissionSchedule
{
public function execute(PublishAdmissionScheduleDTO $dto): void
{
AdmissionSchedule::create([
'date_start' => $dto->startData,
'date_end' => $dto->endData,
're_registration_date_start' => $dto->reRegisterStartData,
're_registration_date_end' => $dto->reRegisterEndData,
'status' => $dto->status,
'term_id' => $dto->termId,
'admission_track_id' => $dto->admissionTrackId,
]);
}
}

17
app/Domains/Admission/Actions/IntakeSourcing/InitializeAdmissionTrack.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Admission\Actions\IntakeSourcing;
use App\Domains\Admission\DTOs\IntakeSourcing\InitializeAdmissionTrackDTO;
use App\Domains\Admission\Models\AdmissionTrack;
class InitializeAdmissionTrack
{
public function execute(InitializeAdmissionTrackDTO $dto): void
{
AdmissionTrack::create([
'name' => $dto->name,
'status' => $dto->status,
]);
}
}

18
app/Domains/Admission/Actions/StudyModeProvisioning/DefineProgramTrack.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admission\Actions\StudyModeProvisioning;
use App\Domains\Admission\DTOs\StudyModeProvisioning\DefineProgramTrackDTO;
use App\Domains\Admission\Models\ProgramTrack;
class DefineProgramTrack
{
public function execute(DefineProgramTrackDTO $dto): void
{
ProgramTrack::create([
'name' => $dto->name,
'code' => $dto->code,
'status' => $dto->status,
]);
}
}

14
app/Domains/Admission/DTOs/AdmissionPricing/AdmissionFeeRatesDTO.php

@ -0,0 +1,14 @@
<?php
namespace App\Domains\Admission\DTOs\AdmissionPricing;
readonly class AdmissionFeeRatesDTO
{
public function __construct(
public int $termId,
public int $programTrackId,
public int $admissionTrackId,
public int $feeTypeId,
public float $amount,
) {}
}

11
app/Domains/Admission/DTOs/AdmissionPricing/FeeTypeDTO.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Admission\DTOs\AdmissionPricing;
readonly class FeeTypeDTO
{
public function __construct(
public string $name,
public string $billingCycle
) {}
}

15
app/Domains/Admission/DTOs/IntakeScheduling/AdjustAdmissionScheduleDurationDTO.php

@ -0,0 +1,15 @@
<?php
namespace App\Domains\Admission\DTOs\IntakeScheduling;
use Carbon\CarbonImmutable;
readonly class AdjustAdmissionScheduleDurationDTO
{
public function __construct(
public CarbonImmutable $startData,
public CarbonImmutable $endData,
public CarbonImmutable $reRegisterStartData,
public CarbonImmutable $reRegisterEndData,
) {}
}

19
app/Domains/Admission/DTOs/IntakeScheduling/PublishAdmissionScheduleDTO.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Admission\DTOs\IntakeScheduling;
use App\Domains\Admission\Enums\AdmissionStatus;
use Carbon\CarbonImmutable;
readonly class PublishAdmissionScheduleDTO
{
public function __construct(
public CarbonImmutable $startData,
public CarbonImmutable $endData,
public CarbonImmutable $reRegisterStartData,
public CarbonImmutable $reRegisterEndData,
public AdmissionStatus $status,
public int $termId,
public int $admissionTrackId
) {}
}

13
app/Domains/Admission/DTOs/IntakeSourcing/InitializeAdmissionTrackDTO.php

@ -0,0 +1,13 @@
<?php
namespace App\Domains\Admission\DTOs\IntakeSourcing;
use App\Domains\System\Enums\LifecycleStatus;
readonly class InitializeAdmissionTrackDTO
{
public function __construct(
public string $name,
public LifecycleStatus $status,
) {}
}

14
app/Domains/Admission/DTOs/StudyModeProvisioning/DefineProgramTrackDTO.php

@ -0,0 +1,14 @@
<?php
namespace App\Domains\Admission\DTOs\StudyModeProvisioning;
use App\Domains\System\Enums\LifecycleStatus;
readonly class DefineProgramTrackDTO
{
public function __construct(
public string $name,
public string $code,
public LifecycleStatus $status,
) {}
}

27
app/Domains/Admission/Enums/AdmissionStatus.php

@ -0,0 +1,27 @@
<?php
namespace App\Domains\Admission\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\Contracts\HasUiBadge;
enum AdmissionStatus: string implements HasLabel, HasUiBadge
{
use HasPredicateMethod;
use InteractsWithLabels;
case PUBLISHED = 'published';
case DRAFT = 'draft';
case ARCHIVED = 'archived';
public function variant(): string
{
return match ($this) {
self::PUBLISHED => 'success',
self::DRAFT => 'warning',
self::ARCHIVED => 'danger',
};
}
}

18
app/Domains/Admission/Enums/BillingCycle.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admission\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum BillingCycle: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case ONCE = 'once';
case MONTHLY = 'monthly';
case SEMESTER = 'semester';
case ANNUAL = 'annual';
}

53
app/Domains/Admission/Models/AdmissionSchedule.php

@ -0,0 +1,53 @@
<?php
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\AdmissionSchedulePolicy;
use App\Domains\Admission\Enums\AdmissionStatus;
use Database\Factories\Admission\AdmissionScheduleFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'date_start',
'date_end',
're_registration_date_start',
're_registration_date_end',
'admission_track_id',
'status',
'term_id',
])]
#[UsePolicy(AdmissionSchedulePolicy::class)]
class AdmissionSchedule extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'date_start' => 'date',
'date_end' => 'date',
're_registration_date_start' => 'date',
're_registration_date_end' => 'date',
'status' => AdmissionStatus::class,
];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return AdmissionScheduleFactory::new();
}
public function admissionTrack(): BelongsTo
{
return $this->belongsTo(AdmissionTrack::class);
}
}

42
app/Domains/Admission/Models/AdmissionTrack.php

@ -0,0 +1,42 @@
<?php
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\AdmissionTrackPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasSlugs;
use Database\Factories\Admission\AdmissionTrackFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'slug', 'status'])]
#[UsePolicy(AdmissionTrackPolicy::class)]
class AdmissionTrack extends Model
{
use HasFactory;
use HasUlids;
use HasSlugs;
protected $casts = [
'status' => LifecycleStatus::class,
];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return AdmissionTrackFactory::new();
}
public function sluggable(): string
{
return 'name';
}
}

52
app/Domains/Admission/Models/FeeRate.php

@ -0,0 +1,52 @@
<?php
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\FeeRatePolicy;
use Database\Factories\Admission\FeeRateFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'admission_schedule_id',
'program_track_id',
'study_program_id',
'fee_type_id',
'amount',
])]
#[UsePolicy(FeeRatePolicy::class)]
class FeeRate extends Model
{
use HasFactory;
use HasUlids;
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return FeeRateFactory::new();
}
public function admissionSchedule(): BelongsTo
{
return $this->belongsTo(AdmissionSchedule::class);
}
public function programTrack(): BelongsTo
{
return $this->belongsTo(ProgramTrack::class);
}
public function feeType(): BelongsTo
{
return $this->belongsTo(FeeType::class);
}
}

35
app/Domains/Admission/Models/FeeType.php

@ -0,0 +1,35 @@
<?php
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Enums\BillingCycle;
use App\Domains\Admission\Policies\FeeTypePolicy;
use Database\Factories\Admission\FeeTypeFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'billing_cycle'])]
#[UsePolicy(FeeTypePolicy::class)]
class FeeType extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'billing_cycle' => BillingCycle::class,
];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return FeeTypeFactory::new();
}
}

35
app/Domains/Admission/Models/ProgramTrack.php

@ -0,0 +1,35 @@
<?php
namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\ProgramTrackPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use Database\Factories\Academic\ProgramTrackFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['code', 'name', 'status'])]
#[UsePolicy(ProgramTrackPolicy::class)]
class ProgramTrack extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'status' => LifecycleStatus::class,
];
protected static function newFactory(): Factory
{
return ProgramTrackFactory::new();
}
public function uniqueIds(): array
{
return ['ulid'];
}
}

49
app/Domains/Admission/Policies/AdmissionSchedulePolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Admission\Policies;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\Identity\Models\User;
class AdmissionSchedulePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('admission-schedule.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, AdmissionSchedule $model): bool
{
return $user->hasPermissionTo('admission-schedule.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('admission-schedule.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, AdmissionSchedule $model): bool
{
return $user->hasPermissionTo('admission-schedule.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, AdmissionSchedule $model): bool
{
return $user->hasPermissionTo('admission-schedule.delete');
}
}

49
app/Domains/Admission/Policies/AdmissionTrackPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Admission\Policies;
use App\Domains\Admission\Models\AdmissionTrack;
use App\Domains\Identity\Models\User;
class AdmissionTrackPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('admission-track.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, AdmissionTrack $model): bool
{
return $user->hasPermissionTo('admission-track.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('admission-track.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, AdmissionTrack $model): bool
{
return $user->hasPermissionTo('admission-track.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, AdmissionTrack $model): bool
{
return $user->hasPermissionTo('admission-track.delete');
}
}

49
app/Domains/Admission/Policies/FeeRatePolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Admission\Policies;
use App\Domains\Admission\Models\FeeRate;
use App\Domains\Identity\Models\User;
class FeeRatePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('fee-rate.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, FeeRate $model): bool
{
return $user->hasPermissionTo('fee-rate.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('fee-rate.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, FeeRate $model): bool
{
return $user->hasPermissionTo('fee-rate.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, FeeRate $model): bool
{
return $user->hasPermissionTo('fee-rate.delete');
}
}

49
app/Domains/Admission/Policies/FeeTypePolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Admission\Policies;
use App\Domains\Admission\Models\FeeType;
use App\Domains\Identity\Models\User;
class FeeTypePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('fee-type.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, FeeType $model): bool
{
return $user->hasPermissionTo('fee-type.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('fee-type.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, FeeType $model): bool
{
return $user->hasPermissionTo('fee-type.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, FeeType $model): bool
{
return $user->hasPermissionTo('fee-type.delete');
}
}

49
app/Domains/Admission/Policies/ProgramTrackPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Admission\Policies;
use App\Domains\Admission\Models\ProgramTrack;
use App\Domains\Identity\Models\User;
class ProgramTrackPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('program-track.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, ProgramTrack $model): bool
{
return $user->hasPermissionTo('program-track.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('program-track.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, ProgramTrack $model): bool
{
return $user->hasPermissionTo('program-track.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, ProgramTrack $model): bool
{
return $user->hasPermissionTo('program-track.delete');
}
}

23
app/Domains/Admission/Providers/AdmissionServiceProvider.php

@ -0,0 +1,23 @@
<?php
namespace App\Domains\Admission\Providers;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
class AdmissionServiceProvider extends ServiceProvider
{
use RegistersDomainEvents;
protected array $listen = [];
public function register(): void
{
$this->app->register(RelationshipServiceProvider::class);
}
public function boot(): void
{
$this->registerEvents();
}
}

31
app/Domains/Admission/Providers/RelationshipServiceProvider.php

@ -0,0 +1,31 @@
<?php
namespace App\Domains\Admission\Providers;
use Illuminate\Support\ServiceProvider;
class RelationshipServiceProvider extends ServiceProvider
{
/**
* Boot cross-domain runtime relationships.
*
* This provider is strictly dedicated to runtime, decoupled cross-domain
* relationship definitions. Use Model::resolveRelationUsing() here to wire
* explicit polymorphic or foreign-key macros between models that live in
* different domains — without introducing a hard compile-time dependency
* between those domains.
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.
*
* Example:
*
* Order::resolveRelationUsing('customer', fn (Order $order) =>
* $order->belongsTo(Customer::class, 'customer_id')
* );
*/
public function boot(): void
{
// Register cross-domain relationships below.
}
}

13
app/Domains/Channel/Actions/AppProvisioning/DeauthorizConsumerApp.php

@ -0,0 +1,13 @@
<?php
namespace App\Domains\Channel\Actions\AppProvisioning;
use App\Domains\Channel\Models\Client;
class DeauthorizConsumerApp
{
public function execute(Client $client): void
{
//
}
}

13
app/Domains/Channel/Actions/AppProvisioning/DeauthorizeConsumerApp.php

@ -0,0 +1,13 @@
<?php
namespace App\Domains\Channel\Actions\AppProvisioning;
use App\Domains\Channel\Models\Client;
class DeauthorizeConsumerApp
{
public function execute(Client $client): void
{
//
}
}

19
app/Domains/Channel/Actions/AppProvisioning/ProvisionConsumerApp.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Channel\Actions\AppProvisioning;
use App\Domains\Channel\DTOs\AppProvisioning\ProvisionConsumerAppDTO;
use App\Domains\Channel\Models\Client;
class ProvisionConsumerApp
{
public function execute(ProvisionConsumerAppDTO $dto): void
{
Client::create([
'name' => $dto->name,
'code' => $dto->code,
'url' => $dto->url,
'secret' => encrypt(uuid_create()),
]);
}
}

14
app/Domains/Channel/Actions/AppProvisioning/RotateSecretKey.php

@ -0,0 +1,14 @@
<?php
namespace App\Domains\Channel\Actions\AppProvisioning;
use App\Domains\Channel\Models\Client;
class RotateSecretKey
{
public function execute(Client $client): void
{
$key = encrypt(uuid_create());
$client->update(['secret' => $key]);
}
}

12
app/Domains/Channel/DTOs/AppProvisioning/ProvisionConsumerAppDTO.php

@ -0,0 +1,12 @@
<?php
namespace App\Domains\Channel\DTOs\AppProvisioning;
readonly class ProvisionConsumerAppDTO
{
public function __construct(
public string $name,
public string $code,
public string $url,
) {}
}

34
app/Domains/Channel/Models/Client.php

@ -0,0 +1,34 @@
<?php
namespace App\Domains\Channel\Models;
use App\Domains\Channel\Policies\ClientPolicy;
use Database\Factories\Channel\ClientFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable(['name', 'url', 'code', 'secret'])]
#[UsePolicy(ClientPolicy::class)]
class Client extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'secret' => 'encrypted',
];
protected static function newFactory(): Factory
{
return ClientFactory::new();
}
public function uniqueIds(): array
{
return ['ulid'];
}
}

49
app/Domains/Channel/Policies/ClientPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Channel\Policies;
use App\Domains\Channel\Models\Client;
use App\Domains\Identity\Models\User;
class ClientPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('client.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Client $model): bool
{
return $user->hasPermissionTo('client.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('client.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Client $model): bool
{
return $user->hasPermissionTo('client.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Client $model): bool
{
return $user->hasPermissionTo('client.delete');
}
}

23
app/Domains/Channel/Providers/ChannelServiceProvider.php

@ -0,0 +1,23 @@
<?php
namespace App\Domains\Channel\Providers;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
class ChannelServiceProvider extends ServiceProvider
{
use RegistersDomainEvents;
protected array $listen = [];
public function register(): void
{
$this->app->register(RelationshipServiceProvider::class);
}
public function boot(): void
{
$this->registerEvents();
}
}

51
app/Domains/Channel/Providers/RelationshipServiceProvider.php

@ -0,0 +1,51 @@
<?php
namespace App\Domains\Channel\Providers;
use App\Domains\Channel\Models\Client;
use App\Domains\Finance\Models\Invoice;
use App\Domains\Finance\Models\ProductMapping;
use Illuminate\Support\ServiceProvider;
class RelationshipServiceProvider extends ServiceProvider
{
/**
* Boot cross-domain runtime relationships.
*
* This provider is strictly dedicated to runtime, decoupled cross-domain
* relationship definitions. Use Model::resolveRelationUsing() here to wire
* explicit polymorphic or foreign-key macros between models that live in
* different domains — without introducing a hard compile-time dependency
* between those domains.
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.
*
* Example:
*
* Order::resolveRelationUsing('customer', fn (Order $order) =>
* $order->belongsTo(Customer::class, 'customer_id')
* );
*/
public function boot(): void
{
// Register cross-domain relationships below.
Client::resolveRelationUsing('transaction', fn (Client $client) => $client->hasMany(
related: Invoice::class,
foreignKey: 'client_id'
));
Invoice::resolveRelationUsing('client', fn (Invoice $transaction) => $transaction->belongsTo(
related: Client::class,
foreignKey: 'client_id'
));
Client::resolveRelationUsing('product_mapping', fn (Client $client) => $client->hasMany(
related: ProductMapping::class,
foreignKey: 'client_id'
));
ProductMapping::resolveRelationUsing('client', fn (ProductMapping $productMapping) => $productMapping->belongsTo(
related: Client::class,
foreignKey: 'client_id'
));
}
}

11
app/Domains/Finance/Actions/DisbursementProcessing/DisburseFunds.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Finance\Actions\DisbursementProcessing;
class DisburseFunds
{
public function execute(): void
{
//
}
}

11
app/Domains/Finance/Actions/DisbursementProcessing/RecordVendorBill.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Finance\Actions\DisbursementProcessing;
class RecordVendorBill
{
public function execute(): void
{
//
}
}

19
app/Domains/Finance/Actions/LedgerConfiguration/AdjustAccountClassification.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Finance\Actions\LedgerConfiguration;
use App\Domains\Finance\DTOs\LedgerConfiguration\AdjustAccountClassificationDTO;
use App\Domains\Finance\Models\ChartOfAccount;
class AdjustAccountClassification
{
public function execute(ChartOfAccount $coa, AdjustAccountClassificationDTO $dto): void
{
$coa->update([
'code' => $dto->code,
'name' => $dto->name,
'classification' => $dto->classification,
'parent_id' => $dto->parentId,
]);
}
}

20
app/Domains/Finance/Actions/LedgerConfiguration/RegisterChartOfAccount.php

@ -0,0 +1,20 @@
<?php
namespace App\Domains\Finance\Actions\LedgerConfiguration;
use App\Domains\Finance\DTOs\LedgerConfiguration\RegisterChartOfAccountDTO;
use App\Domains\Finance\Models\ChartOfAccount;
class RegisterChartOfAccount
{
public function execute(RegisterChartOfAccountDTO $dto): void
{
ChartOfAccount::create([
'code' => $dto->code,
'name' => $dto->name,
'classification' => $dto->classification,
'status' => $dto->status,
'parent_id' => $dto->parentId,
]);
}
}

16
app/Domains/Finance/Actions/LedgerConfiguration/SuspendAccountPostings.php

@ -0,0 +1,16 @@
<?php
namespace App\Domains\Finance\Actions\LedgerConfiguration;
use App\Domains\Finance\Models\ChartOfAccount;
use App\Domains\System\Enums\LifecycleStatus;
class SuspendAccountPostings
{
public function execute(ChartOfAccount $coa): void
{
$coa->update([
'status' => LifecycleStatus::ARCHIVED,
]);
}
}

11
app/Domains/Finance/Actions/PaymentProcessing/InitializeInvoice.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Finance\Actions\PaymentProcessing;
class InitializeInvoice
{
public function execute(): void
{
//
}
}

11
app/Domains/Finance/Actions/PaymentProcessing/ProcessIncomingPaymentWebhook.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Finance\Actions\PaymentProcessing;
class ProcessIncomingPaymentWebhook
{
public function execute(): void
{
//
}
}

11
app/Domains/Finance/Actions/ProductConfiguration/CreateProductMapping.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Finance\Actions\ProductConfiguration;
class CreateProductMapping
{
public function execute(): void
{
//
}
}

11
app/Domains/Finance/Actions/ProductConfiguration/RegisterProductMapping.php

@ -0,0 +1,11 @@
<?php
namespace App\Domains\Finance\Actions\ProductConfiguration;
class RegisterProductMapping
{
public function execute(): void
{
//
}
}

37
app/Domains/Finance/Casts/MoneyCurrency.php

@ -0,0 +1,37 @@
<?php
namespace App\Domains\Finance\Casts;
use App\Domains\Finance\Support\ValueObjects\Money;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
class MoneyCurrency implements CastsAttributes
{
public function get(Model $model, string $key, mixed $value, array $attributes): ?Money
{
if (is_null($value)) {
return null;
}
return new Money((int) $value);
}
public function set(Model $model, string $key, mixed $value, array $attributes): ?float
{
if (is_null($value)) {
return null;
}
if ($value instanceof Money) {
return $value->amount;
}
if (is_numeric($value)) {
return (float) $value;
}
throw new InvalidArgumentException("The {$key} attribute must be an float or an instance of Money.");
}
}

10
app/Domains/Finance/DTOs/DisbursementProcessing/DisburseFundsDTO.php

@ -0,0 +1,10 @@
<?php
namespace App\Domains\Finance\DTOs\DisbursementProcessing;
readonly class DisburseFundsDTO
{
public function __construct(
//
) {}
}

10
app/Domains/Finance/DTOs/DisbursementProcessing/VendorBillDTO.php

@ -0,0 +1,10 @@
<?php
namespace App\Domains\Finance\DTOs\DisbursementProcessing;
readonly class VendorBillDTO
{
public function __construct(
//
) {}
}

15
app/Domains/Finance/DTOs/LedgerConfiguration/AdjustAccountClassificationDTO.php

@ -0,0 +1,15 @@
<?php
namespace App\Domains\Finance\DTOs\LedgerConfiguration;
use App\Domains\Finance\Enums\AccountClassification;
readonly class AdjustAccountClassificationDTO
{
public function __construct(
public string $code,
public string $name,
public AccountClassification $classification,
public ?int $parentId,
) {}
}

17
app/Domains/Finance/DTOs/LedgerConfiguration/RegisterChartOfAccountDTO.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Finance\DTOs\LedgerConfiguration;
use App\Domains\Finance\Enums\AccountClassification;
use App\Domains\System\Enums\LifecycleStatus;
readonly class RegisterChartOfAccountDTO
{
public function __construct(
public string $code,
public string $name,
public AccountClassification $classification,
public LifecycleStatus $status,
public ?int $parentId,
) {}
}

17
app/Domains/Finance/DTOs/PaymentProcessing/BankWebhookPayloadDTO.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Finance\DTOs\PaymentProcessing;
use Carbon\CarbonImmutable;
readonly class BankWebhookPayloadDTO
{
public function __construct(
public int $virtualAccount,
public string $bankRefId,
public CarbonImmutable $validUntil,
public float $amount,
public string $status,
public string $transactionId
) {}
}

15
app/Domains/Finance/DTOs/PaymentProcessing/IssueInvoiceDTO.php

@ -0,0 +1,15 @@
<?php
namespace App\Domains\Finance\DTOs\PaymentProcessing;
readonly class IssueInvoiceDTO
{
public function __construct(
public string $name,
public string $clientRefId,
public string $detail,
public string $type,
public string $status,
public string $clientId
) {}
}

12
app/Domains/Finance/DTOs/ProductConfiguration/RegisterProductMappingDTO.php

@ -0,0 +1,12 @@
<?php
namespace App\Domains\Finance\DTOs\ProductConfiguration;
readonly class RegisterProductMappingDTO
{
public function __construct(
public string $name,
public string $code,
public int $coaId
) {}
}

19
app/Domains/Finance/Enums/AccountClassification.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum AccountClassification: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case ASSET = 'asset';
case LIABILITY = 'liability';
case EQUITY = 'equity';
case REVENUE = 'revenue';
case EXPENSE = 'expense';
}

18
app/Domains/Finance/Enums/InvoiceStatus.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum InvoiceStatus: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case PAID = 'paid';
case PENDING = 'pending';
case PARTIALLY_PAID = 'partially_paid';
case OVERDUE = 'overdue';
}

17
app/Domains/Finance/Enums/InvoiceType.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum InvoiceType: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case CUSTOMER_INVOICE = 'customer_invoice';
case VENDOR_BILL = 'vendor_bill';
case CREDIT_NOTE = 'credit_note';
}

16
app/Domains/Finance/Enums/PaymentDirection.php

@ -0,0 +1,16 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum PaymentDirection: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case RECEIPT = 'receipt';
case DISBURSEMENT = 'disbursement';
}

18
app/Domains/Finance/Enums/PaymentStatus.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Finance\Enums;
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum PaymentStatus: string implements HasLabel
{
use HasPredicateMethod;
use InteractsWithLabels;
case PENDING = 'pending';
case SUCCESS = 'success';
case FAILED = 'failed';
case EXPIRED = 'expired';
}

43
app/Domains/Finance/Models/ChartOfAccount.php

@ -0,0 +1,43 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Enums\AccountClassification;
use App\Domains\Finance\Policies\ChartOfAccountPolicy;
use App\Domains\System\Enums\LifecycleStatus;
use Database\Factories\Finance\ChartOfAccountFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'code',
'name',
'classification',
'status',
'parent_id',
])]
#[UsePolicy(ChartOfAccountPolicy::class)]
class ChartOfAccount extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'classification' => AccountClassification::class,
'status' => LifecycleStatus::class,
];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return ChartOfAccountFactory::new();
}
}

60
app/Domains/Finance/Models/Invoice.php

@ -0,0 +1,60 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType;
use App\Domains\Finance\Policies\InvoicePolicy;
use Database\Factories\Finance\InvoiceFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
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\Database\Eloquent\Relations\HasMany;
#[Fillable([
'name',
'detail',
'client_ref_id',
'type',
'amount',
'status',
'client_id',
'chart_of_account_id',
])]
#[UsePolicy(InvoicePolicy::class)]
class Invoice extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'amount' => MoneyCurrency::class,
'type' => InvoiceType::class,
'status' => InvoiceStatus::class,
];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return InvoiceFactory::new();
}
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
}

42
app/Domains/Finance/Models/InvoiceReport.php

@ -0,0 +1,42 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Policies\InvoiceReportPolicy;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'invoice_id',
'invoice_name',
'customer_id',
'customer_name',
'amount',
'paid_amount',
'issued_at',
'paid_at',
'status',
])]
#[UsePolicy(InvoiceReportPolicy::class)]
class InvoiceReport extends Model
{
public static function boot(): void
{
self::boot();
static::creating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot create invoice report from here, use event instead.'));
static::deleting(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot delete invoice report.'));
static::updating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot update invoice report.'));
}
protected $casts = [
'amount' => MoneyCurrency::class,
'paid_amount' => MoneyCurrency::class,
'issued_at' => 'datetime',
'paid_at' => 'datetime',
'status' => InvoiceStatus::class,
];
}

54
app/Domains/Finance/Models/Payment.php

@ -0,0 +1,54 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\PaymentDirection;
use App\Domains\Finance\Enums\PaymentStatus;
use App\Domains\Finance\Policies\PaymentPolicy;
use Database\Factories\Finance\PaymentFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'virtual_account',
'bank_ref_id',
'amount',
'valid_until',
'direction',
'status',
'transaction_id',
])]
#[UsePolicy(PaymentPolicy::class)]
class Payment extends Model
{
use HasFactory;
use HasUlids;
protected $casts = [
'amount' => MoneyCurrency::class,
'valid_until' => 'datetime_immutable',
'direction' => PaymentDirection::class,
'status' => PaymentStatus::class,
];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory
{
return PaymentFactory::new();
}
public function transaction(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
}

24
app/Domains/Finance/Models/ProductMapping.php

@ -0,0 +1,24 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Policies\ProductMappingPolicy;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
#[Fillable([
'code',
'name',
'chart_of_account_id',
'client_id',
])]
#[UsePolicy(ProductMappingPolicy::class)]
class ProductMapping extends Model
{
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
}

47
app/Domains/Finance/Models/StudentInvoiceReport.php

@ -0,0 +1,47 @@
<?php
namespace App\Domains\Finance\Models;
use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Policies\StudentInvoiceReportPolicy;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Model;
#[Fillable([
'invoice_id',
'student_id',
'name',
'faculty_id',
'faculty_name',
'study_program_id',
'study_program_name',
'term_id',
'term_name',
'amount',
'paid_amount',
'issued_at',
'paid_at',
'status',
])]
#[UsePolicy(StudentInvoiceReportPolicy::class)]
class StudentInvoiceReport extends Model
{
public static function boot(): void
{
self::boot();
static::creating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot create invoice report from here, use event instead.'));
static::deleting(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot delete invoice report.'));
static::updating(fn (InvoiceReport $invoiceReport) => throw new BadMethodCallException('Cannot update invoice report.'));
}
protected $casts = [
'amount' => MoneyCurrency::class,
'paid_amount' => MoneyCurrency::class,
'issued_at' => 'datetime',
'paid_at' => 'datetime',
'status' => InvoiceStatus::class,
];
}

49
app/Domains/Finance/Policies/ChartOfAccountPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\ChartOfAccount;
use App\Domains\Identity\Models\User;
class ChartOfAccountPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('chart-of-account.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('chart-of-account.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('chart-of-account.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('chart-of-account.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, ChartOfAccount $model): bool
{
return $user->hasPermissionTo('chart-of-account.delete');
}
}

49
app/Domains/Finance/Policies/InvoicePolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\Invoice;
use App\Domains\Identity\Models\User;
class InvoicePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('invoice.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Invoice $model): bool
{
return $user->hasPermissionTo('invoice.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('invoice.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Invoice $model): bool
{
return $user->hasPermissionTo('invoice.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Invoice $model): bool
{
return $user->hasPermissionTo('invoice.delete');
}
}

49
app/Domains/Finance/Policies/InvoiceReportPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\InvoiceReport;
use App\Domains\Identity\Models\User;
class InvoiceReportPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('invoice-report.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, InvoiceReport $model): bool
{
return $user->hasPermissionTo('invoice-report.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, InvoiceReport $model): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, InvoiceReport $model): bool
{
return false;
}
}

49
app/Domains/Finance/Policies/PaymentPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\Payment;
use App\Domains\Identity\Models\User;
class PaymentPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('payment.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Payment $model): bool
{
return $user->hasPermissionTo('payment.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('payment.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Payment $model): bool
{
return $user->hasPermissionTo('payment.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Payment $model): bool
{
return $user->hasPermissionTo('payment.delete');
}
}

49
app/Domains/Finance/Policies/ProductMappingPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\ProductMapping;
use App\Domains\Identity\Models\User;
class ProductMappingPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('product-mapping.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, ProductMapping $model): bool
{
return $user->hasPermissionTo('product-mapping.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('product-mapping.create');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, ProductMapping $model): bool
{
return $user->hasPermissionTo('product-mapping.update');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, ProductMapping $model): bool
{
return $user->hasPermissionTo('product-mapping.delete');
}
}

49
app/Domains/Finance/Policies/StudentInvoiceReportPolicy.php

@ -0,0 +1,49 @@
<?php
namespace App\Domains\Finance\Policies;
use App\Domains\Finance\Models\StudentInvoiceReport;
use App\Domains\Identity\Models\User;
class StudentInvoiceReportPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('student-invoice-report.viewAny');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, StudentInvoiceReport $model): bool
{
return $user->hasPermissionTo('student-invoice-report.view');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, StudentInvoiceReport $model): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, StudentInvoiceReport $model): bool
{
return false;
}
}

23
app/Domains/Finance/Providers/FinanceServiceProvider.php

@ -0,0 +1,23 @@
<?php
namespace App\Domains\Finance\Providers;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
class FinanceServiceProvider extends ServiceProvider
{
use RegistersDomainEvents;
protected array $listen = [];
public function register(): void
{
$this->app->register(RelationshipServiceProvider::class);
}
public function boot(): void
{
$this->registerEvents();
}
}

31
app/Domains/Finance/Providers/RelationshipServiceProvider.php

@ -0,0 +1,31 @@
<?php
namespace App\Domains\Finance\Providers;
use Illuminate\Support\ServiceProvider;
class RelationshipServiceProvider extends ServiceProvider
{
/**
* Boot cross-domain runtime relationships.
*
* This provider is strictly dedicated to runtime, decoupled cross-domain
* relationship definitions. Use Model::resolveRelationUsing() here to wire
* explicit polymorphic or foreign-key macros between models that live in
* different domains — without introducing a hard compile-time dependency
* between those domains.
*
* Do NOT add route registrations, bindings, event listeners, or any other
* bootstrapping logic here. Keep this file focused on relationships only.
*
* Example:
*
* Order::resolveRelationUsing('customer', fn (Order $order) =>
* $order->belongsTo(Customer::class, 'customer_id')
* );
*/
public function boot(): void
{
// Register cross-domain relationships below.
}
}

35
app/Domains/Finance/Support/ValueObjects/Money.php

@ -0,0 +1,35 @@
<?php
namespace App\Domains\Finance\Support\ValueObjects;
use InvalidArgumentException;
use NumberFormatter;
use Stringable;
class Money implements Stringable
{
public function __construct(public float $amount)
{
if ($this->amount < 0) {
throw new InvalidArgumentException('Money cannot be negative.');
}
}
/**
* Format the bytes into a human-readable string.
*/
public function format(string $currency = 'IDR'): string
{
$format = new NumberFormatter(app()->getLocale(), NumberFormatter::CURRENCY);
return $format->formatCurrency($this->amount, $currency);
}
/**
* Automatically format when echoed in Blade (e.g., {{ $model->amount }}).
*/
public function __toString(): string
{
return $this->format();
}
}

26
app/Domains/Identity/Actions/AccessControl/DefineSystemRole.php

@ -0,0 +1,26 @@
<?php
namespace App\Domains\Identity\Actions\AccessControl;
use App\Domains\Identity\DTOs\AccessControl\CreateRoleDTO;
use App\Domains\Identity\Models\Role;
use Illuminate\Support\Facades\DB;
use Throwable;
class DefineSystemRole
{
/**
* @throws Throwable
*/
public function execute(CreateRoleDTO $dto): bool
{
return DB::transaction(function () use ($dto) {
$role = Role::create([
'name' => $dto->name,
]);
$role->syncPermissions($dto->permissions);
return true;
});
}
}

26
app/Domains/Identity/Actions/AccessControl/DeleteSystemRole.php

@ -0,0 +1,26 @@
<?php
namespace App\Domains\Identity\Actions\AccessControl;
use App\Domains\Identity\Enums\RoleType;
use App\Domains\Identity\Models\Role;
use Exception;
use function in_array;
class DeleteSystemRole
{
/**
* @throws Exception
*/
public function execute(Role $role): void
{
if (in_array($role->name, [RoleType::ADMIN->value, RoleType::SYSTEM_ADMIN->value])) {
throw new Exception(__('domains/identity/messages.exceptions.cannot_remove_system_role'));
}
if ($role->users()->exists()) {
throw new Exception(__('domains/identity/messages.exceptions.role_has_users'));
}
}
}

22
app/Domains/Identity/Actions/Governance/DeleteUser.php

@ -0,0 +1,22 @@
<?php
namespace App\Domains\Identity\Actions\Governance;
use App\Domains\Identity\Models\User;
class DeleteUser
{
public function __construct(
protected PurgeUser $purgeUser,
protected SuspendUser $suspendUser
) {}
public function execute(User $user): void
{
if ($user->status->isActive()) {
$this->suspendUser->execute($user);
} else {
$this->purgeUser->execute($user);
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save