Browse Source

feat: introduce integration mappers and synchronize Academic and Admission domains

- Implement mappers for Academic (Faculty, StudyProgram, Term) and Admission (AcademicProgram, AdmissionTrack, AdmissionSchedule, FeeType) domains.
- Add registry bindings in service providers for API synchronization.
- Enhance models to use HasPublicUlid and clean up ULID-related traits.
- Update DTOs and actions to accommodate integration features and improve mapping flexibility.
- Standardize billing cycle parsing with PMB label support.
master
Syarif Ubaidillah 2 weeks ago
parent
commit
a496679cb1
  1. 1
      app/Domains/Academic/Actions/Curriculum/ModifyFacultyDetails.php
  2. 2
      app/Domains/Academic/DTOs/Curriculum/RegisterStudyProgramDTO.php
  3. 64
      app/Domains/Academic/Integration/Mappers/FacultyDataMapper.php
  4. 82
      app/Domains/Academic/Integration/Mappers/StudyProgramDataMapper.php
  5. 72
      app/Domains/Academic/Integration/Mappers/TermDataMapper.php
  6. 9
      app/Domains/Academic/Models/Faculty.php
  7. 9
      app/Domains/Academic/Models/StudyProgram.php
  8. 9
      app/Domains/Academic/Models/Term.php
  9. 18
      app/Domains/Academic/Providers/AcademicServiceProvider.php
  10. 1
      app/Domains/Admission/Actions/IntakeSourcing/InitializeAdmissionTrack.php
  11. 1
      app/Domains/Admission/DTOs/IntakeSourcing/InitializeAdmissionTrackDTO.php
  12. 9
      app/Domains/Admission/Enums/BillingCycle.php
  13. 34
      app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php
  14. 78
      app/Domains/Admission/Integration/Mappers/AdmissionScheduleDataMapper.php
  15. 31
      app/Domains/Admission/Integration/Mappers/AdmissionTrackDataMapper.php
  16. 32
      app/Domains/Admission/Integration/Mappers/FeeTypeDataMapper.php
  17. 9
      app/Domains/Admission/Models/AcademicProgram.php
  18. 9
      app/Domains/Admission/Models/AdmissionSchedule.php
  19. 11
      app/Domains/Admission/Models/AdmissionTrack.php
  20. 9
      app/Domains/Admission/Models/FeeRate.php
  21. 9
      app/Domains/Admission/Models/FeeType.php
  22. 23
      app/Domains/Admission/Providers/AdmissionServiceProvider.php
  23. 24
      app/Domains/Channel/Actions/Api/SynchronizeWithApi.php
  24. 2
      app/Domains/Channel/Actions/AppProvisioning/ProvisionConsumerApp.php
  25. 5
      app/Domains/Channel/DTOs/AppProvisioning/ProvisionConsumerAppDTO.php
  26. 9
      app/Domains/Channel/Enums/Api/ApiType.php
  27. 9
      app/Domains/Channel/Models/Client.php
  28. 39
      app/Domains/Channel/Support/Mapper/ApiMapperRegistry.php
  29. 9
      app/Domains/Finance/Models/ChartOfAccount.php
  30. 9
      app/Domains/Finance/Models/Invoice.php
  31. 9
      app/Domains/Finance/Models/Payment.php
  32. 9
      app/Domains/Identity/Integration/Mappers/UserDataMapper.php
  33. 8
      app/Domains/Identity/Models/Role.php
  34. 9
      app/Domains/Identity/Models/User.php
  35. 101
      app/Domains/System/Actions/Integration/RunGenericImportPipeline.php
  36. 11
      app/Domains/System/Support/Integration/DataPayloadMapper.php
  37. 20
      app/Domains/System/Traits/Model/HasPublicUlid.php
  38. 3
      app/Domains/System/Traits/Model/HasSlugs.php
  39. 2
      app/Http/DataTables/Academic/TermDataTable.php
  40. 4
      app/Http/Ingestion/Excel/Identity/UserImport.php
  41. 1
      database/migrations/2026_04_23_045248_create_permission_tables.php
  42. 2
      database/seeders/DatabaseSeeder.php
  43. 4
      database/seeders/UserSeeder.php
  44. 8
      lang/en/domains/channel/messages.php
  45. 13
      resources/js/plugin/sweetalert2.js
  46. 2
      resources/views/components/modal.blade.php
  47. 6
      resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.blade.php
  48. 16
      resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.php
  49. 6
      resources/views/pages/channel/client/⚡form-modal/form-modal.blade.php

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

@ -12,6 +12,7 @@ class ModifyFacultyDetails
$faculty->update([ $faculty->update([
'name' => $dto->name, 'name' => $dto->name,
'code' => $dto->code, 'code' => $dto->code,
'status',
]); ]);
} }
} }

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

@ -12,6 +12,6 @@ readonly class RegisterStudyProgramDTO
public string $level, public string $level,
public LifecycleStatus $status, public LifecycleStatus $status,
public int $facultyId, public int $facultyId,
public int $externalId, public string $externalId,
) {} ) {}
} }

64
app/Domains/Academic/Integration/Mappers/FacultyDataMapper.php

