Browse Source

feat: refactor generators, enhance event handling, and improve API and product mappings

- Replace `AcademicProductGenerator` with `AdmissionProductGenerator` for clearer domain alignment.
- Update `SetUpAdmissionFeeRates` constructor and event listeners for `AdmissionFeeRateWasSetUp`.
- Introduce `ProvisionProductIdentifier` action for streamlined product provisioning.
- Enhance API synchronization logic with dynamic endpoint handling.
- Simplify DTO structures and standardize naming (e.g., `feeRateType` → `pricingType`).
- Add UI/UX updates to endpoint modal, including real-time validation and form simplifications.
master
Syarif Ubaidillah 1 month ago
parent
commit
817bc77ab2
  1. 4
      app/Domains/Admission/Actions/AdmissionPricing/SetUpAdmissionFeeRates.php
  2. 19
      app/Domains/Admission/Support/Generator/AdmissionProductGenerator.php
  3. 11
      app/Domains/Billing/Actions/Catalog/ProvisionProductFromFeeRate.php
  4. 25
      app/Domains/Billing/Actions/Catalog/ProvisionProductIdentifier.php
  5. 10
      app/Domains/Billing/DTOs/Catalog/ProductIdentifierDTO.php
  6. 13
      app/Domains/Billing/DTOs/Catalog/ProvisionProductDTO.php
  7. 14
      app/Domains/Billing/DTOs/Invoicing/DraftInvoiceDTO.php
  8. 21
      app/Domains/Billing/Listeners/Catalog/ProductGenerated.php
  9. 2
      app/Domains/Billing/Models/Product.php
  10. 8
      app/Domains/Billing/Providers/BillingServiceProvider.php
  11. 2
      app/Domains/Billing/Support/Contract/ProductIdentifierGenerator.php
  12. 17
      app/Domains/Channel/Actions/Api/AssignEndpointUrl.php
  13. 2
      app/Domains/Channel/Enums/Api/ApiType.php
  14. 1
      lang/en/domains/channel/enum.php
  15. 1
      lang/id/domains/channel/enum.php
  16. 4
      resources/scss/app.scss
  17. 2
      resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php
  18. 118
      resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.blade.php
  19. 27
      resources/views/pages/channel/client/⚡api-form-modal/api-form-modal.php

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