@ -0,0 +1,64 @@
<?php
namespace App\Domains\Academic\Integration\Mappers;
use App\Domains\Academic\Actions\Curriculum\EstablishFaculty;
use App\Domains\Academic\Actions\Curriculum\ModifyFacultyDetails;
use App\Domains\Academic\DTOs\Curriculum\EstablishFacultyDTO;
use App\Domains\Academic\DTOs\Curriculum\ModifyFacultyDetailsDTO;
use App\Domains\Academic\Models\Faculty;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model;
class FacultyDataMapper implements DataPayloadMapper
{
public function __construct(
protected EstablishFaculty $establishFaculty,
protected ModifyFacultyDetails $modifyFacultyDetails,
)
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'external_id';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return [
'name' => $rawData['nama_fakultas'],
'code' => $rawData['kode_fakultas'],
'external_id' => $rawData['id_fakultas'],
];
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
/** @var Faculty $faculty */
$faculty = $existingModel;
if ($faculty) {
$this->modifyFacultyDetails->execute($faculty, new ModifyFacultyDetailsDTO(
name: $payload['name'],
code: $payload['code'],
));
} else {
$this->establishFaculty->execute(dto: new EstablishFacultyDTO(
name: $payload['name'],
code: $payload['code'],
externalId: $payload['external_id'],
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return Faculty::class;
}
}

82
app/Domains/Academic/Integration/Mappers/StudyProgramDataMapper.php

@ -0,0 +1,82 @@
<?php
namespace App\Domains\Academic\Integration\Mappers;
use App\Domains\Academic\Actions\Curriculum\AdjustStudyProgramDetails;
use App\Domains\Academic\Actions\Curriculum\RegisterStudyProgram;
use App\Domains\Academic\DTOs\Curriculum\AdjustStudyProgramDTO;
use App\Domains\Academic\DTOs\Curriculum\RegisterStudyProgramDTO;
use App\Domains\Academic\Models\Faculty;
use App\Domains\Academic\Models\StudyProgram;
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model;
class StudyProgramDataMapper implements DataPayloadMapper
{
private static array $facultyCache = [];
public function __construct(
protected RegisterStudyProgram $registerStudyProgram,
protected AdjustStudyProgramDetails $adjustStudyProgramDetails,
)
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'external_id';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return [
'name' => $rawData['nama_program_studi'],
'code' => $rawData['kode_program_studi'],
'level' => $rawData['nama_jenjang_pendidikan'],
'faculty_id' => self::resolveFacultyId($rawData['id_fakultas']),
'external_id' => $rawData['id_prodi'],
];
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
/** @var StudyProgram $studyProgram */
$studyProgram = $existingModel;
if ($studyProgram) {
$this->adjustStudyProgramDetails->execute($studyProgram, new AdjustStudyProgramDTO(
level: $payload['level'],
status: LifecycleStatus::ACTIVE,
));
} else {
$this->registerStudyProgram->execute(new RegisterStudyProgramDTO(
name: $payload['name'],
code: $payload['code'],
level: $payload['level'],
status: LifecycleStatus::ACTIVE,
facultyId: $payload['faculty_id'],
externalId: $payload['external_id'],
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return StudyProgram::class;
}
private function resolveFacultyId(string $externalId): int {
if (!isset(self::$facultyCache[$externalId])) {
self::$facultyCache[$externalId] = Faculty::where('external_id', $externalId)->first()->id;
}
return self::$facultyCache[$externalId];
}
}

72
app/Domains/Academic/Integration/Mappers/TermDataMapper.php

@ -0,0 +1,72 @@
<?php
namespace App\Domains\Academic\Integration\Mappers;
use App\Domains\Academic\Actions\Calendar\InitializeAcademicTerm;
use App\Domains\Academic\Actions\Calendar\ModifyTermDate;
use App\Domains\Academic\DTOs\Calendar\InitializeAcademicTermDTO;
use App\Domains\Academic\DTOs\Calendar\ModifyTermDateDTO;
use App\Domains\Academic\Models\Term;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
class TermDataMapper implements DataPayloadMapper
{
public function __construct(
protected InitializeAcademicTerm $initializeAcademicTerm,
protected ModifyTermDate $modifyTermDate,
)
{
// Inject required Domain and Cross-Domain Actions via constructor composition
}
public function getLookupKey(): string
{
// Return the unique string key used to identify existing records (e.g., 'email')
return 'code';
}
public function transform(array $rawData): array
{
// Normalize incoming data array formats into an internal domain-safe layout
return [
'name' => $rawData['nama_semester'],
'code' => $rawData['id_semester'],
'year_study' => $rawData['id_tahun_ajaran'],
'semester' => $rawData['semester'],
'date_start' => $rawData['tanggal_mulai'],
'date_end' => $rawData['tanggal_selesai'],
];
}
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{
/** @var Term $term */
$term = $existingModel;
if(isset($term)) {
$this->modifyTermDate->execute($term, new ModifyTermDateDTO(
startDate: CarbonImmutable::parse($payload['date_start']),
endDate: CarbonImmutable::parse($payload['date_end']),
));
} else {
$this->initializeAcademicTerm->execute(new InitializeAcademicTermDTO(
name: $payload['name'],
code: $payload['code'],
yearStudy: $payload['year_study'],
semester: $payload['semester'],
startDate: CarbonImmutable::parse($payload['date_start']),
endDate: CarbonImmutable::parse($payload['date_end']),
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return Term::class;
}
}

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

@ -4,10 +4,10 @@ namespace App\Domains\Academic\Models;
use App\Domains\Academic\Policies\FacultyPolicy; use App\Domains\Academic\Policies\FacultyPolicy;
use App\Domains\System\Enums\LifecycleStatus; use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Academic\FacultyFactory; use Database\Factories\Academic\FacultyFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\Model;
class Faculty extends Model class Faculty extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'status' => LifecycleStatus::class, 'status' => LifecycleStatus::class,
@ -27,9 +27,4 @@ class Faculty extends Model
{ {
return FacultyFactory::new(); return FacultyFactory::new();
} }
public function uniqueIds(): array
{
return ['ulid'];
}
} }

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

@ -4,10 +4,10 @@ namespace App\Domains\Academic\Models;
use App\Domains\Academic\Policies\StudyProgramPolicy; use App\Domains\Academic\Policies\StudyProgramPolicy;
use App\Domains\System\Enums\LifecycleStatus; use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Academic\StudyProgramFactory; use Database\Factories\Academic\StudyProgramFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -25,7 +25,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class StudyProgram extends Model class StudyProgram extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'status' => LifecycleStatus::class, 'status' => LifecycleStatus::class,
@ -36,11 +36,6 @@ class StudyProgram extends Model
return StudyProgramFactory::new(); return StudyProgramFactory::new();
} }
public function uniqueIds(): array
{
return ['ulid'];
}
public function faculty(): BelongsTo public function faculty(): BelongsTo
{ {
return $this->belongsTo(Faculty::class); return $this->belongsTo(Faculty::class);

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

@ -3,10 +3,10 @@
namespace App\Domains\Academic\Models; namespace App\Domains\Academic\Models;
use App\Domains\Academic\Policies\TermPolicy; use App\Domains\Academic\Policies\TermPolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Academic\TermFactory; use Database\Factories\Academic\TermFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -16,7 +16,7 @@ use Illuminate\Database\Eloquent\Model;
class Term extends Model class Term extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'date_start' => 'date:d/M/Y', 'date_start' => 'date:d/M/Y',
@ -27,9 +27,4 @@ class Term extends Model
{ {
return TermFactory::new(); return TermFactory::new();
} }
public function uniqueIds(): array
{
return ['ulid'];
}
} }

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

@ -2,6 +2,11 @@
namespace App\Domains\Academic\Providers; namespace App\Domains\Academic\Providers;
use App\Domains\Academic\Integration\Mappers\FacultyDataMapper;
use App\Domains\Academic\Integration\Mappers\StudyProgramDataMapper;
use App\Domains\Academic\Integration\Mappers\TermDataMapper;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Support\Mapper\ApiMapperRegistry;
use App\Domains\System\Traits\Provider\RegistersDomainEvents; use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -13,6 +18,19 @@ class AcademicServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
ApiMapperRegistry::register(
type: ApiType::FACULTY,
mapperClass: FacultyDataMapper::class,
);
ApiMapperRegistry::register(
type: ApiType::STUDY_PROGRAM,
mapperClass: StudyProgramDataMapper::class,
);
ApiMapperRegistry::register(
type: ApiType::TERM,
mapperClass: TermDataMapper::class,
);
$this->app->register(RelationshipServiceProvider::class); $this->app->register(RelationshipServiceProvider::class);
} }

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

@ -11,6 +11,7 @@ class InitializeAdmissionTrack
{ {
AdmissionTrack::create([ AdmissionTrack::create([
'name' => $dto->name, 'name' => $dto->name,
'slug' => $dto->slug,
'status' => $dto->status, 'status' => $dto->status,
]); ]);
} }

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

@ -8,6 +8,7 @@ readonly class InitializeAdmissionTrackDTO
{ {
public function __construct( public function __construct(
public string $name, public string $name,
public string $slug,
public LifecycleStatus $status, public LifecycleStatus $status,
) {} ) {}
} }

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

@ -15,4 +15,13 @@ enum BillingCycle: string implements HasLabel
case MONTHLY = 'monthly'; case MONTHLY = 'monthly';
case SEMESTER = 'semester'; case SEMESTER = 'semester';
case ANNUAL = 'annual'; case ANNUAL = 'annual';
public static function tryFromPMB(string $label): ?self
{
return match ($label) {
'Awal Masuk', 'Tagihan Insidentil' => self::ONCE,
'Tagihan per Semester' => self::SEMESTER,
default => self::MONTHLY,
};
}
} }

34
app/Domains/Admission/Integration/Mappers/AcademicProgramDataMapper.php

@ -2,12 +2,18 @@
namespace App\Domains\Admission\Integration\Mappers; namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\Admission\Actions\StudyModeProvisioning\DefineAcademicProgram;
use App\Domains\Admission\DTOs\StudyModeProvisioning\DefineAcademicProgramDTO;
use App\Domains\Admission\Models\AcademicProgram;
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Support\Integration\DataPayloadMapper; use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class AcademicProgramDataMapper implements DataPayloadMapper class AcademicProgramDataMapper implements DataPayloadMapper
{ {
public function __construct() public function __construct(
protected DefineAcademicProgram $defineAcademicProgram,
)
{ {
// Inject required Domain and Cross-Domain Actions via constructor composition // Inject required Domain and Cross-Domain Actions via constructor composition
} }
@ -15,17 +21,37 @@ class AcademicProgramDataMapper implements DataPayloadMapper
public function getLookupKey(): string public function getLookupKey(): string
{ {
// Return the unique string key used to identify existing records (e.g., 'email') // Return the unique string key used to identify existing records (e.g., 'email')
return 'id'; return 'code';
} }
public function transform(array $rawData): array public function transform(array $rawData): array
{ {
// Normalize incoming data array formats into an internal domain-safe layout // Normalize incoming data array formats into an internal domain-safe layout
return $rawData; return [
'name' => $rawData['nama_Skuliah'],
'code' => $rawData['kode_Skuliah'],
];
} }
/**
* @param AcademicProgram|null $existingModel
*/
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{ {
// Allocate mapped payloads into strict DTOs and fire internal/external Domain Actions if(is_null($existingModel)) {
$this->defineAcademicProgram->execute(new DefineAcademicProgramDTO(
name: $payload['name'],
code: $payload['code'],
status: LifecycleStatus::ACTIVE,
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return AcademicProgram::class;
} }
} }

78
app/Domains/Admission/Integration/Mappers/AdmissionScheduleDataMapper.php

@ -2,30 +2,98 @@
namespace App\Domains\Admission\Integration\Mappers; namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\Academic\Models\Term;
use App\Domains\Admission\Actions\IntakeScheduling\AdjustAdmissionScheduleDuration;
use App\Domains\Admission\Actions\IntakeScheduling\DefineAdmissionSchedule;
use App\Domains\Admission\DTOs\IntakeScheduling\AdjustAdmissionScheduleDurationDTO;
use App\Domains\Admission\DTOs\IntakeScheduling\DefineAdmissionScheduleDTO;
use App\Domains\Admission\Models\AdmissionSchedule;
use App\Domains\Admission\Models\AdmissionTrack;
use App\Domains\System\Support\Integration\DataPayloadMapper; use App\Domains\System\Support\Integration\DataPayloadMapper;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class AdmissionScheduleDataMapper implements DataPayloadMapper class AdmissionScheduleDataMapper implements DataPayloadMapper
{ {
public function __construct() private static array $cache = [
'admission_track' => [],
'term' => [],
];
public function __construct(
protected DefineAdmissionSchedule $defineAdmissionSchedule,
protected AdjustAdmissionScheduleDuration $adjustAdmissionScheduleDuration
)
{ {
// Inject required Domain and Cross-Domain Actions via constructor composition // Inject required Domain and Cross-Domain Actions via constructor composition
} }
public function getLookupKey(): string public function getLookupKey(): array
{ {
// Return the unique string key used to identify existing records (e.g., 'email') // Return the unique string key used to identify existing records (e.g., 'email')
return 'id'; return ['admission_track_id', 'term_id'];
} }
public function transform(array $rawData): array public function transform(array $rawData): array
{ {
// Normalize incoming data array formats into an internal domain-safe layout // Normalize incoming data array formats into an internal domain-safe layout
return $rawData; return [
'date_start' => $rawData['tanggal_buka'],
'date_end' => $rawData['tanggal_tutup'],
're_registration_date_start' => $rawData['tanggal_buka_daftar_ulang'],
're_registration_date_end' => $rawData['tanggal_tutup_daftar_ulang'],
'admission_track_id' => $this->resolveAdmissionTrackId($rawData['slug']),
'term_id' => $this->resolveTermId($rawData['angkatan']),
];
} }
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{ {
// Allocate mapped payloads into strict DTOs and fire internal/external Domain Actions /** @var AdmissionSchedule $admissionSchedule */
$admissionSchedule = $existingModel;
if ($admissionSchedule) {
$this->adjustAdmissionScheduleDuration->execute($admissionSchedule, new AdjustAdmissionScheduleDurationDTO(
startData: CarbonImmutable::parse($payload['date_start']),
endData: CarbonImmutable::parse($payload['date_end']),
reRegisterStartData: CarbonImmutable::parse($payload['re_registration_date_start']),
reRegisterEndData: CarbonImmutable::parse($payload['re_registration_date_end']),
));
} else {
;
$this->defineAdmissionSchedule->execute(new DefineAdmissionScheduleDTO(
startData: CarbonImmutable::parse($payload['date_start']),
endData: CarbonImmutable::parse($payload['date_end']),
reRegisterStartData: CarbonImmutable::parse($payload['re_registration_date_start']),
reRegisterEndData: CarbonImmutable::parse($payload['re_registration_date_end']),
termId: $payload['term_id'],
admissionTrackId: $payload['admission_track_id'],
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return AdmissionSchedule::class;
}
private function resolveAdmissionTrackId(string $slug): int {
if (!isset(self::$cache['admission_track'][$slug])) {
self::$cache['admission_track'][$slug] = AdmissionTrack::where('slug', str($slug)->slug())->first()?->id;
}
return self::$cache['admission_track'][$slug];
}
private function resolveTermId(int $yearStudy): int {
if (!isset(self::$cache['term'][$yearStudy])) {
self::$cache['term'][$yearStudy] = Term::where('year_study', $yearStudy)
->where('semester', 1)
->first()?->id ?? 0;
}
return self::$cache['term'][$yearStudy];
} }
} }

31
app/Domains/Admission/Integration/Mappers/AdmissionTrackDataMapper.php

@ -2,12 +2,18 @@
namespace App\Domains\Admission\Integration\Mappers; namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\Admission\Actions\IntakeSourcing\InitializeAdmissionTrack;
use App\Domains\Admission\DTOs\IntakeSourcing\InitializeAdmissionTrackDTO;
use App\Domains\Admission\Models\AdmissionTrack;
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Support\Integration\DataPayloadMapper; use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class AdmissionTrackDataMapper implements DataPayloadMapper class AdmissionTrackDataMapper implements DataPayloadMapper
{ {
public function __construct() public function __construct(
protected InitializeAdmissionTrack $initializeAdmissionTrack,
)
{ {
// Inject required Domain and Cross-Domain Actions via constructor composition // Inject required Domain and Cross-Domain Actions via constructor composition
} }
@ -15,17 +21,34 @@ class AdmissionTrackDataMapper implements DataPayloadMapper
public function getLookupKey(): string public function getLookupKey(): string
{ {
// Return the unique string key used to identify existing records (e.g., 'email') // Return the unique string key used to identify existing records (e.g., 'email')
return 'id'; return 'slug';
} }
public function transform(array $rawData): array public function transform(array $rawData): array
{ {
// Normalize incoming data array formats into an internal domain-safe layout // Normalize incoming data array formats into an internal domain-safe layout
return $rawData; return [
'name' => $rawData['nama_jalur'],
'slug' => str($rawData['slug'])->slug(),
];
} }
public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{ {
// Allocate mapped payloads into strict DTOs and fire internal/external Domain Actions if (!$existingModel) {
$this->initializeAdmissionTrack->execute(new InitializeAdmissionTrackDTO(
name: $payload['name'],
slug: $payload['slug'],
status: LifecycleStatus::ACTIVE,
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return AdmissionTrack::class;
} }
} }

32
app/Domains/Admission/Integration/Mappers/FeeTypeDataMapper.php

@ -2,13 +2,18 @@
namespace App\Domains\Admission\Integration\Mappers; namespace App\Domains\Admission\Integration\Mappers;
use App\Domains\Admission\Actions\AdmissionPricing\DefineFeeType;
use App\Domains\Admission\DTOs\AdmissionPricing\FeeTypeDTO;
use App\Domains\Admission\Enums\BillingCycle;
use App\Domains\Admission\Models\FeeType; use App\Domains\Admission\Models\FeeType;
use App\Domains\System\Support\Integration\DataPayloadMapper; use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class FeeTypeDataMapper implements DataPayloadMapper class FeeTypeDataMapper implements DataPayloadMapper
{ {
public function __construct() public function __construct(
protected DefineFeeType $defineFeeType
)
{ {
// Inject required Domain and Cross-Domain Actions via constructor composition // Inject required Domain and Cross-Domain Actions via constructor composition
} }
@ -16,23 +21,36 @@ class FeeTypeDataMapper implements DataPayloadMapper
public function getLookupKey(): string public function getLookupKey(): string
{ {
// Return the unique string key used to identify existing records (e.g., 'email') // Return the unique string key used to identify existing records (e.g., 'email')
return 'id'; return 'name';
} }
public function transform(array $rawData): array public function transform(array $rawData): array
{ {
// Normalize incoming data array formats into an internal domain-safe layout // Normalize incoming data array formats into an internal domain-safe layout
return [ return [
'name' => $rawData['name'], 'name' => $rawData['nama_Jtagihan'],
'billing_cycle' => $rawData['billing_cycle'], 'billing_cycle' => $rawData['periodesasi_Jtagihan'],
]; ];
} }
/** /**
* @param FeeType|null $model * @param FeeType|null $existingModel
*/ */
public function updateOrCreateDomainState(array $payload, ?Model $model = null): void public function updateOrCreateDomainState(array $payload, ?Model $existingModel = null): void
{ {
// if(!$existingModel) {
$this->defineFeeType->execute(new FeeTypeDTO(
name: $payload['name'],
billingCycle: BillingCycle::tryFromPMB($payload['billing_cycle']),
));
}
}
/**
* @return class-string
*/
public function getModelClass(): string
{
return FeeType::class;
} }
} }

9
app/Domains/Admission/Models/AcademicProgram.php

@ -4,11 +4,11 @@ namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\AcademicProgramPolicy; use App\Domains\Admission\Policies\AcademicProgramPolicy;
use App\Domains\System\Enums\LifecycleStatus; use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasPublicUlid;
use App\Domains\System\Traits\Model\HasSlugs; use App\Domains\System\Traits\Model\HasSlugs;
use Database\Factories\Academic\AcademicProgramFactory; use Database\Factories\Academic\AcademicProgramFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -18,8 +18,8 @@ use Illuminate\Database\Eloquent\Model;
class AcademicProgram extends Model class AcademicProgram extends Model
{ {
use HasFactory; use HasFactory;
use HasPublicUlid;
use HasSlugs; use HasSlugs;
use HasUlids;
protected $casts = [ protected $casts = [
'status' => LifecycleStatus::class, 'status' => LifecycleStatus::class,
@ -30,11 +30,6 @@ class AcademicProgram extends Model
return AcademicProgramFactory::new(); return AcademicProgramFactory::new();
} }
public function uniqueIds(): array
{
return ['ulid'];
}
public function sluggable(): string public function sluggable(): string
{ {
return 'name'; return 'name';

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

@ -4,10 +4,10 @@ namespace App\Domains\Admission\Models;
use App\Domains\Admission\Enums\AdmissionStatus; use App\Domains\Admission\Enums\AdmissionStatus;
use App\Domains\Admission\Policies\AdmissionSchedulePolicy; use App\Domains\Admission\Policies\AdmissionSchedulePolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Admission\AdmissionScheduleFactory; use Database\Factories\Admission\AdmissionScheduleFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -26,7 +26,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AdmissionSchedule extends Model class AdmissionSchedule extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'date_start' => 'date:d/M/Y', 'date_start' => 'date:d/M/Y',
@ -36,11 +36,6 @@ class AdmissionSchedule extends Model
'status' => AdmissionStatus::class, 'status' => AdmissionStatus::class,
]; ];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory protected static function newFactory(): Factory
{ {
return AdmissionScheduleFactory::new(); return AdmissionScheduleFactory::new();

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

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

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

@ -4,10 +4,10 @@ namespace App\Domains\Admission\Models;
use App\Domains\Admission\Policies\FeeRatePolicy; use App\Domains\Admission\Policies\FeeRatePolicy;
use App\Domains\Finance\Casts\MoneyCurrency; use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Admission\FeeRateFactory; use Database\Factories\Admission\FeeRateFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -24,17 +24,12 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class FeeRate extends Model class FeeRate extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'amount' => MoneyCurrency::class, 'amount' => MoneyCurrency::class,
]; ];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory protected static function newFactory(): Factory
{ {
return FeeRateFactory::new(); return FeeRateFactory::new();

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

@ -4,10 +4,10 @@ namespace App\Domains\Admission\Models;
use App\Domains\Admission\Enums\BillingCycle; use App\Domains\Admission\Enums\BillingCycle;
use App\Domains\Admission\Policies\FeeTypePolicy; use App\Domains\Admission\Policies\FeeTypePolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Admission\FeeTypeFactory; use Database\Factories\Admission\FeeTypeFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -17,17 +17,12 @@ use Illuminate\Database\Eloquent\Model;
class FeeType extends Model class FeeType extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'billing_cycle' => BillingCycle::class, 'billing_cycle' => BillingCycle::class,
]; ];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory protected static function newFactory(): Factory
{ {
return FeeTypeFactory::new(); return FeeTypeFactory::new();

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

@ -2,6 +2,12 @@
namespace App\Domains\Admission\Providers; namespace App\Domains\Admission\Providers;
use App\Domains\Admission\Integration\Mappers\AcademicProgramDataMapper;
use App\Domains\Admission\Integration\Mappers\AdmissionScheduleDataMapper;
use App\Domains\Admission\Integration\Mappers\AdmissionTrackDataMapper;
use App\Domains\Admission\Integration\Mappers\FeeTypeDataMapper;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Support\Mapper\ApiMapperRegistry;
use App\Domains\System\Traits\Provider\RegistersDomainEvents; use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -13,6 +19,23 @@ class AdmissionServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
ApiMapperRegistry::register(
type: ApiType::ADMISSION_TRACK,
mapperClass: AdmissionTrackDataMapper::class,
);
ApiMapperRegistry::register(
type: ApiType::ACADEMIC_PROGRAM,
mapperClass: AcademicProgramDataMapper::class,
);
ApiMapperRegistry::register(
type: ApiType::ADMISSION_SCHEDULE,
mapperClass: AdmissionScheduleDataMapper::class,
);
ApiMapperRegistry::register(
type: ApiType::FEE_TYPE,
mapperClass: FeeTypeDataMapper::class,
);
$this->app->register(RelationshipServiceProvider::class); $this->app->register(RelationshipServiceProvider::class);
} }

24
app/Domains/Channel/Actions/Api/SynchronizeWithApi.php

@ -4,12 +4,22 @@ namespace App\Domains\Channel\Actions\Api;
use App\Domains\Channel\Enums\Api\ApiType; use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Models\ApiEndpoint; use App\Domains\Channel\Models\ApiEndpoint;
use App\Domains\Channel\Support\Mapper\ApiMapperRegistry;
use App\Domains\System\Actions\Integration\RunGenericImportPipeline;
use Exception;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class SynchronizeWithApi class SynchronizeWithApi
{ {
/**
* @throws Exception
*/
public function execute(ApiType $apiType): void public function execute(ApiType $apiType): void
{ {
try {
$mapper = ApiMapperRegistry::resolve($apiType);
$endpoint = ApiEndpoint::query() $endpoint = ApiEndpoint::query()
->with('client') ->with('client')
->where('key', $apiType) ->where('key', $apiType)
@ -19,10 +29,20 @@ class SynchronizeWithApi
$url = $endpoint->value; $url = $endpoint->value;
$credentials = $endpoint->client->credentials; $credentials = $endpoint->client->credentials;
$authType = $endpoint->client->auth_type; $authType = $endpoint->client->auth_type;
$url = "https://$domain$url"; $url = "$domain$url";
$header = $authType->buildCredentials($credentials); $header = $authType->buildCredentials($credentials);
$response = Http::withHeaders($header)->get($url); $response = Http::withHeaders($header)->get($url);
dd($response); $result = $response->json()['data'];
app(RunGenericImportPipeline::class)->execute(
rows: collect($result),
mapper: $mapper,
);
} catch (Exception $e) {
Log::error('Failed to synchronize with API: '.$e->getMessage());
throw new Exception(__('domains/channel/messages.clients.fail_to_sync', ['error' => $e->getMessage()]));
}
} }
} }

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

@ -14,7 +14,7 @@ class ProvisionConsumerApp
'domain' => $dto->domain, 'domain' => $dto->domain,
'webhook' => $dto->webhook_url, 'webhook' => $dto->webhook_url,
'auth_type' => $dto->auth_type, 'auth_type' => $dto->auth_type,
'credentials' => $dto->credentials, 'credentials' => $dto->credentials ?? [],
'secret' => encrypt(uuid_create()), 'secret' => encrypt(uuid_create()),
]); ]);
} }

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

@ -7,14 +7,11 @@ use App\Domains\Channel\Enums\Client\AuthType;
readonly class ProvisionConsumerAppDTO readonly class ProvisionConsumerAppDTO
{ {
/**
* @param array<string, string> $credentials
*/
public function __construct( public function __construct(
public string $name, public string $name,
public string $domain, public string $domain,
public string $webhook_url, public string $webhook_url,
public AuthType $auth_type, public AuthType $auth_type,
public AuthCredentials $credentials, public ?AuthCredentials $credentials,
) {} ) {}
} }

9
app/Domains/Channel/Enums/Api/ApiType.php

@ -2,10 +2,10 @@
namespace App\Domains\Channel\Enums\Api; namespace App\Domains\Channel\Enums\Api;
use App\Domains\Channel\Support\Mapper\ApiMapperRegistry;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use App\UI\Enums\Concerns\InteractsWithLabels; use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel; use App\UI\Enums\Contracts\HasLabel;
use App\UI\Enums\InputType;
use App\UI\Support\Schemas\InputSchema;
enum ApiType: string implements HasLabel enum ApiType: string implements HasLabel
{ {
@ -18,4 +18,9 @@ enum ApiType: string implements HasLabel
case ADMISSION_SCHEDULE = 'admission_schedule'; case ADMISSION_SCHEDULE = 'admission_schedule';
case ACADEMIC_PROGRAM = 'academic_program'; case ACADEMIC_PROGRAM = 'academic_program';
case FEE_TYPE = 'fee_type'; case FEE_TYPE = 'fee_type';
public function mapper(): DataPayloadMapper
{
return ApiMapperRegistry::resolve($this);
}
} }

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

@ -4,10 +4,10 @@ namespace App\Domains\Channel\Models;
use App\Domains\Channel\Enums\Client\AuthType; use App\Domains\Channel\Enums\Client\AuthType;
use App\Domains\Channel\Policies\ClientPolicy; use App\Domains\Channel\Policies\ClientPolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Channel\ClientFactory; use Database\Factories\Channel\ClientFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -17,7 +17,7 @@ use Illuminate\Database\Eloquent\Model;
class Client extends Model class Client extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'secret' => 'encrypted', 'secret' => 'encrypted',
@ -29,9 +29,4 @@ class Client extends Model
{ {
return ClientFactory::new(); return ClientFactory::new();
} }
public function uniqueIds(): array
{
return ['ulid'];
}
} }