@ -2,14 +2,14 @@
namespace App\Domains\Admission\Actions\AdmissionPricing;
use App\Domains\Academic\Support\Generator\AcademicProductGenerator;
use App\Domains\Admission\DTOs\AdmissionPricing\AdmissionFeeRatesDTO;
use App\Domains\Admission\Events\Pricing\AdmissionFeeRateWasSetUp;
use App\Domains\Admission\Models\FeeRate;
use App\Domains\Admission\Support\Generator\AdmissionProductGenerator;
class SetUpAdmissionFeeRates
{
public function __construct(protected AcademicProductGenerator $generator)
public function __construct(protected AdmissionProductGenerator $generator)
{
}

19
app/Domains/Academic/Support/Generator/AcademicProductGenerator.php → app/Domains/Admission/Support/Generator/AdmissionProductGenerator.php

@ -1,12 +1,12 @@
<?php
namespace App\Domains\Academic\Support\Generator;
namespace App\Domains\Admission\Support\Generator;
use App\Domains\Admission\Models\FeeRate;
use App\Domains\Billing\DTOs\Products\ProductIdentifierDTO;
use App\Domains\Billing\DTOs\Catalog\ProductIdentifierDTO;
use App\Domains\Billing\Support\Contract\ProductIdentifierGenerator;
class AcademicProductGenerator implements ProductIdentifierGenerator
class AdmissionProductGenerator implements ProductIdentifierGenerator
{
/**
@ -31,21 +31,24 @@ class AcademicProductGenerator implements ProductIdentifierGenerator
'admissionSchedule.admissionTrack',
'admissionSchedule.term',
'academicProgram',
'studyProgram',
'feeType',
]);
$code = sprintf(
'%s-%s-%s-%s',
'%s-%s-%s-%s-%s',
str($rate->admissionSchedule->admissionTrack->name)->initials(),
'AC'.$rate->academicProgram->code,
'AP'.$rate->academicProgram->code,
$rate->studyProgram->code,
'FT'.$rate->feeType->code,
$rate->admissionSchedule->term->year_study
);
$name = sprintf(
'%s %s %s %s',
'%s %s %s %s %s',
$rate->admissionSchedule->admissionTrack->name,
$rate->academicProgram->name,
$rate->studyProgram->name,
$rate->feeType->name,
$rate->admissionSchedule->term->year_study
);
@ -53,8 +56,8 @@ class AcademicProductGenerator implements ProductIdentifierGenerator
return new ProductIdentifierDTO(
code: $code,
name: $name,
feeRateType: $rate->getMorphClass(),
feeRateId: $rate->id
pricingType: $rate->getMorphClass(),
pricingId: $rate->id
);
}
}

11
app/Domains/Billing/Actions/Catalog/ProvisionProductFromFeeRate.php

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

25
app/Domains/Billing/Actions/Catalog/ProvisionProductIdentifier.php

@ -0,0 +1,25 @@
<?php
namespace App\Domains\Billing\Actions\Catalog;
use App\Domains\Billing\DTOs\Catalog\ProductIdentifierDTO;
use App\Domains\Billing\Models\Product;
class ProvisionProductIdentifier
{
public function execute(ProductIdentifierDTO $identifierDTO): void
{
$isExists = Product::where('pricing_id', $identifierDTO->pricingId)
->where('pricing_type', $identifierDTO->pricingType)
->exists();
if (!$isExists) {
Product::create([
'code' => $identifierDTO->code,
'name' => $identifierDTO->name,
'pricing_id' => $identifierDTO->pricingId,
'pricing_type' => $identifierDTO->pricingType,
]);
}
}
}

10
app/Domains/Billing/DTOs/Products/ProductIdentifierDTO.php → app/Domains/Billing/DTOs/Catalog/ProductIdentifierDTO.php

@ -1,6 +1,6 @@
<?php
namespace App\Domains\Billing\DTOs\Products;
namespace App\Domains\Billing\DTOs\Catalog;
/**
* Readonly Data Transfer Object containing generated product credentials.
@ -12,13 +12,13 @@ readonly class ProductIdentifierDTO
*
* @param string $code Short deterministic business code (e.g. PMDK-UKT-PRK-SI-25G).
* @param string $name Full descriptive name for UI and invoice documents.
* @param string $feeRateType relation map
* @param string|int $feeRateId relation identifier to fee rate
* @param string $pricingType relation map
* @param string|int $pricingId relation identifier to fee rate
*/
public function __construct(
public string $code,
public string $name,
public string $feeRateType,
public string|int $feeRateId
public string $pricingType,
public string|int $pricingId
) {}
}

13
app/Domains/Billing/DTOs/Catalog/ProvisionProductDTO.php

@ -1,13 +0,0 @@
<?php
namespace App\Domains\Billing\DTOs\Catalog;
readonly class ProvisionProductDTO
{
public function __construct(
public string $name,
public string $code,
public string $pricingType,
public string $pricingId,
) {}
}

14
app/Domains/Billing/DTOs/Invoicing/DraftInvoiceDTO.php

@ -1,14 +0,0 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing;
readonly class DraftInvoiceDTO
{
public function __construct(
public string $name,
public string $clientId,
public string $clientRefId,
public array $items,
public array $metadata,
) {}
}

21
app/Domains/Billing/Listeners/Catalog/ProductGenerated.php

@ -0,0 +1,21 @@
<?php
namespace App\Domains\Billing\Listeners\Catalog;
use App\Domains\Billing\Actions\Catalog\ProvisionProductIdentifier;
use App\Domains\Billing\DTOs\Catalog\ProductIdentifierDTO;
class ProductGenerated
{
public function __construct(protected ProvisionProductIdentifier $provisionProductIdentifier)
{
}
public function handle(mixed $event): void
{
if($event->product instanceof ProductIdentifierDTO)
{
$this->provisionProductIdentifier->execute($event->product);
}
}
}

2
app/Domains/Billing/Models/Product.php

@ -13,6 +13,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
#[Fillable([
'code',
'name',
'pricing_type',
'pricing_id',
'chart_of_account_id'
])]
#[UseFactory(ProductFactory::class)]