39
app/Domains/Channel/Support/Mapper/ApiMapperRegistry.php

@ -0,0 +1,39 @@
<?php
namespace App\Domains\Channel\Support\Mapper;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\System\Support\Integration\DataPayloadMapper;
use InvalidArgumentException;
class ApiMapperRegistry
{
/**
* @var array<string, class-string<DataPayloadMapper>>
*/
protected static array $mappers = [];
/**
* Register mapper with type-safe using enum ApiType
*/
public static function register(ApiType $type, string $mapperClass): void
{
static::$mappers[$type->value] = $mapperClass;
}
/**
* @param ApiType $type
* @throws InvalidArgumentException
* @return DataPayloadMapper
*/
public static function resolve(ApiType $type): DataPayloadMapper
{
$mapperClass = static::$mappers[$type->value] ?? null;
if (! $mapperClass || ! class_exists($mapperClass)) {
throw new InvalidArgumentException("No DataMapper registered for ApiType: {$type->value}");
}
return app($mapperClass);
}
}

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

@ -5,10 +5,10 @@ namespace App\Domains\Finance\Models;
use App\Domains\Finance\Enums\AccountClassification; use App\Domains\Finance\Enums\AccountClassification;
use App\Domains\Finance\Policies\ChartOfAccountPolicy; use App\Domains\Finance\Policies\ChartOfAccountPolicy;
use App\Domains\System\Enums\LifecycleStatus; use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Finance\ChartOfAccountFactory; use Database\Factories\Finance\ChartOfAccountFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -24,18 +24,13 @@ use Illuminate\Database\Eloquent\Model;
class ChartOfAccount extends Model class ChartOfAccount extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'classification' => AccountClassification::class, 'classification' => AccountClassification::class,
'status' => LifecycleStatus::class, 'status' => LifecycleStatus::class,
]; ];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory protected static function newFactory(): Factory
{ {
return ChartOfAccountFactory::new(); return ChartOfAccountFactory::new();

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

@ -6,10 +6,10 @@ use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\InvoiceStatus; use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType; use App\Domains\Finance\Enums\InvoiceType;
use App\Domains\Finance\Policies\InvoicePolicy; use App\Domains\Finance\Policies\InvoicePolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Finance\InvoiceFactory; use Database\Factories\Finance\InvoiceFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -30,7 +30,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Invoice extends Model class Invoice extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'amount' => MoneyCurrency::class, 'amount' => MoneyCurrency::class,
@ -38,11 +38,6 @@ class Invoice extends Model
'status' => InvoiceStatus::class, 'status' => InvoiceStatus::class,
]; ];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory protected static function newFactory(): Factory
{ {
return InvoiceFactory::new(); return InvoiceFactory::new();

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

@ -6,10 +6,10 @@ use App\Domains\Finance\Casts\MoneyCurrency;
use App\Domains\Finance\Enums\PaymentDirection; use App\Domains\Finance\Enums\PaymentDirection;
use App\Domains\Finance\Enums\PaymentStatus; use App\Domains\Finance\Enums\PaymentStatus;
use App\Domains\Finance\Policies\PaymentPolicy; use App\Domains\Finance\Policies\PaymentPolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Finance\PaymentFactory; use Database\Factories\Finance\PaymentFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -28,7 +28,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Payment extends Model class Payment extends Model
{ {
use HasFactory; use HasFactory;
use HasUlids; use HasPublicUlid;
protected $casts = [ protected $casts = [
'amount' => MoneyCurrency::class, 'amount' => MoneyCurrency::class,
@ -37,11 +37,6 @@ class Payment extends Model
'status' => PaymentStatus::class, 'status' => PaymentStatus::class,
]; ];
public function uniqueIds(): array
{
return ['ulid'];
}
protected static function newFactory(): Factory protected static function newFactory(): Factory
{ {
return PaymentFactory::new(); return PaymentFactory::new();

9
app/Domains/Identity/Integration/Mappers/UserDataMapper.php

@ -49,10 +49,10 @@ class UserDataMapper implements DataPayloadMapper
* *
* @throws Throwable * @throws Throwable
*/ */
public function updateOrCreateDomainState(array $payload, ?Model $model = null): void public function updateOrCreateDomainState(array $payload, ?Model $existingModel): void
{ {
/** @var User|null $user */ /** @var User|null $user */
$user = $model; $user = $existingModel ?? $this->getModelClass();
if ($user) { if ($user) {
$this->updateUser->execute( $this->updateUser->execute(
@ -79,4 +79,9 @@ class UserDataMapper implements DataPayloadMapper
phoneNumber: $payload['phone_number'] phoneNumber: $payload['phone_number']
); );
} }
public function getModelClass(): string
{
return User::class;
}
} }

8
app/Domains/Identity/Models/Role.php

@ -3,9 +3,9 @@
namespace App\Domains\Identity\Models; namespace App\Domains\Identity\Models;
use App\Domains\Identity\Policies\RolePolicy; use App\Domains\Identity\Policies\RolePolicy;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use OwenIt\Auditing\Contracts\Auditable; use OwenIt\Auditing\Contracts\Auditable;
use Spatie\Permission\Contracts\Role as RoleContract; use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\Models\Role as SpatieRole; use Spatie\Permission\Models\Role as SpatieRole;
@ -14,11 +14,5 @@ use Spatie\Permission\Models\Role as SpatieRole;
#[UsePolicy(RolePolicy::class)] #[UsePolicy(RolePolicy::class)]
class Role extends SpatieRole implements Auditable, RoleContract class Role extends SpatieRole implements Auditable, RoleContract
{ {
use HasUlids;
use \OwenIt\Auditing\Auditable; use \OwenIt\Auditing\Auditable;
public function uniqueIds(): array
{
return ['ulid'];
}
} }

9
app/Domains/Identity/Models/User.php

@ -7,13 +7,13 @@ use App\Domains\Identity\Notifications\ResetPasswordNotification;
use App\Domains\Identity\Notifications\VerifyEmailNotification; use App\Domains\Identity\Notifications\VerifyEmailNotification;
use App\Domains\Identity\Policies\UserPolicy; use App\Domains\Identity\Policies\UserPolicy;
use App\Domains\System\Traits\Model\HasFile; use App\Domains\System\Traits\Model\HasFile;
use App\Domains\System\Traits\Model\HasPublicUlid;
use Database\Factories\Identity\UserFactory; use Database\Factories\Identity\UserFactory;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden; use Illuminate\Database\Eloquent\Attributes\Hidden;
use Illuminate\Database\Eloquent\Attributes\UseFactory; use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Attributes\UsePolicy; use Illuminate\Database\Eloquent\Attributes\UsePolicy;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\MorphOne;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
@ -31,8 +31,8 @@ class User extends Authenticatable implements Auditable, MustVerifyEmail
use HasApiTokens; use HasApiTokens;
use HasFactory; use HasFactory;
use HasFile; use HasFile;
use HasPublicUlid;
use HasRoles; use HasRoles;
use HasUlids;
use Notifiable; use Notifiable;
use \OwenIt\Auditing\Auditable; use \OwenIt\Auditing\Auditable;
@ -68,11 +68,6 @@ class User extends Authenticatable implements Auditable, MustVerifyEmail
$this->notify(new VerifyEmailNotification); $this->notify(new VerifyEmailNotification);
} }
public function uniqueIds(): array
{
return ['ulid'];
}
public function getRoleNameAttribute() public function getRoleNameAttribute()
{ {
return $this->roles->first()->name ?? '-'; return $this->roles->first()->name ?? '-';

101
app/Domains/System/Actions/Integration/RunGenericImportPipeline.php

@ -3,7 +3,7 @@
namespace App\Domains\System\Actions\Integration; namespace App\Domains\System\Actions\Integration;
use App\Domains\System\Support\Integration\DataPayloadMapper; use App\Domains\System\Support\Integration\DataPayloadMapper;
use Illuminate\Database\Eloquent\Model; use Exception;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -12,16 +12,26 @@ use Throwable;
class RunGenericImportPipeline class RunGenericImportPipeline
{ {
/** /**
* @param Collection<int, array<string, mixed>> $rows Raw spreadsheet rows from the current chunk * Execute the generic import pipeline by transforming rows, fetching existing state,
* @param DataPayloadMapper $mapper The specific structural mapping engine * and delegating the persistence operation to the mapper.
* @param class-string<Model> $modelClass The Eloquent model class to query against *
* @param Collection<int, array<string, mixed>> $rows Raw payload rows from the API or spreadsheet chunk
* @param DataPayloadMapper $mapper The domain-specific structural mapping engine
* @throws Exception
*/ */
public function execute(Collection $rows, DataPayloadMapper $mapper, string $modelClass): void public function execute(Collection $rows, DataPayloadMapper $mapper): void
{ {
$lookupKey = $mapper->getLookupKey(); $lookupKey = $mapper->getLookupKey();
$modelClass = $mapper->getModelClass();
$lookupValues = $rows->pluck($lookupKey) // 1. Transform raw payload rows into domain-aligned internal data structures
->filter() $mappedRows = $rows->map(fn ($row) => $mapper->transform($row));
// 2. Resolve existing domain records from the database
if (is_string($lookupKey)) {
// Single key lookup strategy
$lookupValues = $mappedRows->pluck($lookupKey)
->filter(fn ($val) => ! is_null($val) && $val !== '')
->map(fn ($val) => trim((string) $val)) ->map(fn ($val) => trim((string) $val))
->toArray(); ->toArray();
@ -30,30 +40,81 @@ class RunGenericImportPipeline
->get() ->get()
->keyBy($lookupKey); ->keyBy($lookupKey);
foreach ($rows->toArray() as $row) { } else {
if (empty($row[$lookupKey])) { // Composite key lookup strategy (Array of keys)
continue; $query = $modelClass::query();
// Construct composite OR-WHERE query conditions using mapped internal keys
$query->where(function ($q) use ($mappedRows, $lookupKey) {
foreach ($mappedRows as $mappedRow) {
$q->orWhere(function ($subQ) use ($mappedRow, $lookupKey) {
foreach ($lookupKey as $col) {
$subQ->where($col, $mappedRow[$col] ?? null);
}
});
} }
});
try { // Map retrieved database models into an in-memory composite hash index
DB::transaction(function () use ($row, $lookupKey, $mapper, $existingRecords) { $existingRecords = $query->get()->keyBy(function ($item) use ($lookupKey) {
$lookupValue = trim((string) $row[$lookupKey]); return $this->generateCompositeIdentifier($item->toArray(), $lookupKey);
});
}
// Match against our pre-fetched in-memory cache // 3. Process each row and delegate execution to the domain state action
$matchedModel = $existingRecords->get($lookupValue); foreach ($mappedRows as $row) {
// Guard clause: Ensure all required lookup key attributes are populated
if ($this->isInvalidRow($row, $lookupKey)) {
continue;
}
// Normalize data columns via the mapper specification // Derive the string identifier used to match existing records in memory
$normalizedPayload = $mapper->transform($row); $lookupIdentifier = is_array($lookupKey)
? $this->generateCompositeIdentifier($row, $lookupKey)
: trim((string) $row[$lookupKey]);
// Delegate execution to the underlying actions try {
$mapper->updateOrCreateDomainState(payload: $normalizedPayload, model: $matchedModel); DB::transaction(function () use ($row, $lookupIdentifier, $mapper, $existingRecords) {
$existingModel = $existingRecords->get($lookupIdentifier);
// Delegate execution to the underlying domain action
$mapper->updateOrCreateDomainState(payload: $row, existingModel: $existingModel);
}); });
} catch (Throwable $e) { } catch (Throwable $e) {
Log::error('Generic Import failure on Row processing: '.$e->getMessage(), [ Log::error('Generic Import failure on Row processing: '.$e->getMessage(), [
'row' => $row, 'row' => $row,
'lookupKey' => $lookupKey, 'lookupIdentifier' => $lookupIdentifier,
]); ]);
throw new Exception('Failed to process row: '.$e->getMessage());
}
} }
} }
/**
* Generate a deterministic string hash key for composite lookup matching.
*/
protected function generateCompositeIdentifier(array $data, array $keys): string
{
$values = array_map(fn ($k) => trim((string) ($data[$k] ?? '')), $keys);
return implode('_', $values);
}
/**
* Universal guard clause to validate lookup key integrity for single and composite keys.
*/
protected function isInvalidRow(array $row, string|array $lookupKey): bool
{
$keys = (array) $lookupKey;
foreach ($keys as $key) {
$val = $row[$key] ?? null;
if (is_null($val) || $val === '') {
return true; // Skip row if any required lookup key attribute is missing
}
}
return false;
} }
} }

11
app/Domains/System/Support/Integration/DataPayloadMapper.php

@ -9,15 +9,22 @@ interface DataPayloadMapper
/** /**
* Define the unique column used to locate records across entry streams. * Define the unique column used to locate records across entry streams.
*/ */
public function getLookupKey(): string; public function getLookupKey(): array|string;
/** /**
* Normalize raw incoming data structures into an internal domain-safe layout array * Normalize raw incoming data structures into an internal domain-safe layout array
*/ */
public function transform(array $rawData): array; public function transform(array $rawData): array;
/**
* Define model for transformation
*
* @return class-string
*/
public function getModelClass(): string;
/** /**
* Coordinate and execute internal domain mutations using the transformed payload. * Coordinate and execute internal domain mutations using the transformed payload.
*/ */
public function updateOrCreateDomainState(array $payload, ?Model $model = null): void; public function updateOrCreateDomainState(array $payload, ?Model $existingModel): void;
} }

20
app/Domains/System/Traits/Model/HasPublicUlid.php

@ -0,0 +1,20 @@
<?php
namespace App\Domains\System\Traits\Model;
use Illuminate\Support\Str;
trait HasPublicUlid
{
public static function bootHasPublicUlid(): void
{
static::creating(function ($model) {
$model->ulid = Str::ulid();
});
}
public function getRouteKeyName(): string
{
return 'ulid';
}
}

3
app/Domains/System/Traits/Model/HasSlugs.php

@ -14,7 +14,8 @@ trait HasSlugs
public static function bootHasSlugs(): void public static function bootHasSlugs(): void
{ {
static::creating(function (Model $model) { static::creating(function (Model $model) {
$model->slug = str($model->sluggable())->slug(); $sluggable = $model->sluggable();
$model->slug = str($model->$sluggable)->slug();
}); });
} }
} }

2
app/Http/DataTables/Academic/TermDataTable.php

@ -62,7 +62,7 @@ class TermDataTable extends DataTable
->setTableId('term-table') ->setTableId('term-table')
->columns($this->getColumns()) ->columns($this->getColumns())
->minifiedAjax() ->minifiedAjax()
->orderBy(-1) ->orderBy(1)
->layout([ ->layout([
'topStart' => [ 'topStart' => [
'rowClass' => 'row gap-1', 'rowClass' => 'row gap-1',

4
app/Http/Ingestion/Excel/Identity/UserImport.php

@ -3,7 +3,6 @@
namespace App\Http\Ingestion\Excel\Identity; namespace App\Http\Ingestion\Excel\Identity;
use App\Domains\Identity\Integration\Mappers\UserDataMapper; use App\Domains\Identity\Integration\Mappers\UserDataMapper;
use App\Domains\Identity\Models\User;
use App\Domains\System\Actions\Integration\RunGenericImportPipeline; use App\Domains\System\Actions\Integration\RunGenericImportPipeline;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -21,8 +20,7 @@ class UserImport implements ShouldQueue, ToCollection, WithChunkReading, WithHea
// Excel specific processor reads a file chunk, then uses the generic integration pipeline // Excel specific processor reads a file chunk, then uses the generic integration pipeline
app(RunGenericImportPipeline::class)->execute( app(RunGenericImportPipeline::class)->execute(
rows: $rows, rows: $rows,
mapper: app(UserDataMapper::class), mapper: app(UserDataMapper::class)
modelClass: User::class,
); );
} }

1
database/migrations/2026_04_23_045248_create_permission_tables.php

@ -37,7 +37,6 @@ return new class extends Migration
*/ */
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
$table->id(); // role id $table->id(); // role id
$table->ulid()->unique();
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');

2
database/seeders/DatabaseSeeder.php

@ -7,8 +7,6 @@ use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
use WithoutModelEvents;
/** /**
* Seed the application's database. * Seed the application's database.
*/ */

4
database/seeders/UserSeeder.php

@ -5,7 +5,6 @@ namespace Database\Seeders;
use App\Domains\Identity\Enums\RoleType; use App\Domains\Identity\Enums\RoleType;
use App\Domains\Identity\Models\Role; use App\Domains\Identity\Models\Role;
use App\Domains\Identity\Models\User; use App\Domains\Identity\Models\User;
use App\Domains\Identity\Scopes\HideSystemAdminRole;
use Database\Factories\Account\ProfileFactory; use Database\Factories\Account\ProfileFactory;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
@ -24,8 +23,7 @@ class UserSeeder extends Seeder
'email_verified_at' => now(), 'email_verified_at' => now(),
]); ]);
$systemRoles = Role::withoutGlobalScopes([HideSystemAdminRole::class]) $systemRoles = Role::where('name', RoleType::SYSTEM_ADMIN)
->where('name', RoleType::SYSTEM_ADMIN)
->first(); ->first();
$admin->profile()->create($profile->definition()); $admin->profile()->create($profile->definition());

8
lang/en/domains/channel/messages.php

@ -0,0 +1,8 @@
<?php
return [
'clients' => [
'synced' => 'Clients synced successfully.',
'fail_to_sync' => 'Clients failed to sync: :error.'
]
];

13
resources/js/plugin/sweetalert2.js

@ -10,11 +10,20 @@ window.toast = function (text, type = 'success') {
icon: type, icon: type,
text: text, text: text,
toast: true, toast: true,
position: 'top-end', position: "top-end",
showConfirmButton: false, showConfirmButton: false,
timer: 3000, timer: 3000,
timerProgressBar: true, timerProgressBar: true,
}) didOpen: () => {
const popup = Swal.getPopup();
// Pause timer when mouse enters
popup.addEventListener("mouseenter", Swal.stopTimer);
// Resume timer when mouse leaves
popup.addEventListener("mouseleave", Swal.resumeTimer);
},
});
} }