8
app/Domains/Billing/Providers/BillingServiceProvider.php

@ -2,6 +2,8 @@
namespace App\Domains\Billing\Providers;
use App\Domains\Admission\Events\Pricing\AdmissionFeeRateWasSetUp;
use App\Domains\Billing\Listeners\Catalog\ProductGenerated;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Support\ServiceProvider;
@ -9,7 +11,11 @@ class BillingServiceProvider extends ServiceProvider
{
use RegistersDomainEvents;
protected array $listen = [];
protected array $listen = [
AdmissionFeeRateWasSetUp::class => [
ProductGenerated::class
]
];
public function register(): void
{

2
app/Domains/Billing/Support/Contract/ProductIdentifierGenerator.php

@ -2,7 +2,7 @@
namespace App\Domains\Billing\Support\Contract;
use App\Domains\Billing\DTOs\Products\ProductIdentifierDTO;
use App\Domains\Billing\DTOs\Catalog\ProductIdentifierDTO;
/**
* Contract interface for generating product codes and display names.

17
app/Domains/Channel/Actions/Api/AssignEndpointUrl.php

@ -9,22 +9,17 @@ use Exception;
class AssignEndpointUrl
{
/**
* @param EndpointUrlDTO[] $endpointToSave
* @param EndpointUrlDTO $endpoint
*
* @throws Exception
*/
public function execute(array $endpointToSave): void
public function execute(EndpointUrlDTO $endpoint): void
{
$validateArrayDTO = array_map(fn ($endpoint) => $endpoint instanceof EndpointUrlDTO, $endpointToSave);
if (in_array(false, $validateArrayDTO)) {
throw new Exception(sprintf('Invalid parameter. to use this action, the $endpointToSave must be an array of %s', EndpointUrlDTO::class));
}
$toSave = array_map(fn (EndpointUrlDTO $endpoint) => [
'client_id' => $endpoint->clientId,
ApiEndpoint::updateOrCreate([
'key' => $endpoint->field,
], [
'client_id' => $endpoint->clientId,
'value' => $endpoint->endpointUrl,
], $endpointToSave);
ApiEndpoint::upsert($toSave, ['client_id', 'key']);
]);
}
}

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

@ -15,9 +15,9 @@ enum ApiType: string implements HasLabel
case STUDY_PROGRAM = 'study_program';
case TERM = 'term';
case ADMISSION_TRACK = 'admission_track';
case ADMISSION_SCHEDULE = 'admission_schedule';
case ACADEMIC_PROGRAM = 'academic_program';
case FEE_TYPE = 'fee_type';
case ADMISSION_SCHEDULE = 'admission_schedule';
case FEE_RATE = 'fee_rate';
public function mapper(): DataPayloadMapper

1
lang/en/domains/channel/enum.php

@ -12,6 +12,7 @@ return [
ApiType::ADMISSION_SCHEDULE->value => 'Admission Schedule',
ApiType::ACADEMIC_PROGRAM->value => 'Academic Program',
ApiType::FEE_TYPE->value => 'Fee Type',
ApiType::FEE_RATE->value => 'Fee Rate',
],
'auth_type' => [
AuthType::NONE->value => 'None',

1
lang/id/domains/channel/enum.php

@ -12,6 +12,7 @@ return [
ApiType::ADMISSION_SCHEDULE->value => 'Jadwal Pendaftaran',
ApiType::ACADEMIC_PROGRAM->value => 'Program Akademik',
ApiType::FEE_TYPE->value => 'Jenis Biaya',
ApiType::FEE_RATE->value => 'Rate Biaya',
],
'auth_type' => [
AuthType::NONE->value => 'Tidak Ada',

4
resources/scss/app.scss

@ -2,6 +2,10 @@
$enable-deprecation-messages: false
);
svg {
pointer-events: none;
}
body {
background-color: var(--cui-tertiary-bg);
}

2
resources/views/pages/admission/admission-schedule/⚡detail-view/detail-view.php

@ -40,7 +40,7 @@ class extends Component
public function admissionSchedule(): AdmissionSchedule
{
return AdmissionSchedule::with(['admissionTrack', 'term'])
->where('ulid', $this->id)->first();
->where('ulid', $this->id)->firstOrFail();
}
#[Computed]

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

@ -1,18 +1,14 @@
<x-modal id="api-form-modal" :title="$this->title" size="modal-xl" wire:submit="save" wire:loading form livewire>
<x-modal id="api-form-modal" data-coreui-focus="false" :title="$this->title" size="modal-xl" wire:submit="save" wire:loading form livewire>
<div class="row g-3 g-lg-4">
@foreach($this->endpoints as $key => $endpoint)
<div class="col-12 col-md-6" wire:key="endpoint-card-{{ $key }}">
<div class="card border border-2 shadow-sm rounded-3 h-100 mb-0 overflow-hidden transition-all"
:class="$wire.api_code === '{{ $key }}' ? 'border-primary ring-1' : 'border-body-tertiary'">
<div class="card-header border-bottom p-3 d-flex align-items-center justify-content-between gap-2 transition-all"
:class="$wire.api_code === '{{ $key }}' ? 'bg-primary-subtle' : 'bg-body-tertiary'">
<div class="card border border-body-tertiary shadow-sm rounded-3 h-100 mb-0 overflow-hidden transition-all">
<!-- Card Header -->
<div class="card-header border-bottom border-body-tertiary bg-body-tertiary p-3 d-flex align-items-center justify-content-between gap-2">
<div class="d-flex align-items-center gap-2 overflow-hidden">
<span class="badge rounded-circle p-2 d-flex align-items-center justify-content-center flex-shrink-0 transition-all"
:class="$wire.api_code === '{{ $key }}' ? 'bg-primary text-white' : 'bg-primary-subtle text-primary'">
<span class="badge rounded-circle p-2 d-flex align-items-center justify-content-center flex-shrink-0 bg-primary-subtle text-primary">
<x-tabler-api-app width="16" height="16" />
</span>
<span class="fw-bold text-body text-truncate small text-uppercase tracking-wider" title="{{ $endpoint['label'] }}">
@ -20,6 +16,7 @@
</span>
</div>
<!-- Actions -->
<div class="d-flex align-items-center gap-2 flex-shrink-0">
<x-button
type="button"
@ -27,11 +24,16 @@
size="sm"
class="btn-icon rounded-circle"
icon="tabler-pencil"
x-on:click="$js.edit('{{ $key }}')"
x-on:click="$js.editEndpoint"
icon-only
data-key="{{ $key }}"
data-label="{{ $endpoint['label'] }}"
data-value="{{ $endpoint['value'] }}"
data-client="{{ $endpoint['client_id'] }}"
:title="__('ui/button.edit')"
/>
@isset($endpoint['client_id'])
<x-button
type="button"
theme="danger"
@ -42,31 +44,30 @@
icon-only
:title="__('ui/button.run-sync')"
/>
@endif
</div>
</div>
<div class="card-body p-3">
<div class="row g-3">
<div class="col-12">
<x-form.select
name="{{ $key }}_client"
wire:model="field.{{ $key }}.client_id"
:options="$this->clients->mapWithKeys(fn ($item) => [$item->ulid => $item->name])"
::disabled="$wire.api_code !== '{{ $key }}'"
:label="__('domains/channel/field.client.name')"
:placeholder="__('ui/placeholder.select', ['attribute' => __('domains/channel/field.client.name')])"
/>
<!-- Card Body (Read-Only Detail View) -->
<div class="card-body p-3 d-flex flex-column gap-3">
<!-- Client Name -->
<div class="p-3 bg-body-tertiary border border-body-tertiary rounded-3">
<small class="text-body-secondary fw-semibold text-uppercase d-block mb-1 fs-7">
{{ __('domains/channel/field.client.name') }}
</small>
<span class="fw-medium text-body text-break small">
{{ $this->clients->firstWhere('ulid', $endpoint['client_id'] ?? null)?->name ?? '-' }}
</span>
</div>
<div class="col-12">
<x-form.input
name="{{ $key }}_url"
wire:model="field.{{ $key }}.value"
::disabled="$wire.api_code !== '{{ $key }}'"
:label="__('domains/channel/field.client.endpoint_url')"
placeholder="/v1/endpoint"
/>
</div>
<!-- Endpoint URL -->
<div class="p-3 bg-body-tertiary border border-body-tertiary rounded-3">
<small class="text-body-secondary fw-semibold text-uppercase d-block mb-1 fs-7">
{{ __('domains/channel/field.client.endpoint_url') }}
</small>
<code class="fw-medium text-primary text-break small font-monospace">
{{ $endpoint['value'] ?? '-' }}
</code>
</div>
</div>
@ -81,9 +82,60 @@
@script
<script>
$js.edit = (key) => {
$wire.api_code = key;
console.log($wire.api_code, key);
$js.editEndpoint = (e) => {
const data = e.target.dataset;
Swal.fire({
title: data.label,
html: `
<div class="text-start d-flex flex-column gap-3 py-2">
<div>
<label class="form-label small fw-semibold text-body mb-1">
${'{{ __("domains/channel/field.client.name") }}'}
</label>
<select id="swal-client-id" class="form-select form-select-sm">
@foreach($this->clients as $client)
<option value="{{ $client->ulid }}">{{ $client->name }}</option>
@endforeach
</select>
</div>
<div>
<label class="form-label small fw-semibold text-body mb-1">
${'{{ __("domains/channel/field.client.endpoint_url") }}'}
</label>
<input id="swal-endpoint-url" class="form-control form-control-sm font-monospace" placeholder="/v1/endpoint" value="${data.value || ''}">
</div>
</div>
`,
showCancelButton: true,
confirmButtonText: '{{ __("ui/button.save") }}',
cancelButtonText: '{{ __("ui/button.cancel") }}',
customClass: {
confirmButton: 'btn btn-primary me-2',
cancelButton: 'btn btn-secondary'
},
buttonsStyling: false,
didOpen: () => {
if (data.client) {
document.getElementById('swal-client-id').value = data.client;
}
},
preConfirm: async () => {
return new Promise((resolve, reject) => {
try {
const clientId = document.getElementById('swal-client-id').value;
const endpointUrl = document.getElementById('swal-endpoint-url').value;
$wire.set('field.type', data.key);
$wire.set('field.client_id', clientId);
$wire.set('field.value', endpointUrl);
$wire.save();
return resolve();
}catch (e) {
return reject(e.message);
}
})
}
})
};
</script>
@endscript

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

@ -26,8 +26,9 @@ new class extends Component
public string $mode = 'view';
#[Validate([
'field.*.client_id' => 'required',
'field.*.value' => 'required',
'field.type' => 'required',
'field.client_id' => 'required',
'field.value' => 'required',
])]
public array $field = [];
@ -40,7 +41,7 @@ new class extends Component
public function hide(): void
{
$this->reset('id', 'mode');
$this->reset('mode');
}
public function editMode(): void
@ -70,10 +71,6 @@ new class extends Component
$endpoints[$item->value] = [
'label' => $item->label(),
'value' => $value,
];
$this->field[$item->value] = [
'type' => $item->value,
'value' => $value,
'client_id' => $client_id,
];
}
@ -91,17 +88,11 @@ new class extends Component
{
$this->validate();
$clients = $this->clients->pluck('id', 'ulid');
$endpointToSave = [];
foreach ($this->field as $key => $item) {
$endpointToSave[$key] = new EndpointUrlDTO(
clientId: $clients[$item['client_id']],
endpointUrl: $item['value'],
field: ApiType::tryFrom($item['type'])
);
}
$assign->execute($endpointToSave);
$assign->execute(new EndpointUrlDTO(
clientId: $this->clients->where('ulid', $this->field['client_id'])->first()->id,
endpointUrl: $this->field['value'],
field: ApiType::tryFrom($this->field['type'])
));
$this->mode = 'view';

Loading…
Cancel
Save