2
resources/views/components/modal.blade.php

@ -37,7 +37,7 @@
x-on:hide-{{ $id }}.window="$bs.modal.instance($el).hide();" x-on:hide-{{ $id }}.window="$bs.modal.instance($el).hide();"
@endif @endif
> >
<div class="modal-dialog modal-dialog-scrollable modal-fullscreen-md-down {{ $size }}"> <div class="modal-dialog modal-dialog-scrollable modal-fullscreen-md-down {{ $size }}" role="document">
@if ($form) @if ($form)
<form <form
@if ($attributes->has('x-on:submit')) x-on:submit="{{ $attributes->get('x-on:submit') }}" @endif @if ($attributes->has('x-on:submit')) x-on:submit="{{ $attributes->get('x-on:submit') }}" @endif

6
resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.blade.php

@ -1,7 +1,8 @@
<x-modal id="api-form-modal" :title="$this->title" size="modal-lg" wire:submit="save" wire:loading form livewire> <x-modal id="api-form-modal" :title="$this->title" size="modal-fullscreen" wire:submit="save" wire:loading form livewire>
<div class="d-flex flex-column gap-3"> <div class="row g-3">
@foreach($this->endpoints as $key => $endpoint) @foreach($this->endpoints as $key => $endpoint)
<div class="col-sm-12 col-md-4">
<x-card :subtitle="$endpoint['label']"> <x-card :subtitle="$endpoint['label']">
<x-slot:actions> <x-slot:actions>
<x-button type="button" theme="danger" wire:click="runSynchronization('{{ $key }}')" icon="tabler-player-play" /> <x-button type="button" theme="danger" wire:click="runSynchronization('{{ $key }}')" icon="tabler-player-play" />
@ -28,6 +29,7 @@
</div> </div>
</div> </div>
</x-card> </x-card>
</div>
@endforeach @endforeach
</div> </div>

16
resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.php

@ -8,6 +8,7 @@ use App\Domains\Channel\Models\ApiEndpoint;
use App\Domains\Channel\Models\Client; use App\Domains\Channel\Models\Client;
use App\Livewire\Concerns\WithModal; use App\Livewire\Concerns\WithModal;
use App\Livewire\Concerns\WithToast; use App\Livewire\Concerns\WithToast;
use http\Exception;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Livewire\Attributes\Computed; use Livewire\Attributes\Computed;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
@ -92,13 +93,14 @@ new class extends Component
$this->validate(); $this->validate();
$clients = $this->clients->pluck('id', 'ulid'); $clients = $this->clients->pluck('id', 'ulid');
$endpointToSave = array_map(function ($item) use ($clients) { $endpointToSave = [];
return new EndpointUrlDTO( foreach ($this->field as $key => $item) {
$endpointToSave[$key] = new EndpointUrlDTO(
clientId: $clients[$item['client_id']], clientId: $clients[$item['client_id']],
endpointUrl: $item['value'], endpointUrl: $item['value'],
field: ApiType::from($item['type']) field: ApiType::tryFrom($item['type'])
); );
}, $this->field); }
$assign->execute($endpointToSave); $assign->execute($endpointToSave);
@ -109,6 +111,12 @@ new class extends Component
public function runSynchronization(ApiType $type, SynchronizeWithApi $api): void public function runSynchronization(ApiType $type, SynchronizeWithApi $api): void
{ {
try {
$api->execute($type); $api->execute($type);
$this->success(__('domains/channel/messages.clients.synced'));
} catch (\Exception $e) {
$this->error($e->getMessage());
}
} }
}; };

6
resources/views/pages/channel/client/⚡form-modal/form-modal.blade.php

@ -40,11 +40,11 @@
wire:model="form.credentials.password" wire:model="form.credentials.password"
:label="__('domains/channel/field.client.credentials.password')" :label="__('domains/channel/field.client.credentials.password')"
/> />
@elseif ($form->auth_type == AuthType::TOKEN) @elseif ($form->auth_type == AuthType::BEARER)
<x-form.input <x-form.input
name="form.credentials.token" name="form.credentials.bearer"
wire:model="form.credentials.token" wire:model="form.credentials.token"
:label="__('domains/channel/field.client.credentials.token')" :label="__('domains/channel/field.client.credentials.bearer')"
/> />
@endif @endif
</div> </div>

Loading…
Cancel
Save