Browse Source

refactor(billing): align invoicing and catalog with schema

- Add transactional product registration and modification with COA assignment
- Store invoice-item unit prices and require a chart-of-accounts relation
- Remove defaults from client credentials and chart-of-account status
- Remove legacy invoicing actions and DTOs
master
Syarif Ubaidillah 3 days ago
parent
commit
6b50605db5
  1. 1382
      app/Console/Commands/CheckMissingTranslations.php
  2. 19
      app/Console/Commands/Contracts/AttributeScanHandler.php
  3. 3
      app/Console/Commands/DomainMakeCommand.php
  4. 28
      app/Console/Commands/Handlers/GenericSeoAttributeHandler.php
  5. 33
      app/Console/Commands/Handlers/LayoutAttributeHandler.php
  6. 39
      app/Console/Commands/Handlers/LayoutDataAttributeHandler.php
  7. 12
      app/Console/stubs/domain-make/enum.stub
  8. 20
      app/Console/stubs/domain-make/input-schema.stub
  9. 6
      app/Console/stubs/domain-make/query.stub
  10. 4
      app/Domains/Academic/Queries/Lookup/FacultyLookup.php
  11. 6
      app/Domains/Academic/Queries/Lookup/StudyProgramLookup.php
  12. 2
      app/Domains/Academic/Queries/Lookup/TermLookup.php
  13. 9
      app/Domains/Accounting/Providers/RelationshipServiceProvider.php
  14. 1
      app/Domains/Admission/Integration/Mappers/FeeRateDataMapper.php
  15. 18
      app/Domains/Admission/Queries/Lookup/AcademicProgramLookup.php
  16. 18
      app/Domains/Admission/Queries/Lookup/AdmissionTrackLookup.php
  17. 4
      app/Domains/Admission/Queries/Lookup/FeeTypeLookup.php
  18. 37
      app/Domains/Admission/Queries/SearchAdmissionSchedulePricing.php
  19. 23
      app/Domains/Billing/Actions/Catalog/ModifyProduct.php
  20. 21
      app/Domains/Billing/Actions/Catalog/RegisterProduct.php
  21. 18
      app/Domains/Billing/Actions/Catalog/SetChartOfAccountForProduct.php
  22. 11
      app/Domains/Billing/Actions/Discounts/ReloveDiscount.php
  23. 24
      app/Domains/Billing/Actions/Invoicing/AttachItemsToInvoice.php
  24. 11
      app/Domains/Billing/Actions/Invoicing/DetachUnusedItemsFromInvoice.php
  25. 47
      app/Domains/Billing/Actions/Invoicing/DraftInvoice.php
  26. 45
      app/Domains/Billing/Actions/Invoicing/GenerateInvoiceCode.php
  27. 25
      app/Domains/Billing/Actions/Invoicing/IssueInvoice.php
  28. 38
      app/Domains/Billing/Actions/Invoicing/UpdateInvoiceDetail.php
  29. 14
      app/Domains/Billing/Actions/Invoicing/UpdateInvoiceStatus.php
  30. 12
      app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php
  31. 12
      app/Domains/Billing/DTOs/Catalog/RegisterProductDTO.php
  32. 20
      app/Domains/Billing/DTOs/Invoicing/Context/AcademicContextDTO.php
  33. 8
      app/Domains/Billing/DTOs/Invoicing/Context/ContextDTOInterface.php
  34. 28
      app/Domains/Billing/DTOs/Invoicing/InvoiceDTO.php
  35. 38
      app/Domains/Billing/DTOs/Invoicing/InvoiceItemDTO.php
  36. 19
      app/Domains/Billing/DTOs/Invoicing/PricingDTO.php
  37. 17
      app/Domains/Billing/DTOs/Invoicing/UpdateInvoiceDetailDTO.php
  38. 27
      app/Domains/Billing/Enums/Invoicing/InvoiceStatus.php
  39. 32
      app/Domains/Billing/Enums/Invoicing/InvoiceType.php
  40. 18
      app/Domains/Billing/Models/Invoice.php
  41. 15
      app/Domains/Billing/Models/InvoiceItem.php
  42. 8
      app/Domains/Billing/Models/Product.php
  43. 37
      app/Domains/Billing/Policies/ProductPolicy.php
  44. 16
      app/Domains/Billing/Queries/Catalog/SearchProductInfo.php
  45. 24
      app/Domains/Billing/Support/Schemas/Invoice/CustomerInvoiceSchema.php
  46. 118
      app/Domains/Billing/Support/Schemas/Invoice/StudentInvoiceSchema.php
  47. 20
      app/Domains/Billing/Support/Schemas/Invoice/VendorInvoiceSchema.php
  48. 1
      app/Domains/Channel/Actions/Api/SynchronizeWithApi.php
  49. 2
      app/Domains/System/Casts/MoneyCurrency.php
  50. 4
      app/Domains/System/Providers/SystemServiceProvider.php
  51. 26
      app/Domains/System/Support/ValueObjects/Money.php
  52. 32
      app/Http/Controllers/Web/Billing/InvoiceController.php
  53. 1
      app/Http/DataTables/Academic/FacultyDataTable.php
  54. 1
      app/Http/DataTables/Academic/StudyProgramDataTable.php
  55. 1
      app/Http/DataTables/Academic/TermDataTable.php
  56. 1
      app/Http/DataTables/Admission/AcademicProgramDataTable.php
  57. 1
      app/Http/DataTables/Admission/AdmissionScheduleDataTable.php
  58. 1
      app/Http/DataTables/Admission/AdmissionTrackDataTable.php
  59. 1
      app/Http/DataTables/Admission/FeeTypeDataTable.php
  60. 136
      app/Http/DataTables/Billing/InvoiceDataTable.php
  61. 6
      app/Http/DataTables/Billing/ProductDataTable.php
  62. 1
      app/Http/DataTables/Channel/ClientDataTable.php
  63. 4
      app/Http/Middleware/HandleSeoSetting.php
  64. 84
      app/Livewire/Concerns/HasModelFill.php
  65. 3
      app/Livewire/Forms/Accounting/ChartOfAccountForm.php
  66. 165
      app/Livewire/Forms/Billing/InvoiceForm.php
  67. 82
      app/Livewire/Forms/Finance/InvoiceForm.php
  68. 27
      app/UI/Support/Schemas/AbstractGroupSchema.php
  69. 53
      app/UI/Support/Schemas/InputSchema.php
  70. 2
      app/UI/Support/Schemas/InputSchemaField.php
  71. 2
      config/app.php
  72. 2
      database/migrations/2026_07_06_031003_create_clients_table.php
  73. 2
      database/migrations/2026_09_08_022815_create_chart_of_accounts_table.php
  74. 6
      database/migrations/2026_09_08_022818_create_invoice_items_table.php
  75. 93
      database/seeders/Accounting/ChartOfAccountSeeder.php
  76. 83
      database/seeders/Channel/ClientSeeder.php
  77. 5
      database/seeders/DatabaseSeeder.php
  78. 6
      lang/en.json
  79. 26
      lang/en/domains/account/enum.php
  80. 10
      lang/en/domains/account/pages.php
  81. 5
      lang/en/domains/admission/exception.php
  82. 2
      lang/en/domains/admission/field.php
  83. 2
      lang/en/domains/admission/seo.php
  84. 6
      lang/en/domains/auth/pages.php
  85. 5
      lang/en/domains/auth/seo.php
  86. 10
      lang/en/domains/billing.php
  87. 8
      lang/en/domains/billing/button.php
  88. 16
      lang/en/domains/billing/enum.php
  89. 30
      lang/en/domains/billing/field.php
  90. 5
      lang/en/domains/billing/messages.php
  91. 19
      lang/en/domains/billing/section.php
  92. 8
      lang/en/domains/billing/seo.php
  93. 1
      lang/en/domains/channel/field.php
  94. 12
      lang/en/domains/identity/enum.php
  95. 5
      lang/en/domains/identity/messages.php
  96. 31
      lang/en/domains/identity/notifications.php
  97. 3
      lang/en/domains/identity/pages.php
  98. 5
      lang/en/domains/identity/seo.php
  99. 7
      lang/en/domains/system.php
  100. 31
      lang/en/domains/system/enum.php

1382
app/Console/Commands/CheckMissingTranslations.php

File diff suppressed because it is too large

19
app/Console/Commands/Contracts/AttributeScanHandler.php

@ -0,0 +1,19 @@
<?php
namespace App\Console\Commands\Contracts;
interface AttributeScanHandler
{
/**
* Nama kelas atribut PHP (misal: 'LayoutData', 'Layout')
*/
public function attributeClass(): string;
/**
* Ekstrak kunci translasi dari argumen atribut
*
* @param array $arguments Argumen yang dievaluasi dari atribut
* @return array<int, string>
*/
public function extractTranslationKeys(array $arguments): array;
}

3
app/Console/Commands/DomainMakeCommand.php

@ -9,7 +9,7 @@ use Illuminate\Support\Str;
class DomainMakeCommand extends Command
{
protected $signature = 'domain:make
{type : Type to generate: model, action, dto, enum, event, listener, notification, policy, query, provider, relationship-provider, export, mapper, scope, mailable}
{type : Type to generate: model, action, dto, enum, event, listener, notification, policy, query, provider, relationship-provider, export, mapper, scope, mailable, input-schema}
{domain : Domain name, e.g. Identity, Account, System}
{name : Class name, supports sub-paths e.g. Backup/DeleteBackup}
{--factory : Also generate a factory (model only)}
@ -40,6 +40,7 @@ class DomainMakeCommand extends Command
'integration' => 'Integration',
'mapper' => 'Integration/Mappers',
'mailable' => 'Mail',
'input-schema' => 'Support/Schemas',
];
public function __construct(protected Filesystem $files)

28
app/Console/Commands/Handlers/GenericSeoAttributeHandler.php

@ -0,0 +1,28 @@
<?php
namespace App\Console\Commands\Handlers;
use App\Console\Commands\Contracts\AttributeScanHandler;
class GenericSeoAttributeHandler implements AttributeScanHandler
{
public function __construct(private readonly string $className) {}
public function attributeClass(): string
{
return $this->className;
}
public function extractTranslationKeys(array $arguments): array
{
$keys = [];
array_walk_recursive($arguments, function ($val) use (&$keys) {
if (is_string($val) && (str_contains($val, '/') || str_contains($val, '.'))) {
$keys[] = $val;
}
});
return $keys;
}
}

33
app/Console/Commands/Handlers/LayoutAttributeHandler.php

@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands\Handlers;
use App\Console\Commands\Contracts\AttributeScanHandler;
class LayoutAttributeHandler implements AttributeScanHandler
{
public function attributeClass(): string
{
return 'Layout';
}
public function extractTranslationKeys(array $arguments): array
{
$keys = [];
foreach ($arguments as $argument) {
if(is_array($argument)) {
foreach ($argument as $label) {
$keys[] = $label;
}
}
}
return $keys;
}
private function isTranslationKey(string $value): bool
{
return str_contains($value, '/') || str_contains($value, '.');
}
}

39
app/Console/Commands/Handlers/LayoutDataAttributeHandler.php

@ -0,0 +1,39 @@
<?php
namespace App\Console\Commands\Handlers;
use App\Console\Commands\Contracts\AttributeScanHandler;
class LayoutDataAttributeHandler implements AttributeScanHandler
{
public function attributeClass(): string
{
return 'LayoutData';
}
public function extractTranslationKeys(array $arguments): array
{
$keys = [];
// 1. Ambil 'header' jika berupa kunci translasi
if (isset($arguments['header']) && is_string($arguments['header']) && $this->isTranslationKey($arguments['header'])) {
$keys[] = $arguments['header'];
}
// 2. Ambil breadcrumbs (hanya KEY/LABEL dari array, abaikan URL/Value)
if (isset($arguments['breadcrumbs']) && is_array($arguments['breadcrumbs'])) {
foreach ($arguments['breadcrumbs'] as $labelKey => $url) {
if (is_string($labelKey) && $this->isTranslationKey($labelKey)) {
$keys[] = $labelKey;
}
}
}
return $keys;
}
private function isTranslationKey(string $value): bool
{
return str_contains($value, '/') || str_contains($value, '.');
}
}

12
app/Console/stubs/domain-make/enum.stub

@ -2,7 +2,13 @@
namespace {{ namespace }};
enum {{ class }}: string
use App\Domains\System\Traits\Enum\HasPredicateMethod;
use App\UI\Enums\Concerns\InteractsWithLabels;
use App\UI\Enums\Contracts\HasLabel;
enum {{ class }}: string implements HasLabel
{
//
}
use HasPredicateMethod;
use InteractsWithLabels;
}

20
app/Console/stubs/domain-make/input-schema.stub

@ -0,0 +1,20 @@
<?php
namespace {{ namespace }};
use App\UI\Support\Schemas\AbstractGroupSchema;
use App\UI\Support\Schemas\InputSchema;
class {{ class }} extends AbstractGroupSchema
{
/**
* Define the schema composition.
*/
public static function make(): static
{
return self::make()
->addInput(
InputSchema::make()
);
}
}

6
app/Console/stubs/domain-make/query.stub

@ -2,12 +2,12 @@
namespace {{ namespace }};
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection;
class {{ class }}
{
public function fetch(): Collection
public static function fetch(): Collection
{
return new Collection();
}
}
}

4
app/Domains/Academic/Queries/Lookup/FacultyLookup.php

@ -10,9 +10,9 @@ class FacultyLookup
public static function fetch(): Collection
{
return once(fn () => Faculty::query()
->select(['id', 'name'])
->select(['ulid', 'name'])
->latest()
->get()
->pluck('name', 'id'));
->pluck('name', 'ulid'));
}
}

6
app/Domains/Academic/Queries/Lookup/StudyProgramLookup.php

@ -14,10 +14,10 @@ class StudyProgramLookup
}
return once(fn () => StudyProgram::query()
->select('id', 'name')
->where('faculty_id', $context['faculty'])
->select('ulid', 'name')
->whereRelationUlid('faculty', $context['faculty'])
->latest()
->get()
->pluck('name', 'id'));
->pluck('name', 'ulid'));
}
}

2
app/Domains/Academic/Queries/Lookup/TermLookup.php

@ -10,7 +10,7 @@ class TermLookup
public static function fetch($search = null): Collection
{
return once(fn () => Term::query()
->select(['id', 'name'])
->select(['name'])
->when($search, fn ($query) => $query->where('name', 'like', "%{$search}%"))
->latest('date_start')
->pluck('name', 'name'));

9
app/Domains/Accounting/Providers/RelationshipServiceProvider.php

@ -2,6 +2,8 @@
namespace App\Domains\Accounting\Providers;
use App\Domains\Accounting\Models\ChartOfAccount;
use App\Domains\Billing\Models\Product;
use Illuminate\Support\ServiceProvider;
class RelationshipServiceProvider extends ServiceProvider
@ -26,6 +28,11 @@ class RelationshipServiceProvider extends ServiceProvider
*/
public function boot(): void
{
// Register cross-domain relationships below.
ChartOfAccount::resolveRelationUsing('product', fn (ChartOfAccount $coa) =>
$coa->hasMany(Product::class, 'coa_id')
);
Product::resolveRelationUsing('chartOfAccount', fn (Product $product) =>
$product->belongsTo(ChartOfAccount::class, 'chart_of_account_id')
);
}
}

1
app/Domains/Admission/Integration/Mappers/FeeRateDataMapper.php

@ -34,6 +34,7 @@ class FeeRateDataMapper implements DataPayloadMapper
if(!isset($rawData['jalur'])){
dd($rawData);
}
// Normalize incoming data array formats into an internal domain-safe layout
return [
'admission_schedule_id' => $this->resolveAdmissionScheduleId($rawData['jalur'], $rawData['angkatan']),

18
app/Domains/Admission/Queries/Lookup/AcademicProgramLookup.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admission\Queries\Lookup;
use App\Domains\Admission\Models\AcademicProgram;
use Illuminate\Support\Collection;
class AcademicProgramLookup
{
public static function fetch(): Collection
{
return once(fn () => AcademicProgram::query()
->select(['ulid', 'name'])
->latest()
->get()
->pluck('name', 'ulid'));
}
}

18
app/Domains/Admission/Queries/Lookup/AdmissionTrackLookup.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Admission\Queries\Lookup;
use App\Domains\Admission\Models\AdmissionTrack;
use Illuminate\Support\Collection;
class AdmissionTrackLookup
{
public static function fetch(): Collection
{
return once(fn () => AdmissionTrack::query()
->select(['ulid', 'name'])
->latest()
->get()
->pluck('name', 'ulid'));
}
}

4
app/Domains/Admission/Queries/Lookup/FeeTypeLookup.php

@ -10,9 +10,9 @@ class FeeTypeLookup
public static function fetch(): Collection
{
return once(fn () => FeeType::query()
->select(['id', 'name'])
->select(['ulid', 'name'])
->latest()
->get()
->pluck('name', 'id'));
->pluck('name', 'ulid'));
}
}

37
app/Domains/Admission/Queries/SearchAdmissionSchedulePricing.php

@ -0,0 +1,37 @@
<?php
namespace App\Domains\Admission\Queries;
use App\Domains\Admission\Models\FeeRate;
use Exception;
class SearchAdmissionSchedulePricing
{
/**
* @throws Exception
*/
public static function fetch(
string $admissionTrack,
string $academicProgram,
string $studyProgram,
string $termEntry,
string $feeType): ?FeeRate
{
$fee = FeeRate::query()
->whereHas('admissionSchedule', fn ($query) => $query
->whereRelationUlid('admissionTrack', $admissionTrack)
->whereHas('term', fn ($query) => $query->where('name', $termEntry))
)
->whereRelationUlid('academicProgram', $academicProgram)
->whereRelationUlid('studyProgram', $studyProgram)
->whereRelationUlid('feeType', $feeType)
->select(['amount', 'id'])
->first();
if(is_null($fee)) {
throw new Exception(__('domains/admission/exception.fee_rate_not_found'));
}
return $fee;
}
}

23
app/Domains/Billing/Actions/Catalog/ModifyProduct.php

@ -1,23 +0,0 @@
<?php
namespace App\Domains\Billing\Actions\Catalog;
use App\Domains\Billing\DTOs\Catalog\ModifyProductDTO;
use App\Domains\Billing\Models\Product;
use Illuminate\Support\Facades\DB;
class ModifyProduct
{
public function execute(Product $product, ModifyProductDTO $dto): Product
{
return DB::transaction(function () use ($product, $dto) {
$product->update([
'name' => $dto->name,
'code' => $dto->code,
'coa_id' => $dto->coaId,
]);
return $product->fresh();
});
}
}

21
app/Domains/Billing/Actions/Catalog/RegisterProduct.php

@ -1,21 +0,0 @@
<?php
namespace App\Domains\Billing\Actions\Catalog;
use App\Domains\Billing\DTOs\Catalog\RegisterProductDTO;
use App\Domains\Billing\Models\Product;
use Illuminate\Support\Facades\DB;
class RegisterProduct
{
public function execute(RegisterProductDTO $dto): Product
{
return DB::transaction(function () use ($dto) {
return Product::create([
'name' => $dto->name,
'code' => $dto->code,
'coa_id' => $dto->coaId,
]);
});
}
}

18
app/Domains/Billing/Actions/Catalog/SetChartOfAccountForProduct.php

@ -0,0 +1,18 @@
<?php
namespace App\Domains\Billing\Actions\Catalog;
use App\Domains\Billing\Models\Product;
use Illuminate\Support\Facades\DB;
class SetChartOfAccountForProduct
{
public function execute(Product $product, int $coaId): void
{
DB::transaction(function () use ($product, $coaId) {
$product->update([
'chart_of_account_id' => $coaId,
]);
});
}
}

11
app/Domains/Billing/Actions/Discounts/ReloveDiscount.php

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

24
app/Domains/Billing/Actions/Invoicing/AttachItemsToInvoice.php

@ -0,0 +1,24 @@
<?php
namespace App\Domains\Billing\Actions\Invoicing;
use App\Domains\Billing\DTOs\Invoicing\InvoiceItemDTO;
use App\Domains\Billing\Models\InvoiceItem;
class AttachItemsToInvoice
{
public function execute(int $invoiceId, InvoiceItemDTO $item): void
{
InvoiceItem::create([
'code' => $item->code,
'name' => $item->name,
'quantity' => $item->quantity,
'gross_amount' => $item->pricing->gross_amount,
'discount_amount' => $item->pricing->discount_amount,
'net_amount' => $item->pricing->net_amount,
'invoice_id' => $invoiceId,
'product_id' => $item->product_id,
'chart_of_account_id' => $item->coa_id
]);
}
}

11
app/Domains/Billing/Actions/Invoicing/DetachUnusedItemsFromInvoice.php

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

47
app/Domains/Billing/Actions/Invoicing/DraftInvoice.php

@ -2,10 +2,51 @@
namespace App\Domains\Billing\Actions\Invoicing;
use App\Domains\Billing\DTOs\Invoicing\InvoiceDTO;
use App\Domains\Billing\Enums\Invoicing\InvoiceStatus;
use App\Domains\Billing\Models\Invoice;
use Exception;
use Illuminate\Support\Facades\DB;
use Throwable;
class DraftInvoice
{
public function execute(): void
public function __construct(
protected GenerateInvoiceCode $generateInvoiceCode,
protected AttachItemsToInvoice $attachItemsToInvoice
) {}
/**
* @throws Throwable
*/
public function execute(InvoiceDTO $dto): Invoice
{
//
try {
return DB::transaction(function() use ($dto) {
$code = $this->generateInvoiceCode->execute($dto->client_id);
$invoice = Invoice::create([
'code' => $code,
'name' => $dto->name,
'client_detail' => $dto->client_detail,
'client_ref_id' => $dto->client_ref_id,
'type' => $dto->type,
'gross_amount' => $dto->pricing->gross_amount,
'discount_amount' => $dto->pricing->discount_amount,
'net_amount' => $dto->pricing->net_amount,
'status' => InvoiceStatus::DRAFT,
'client_id' => $dto->client_id
]);
foreach ($dto->items as $item) {
$this->attachItemsToInvoice->execute(invoiceId: $invoice->id, item: $item);
}
return $invoice;
});
} catch (\Throwable $th) {
info($th->getMessage());
throw new Exception(__('domains/system.exception.something_wrong'));
}
}
}
}

45
app/Domains/Billing/Actions/Invoicing/GenerateInvoiceCode.php

@ -0,0 +1,45 @@
<?php
namespace App\Domains\Billing\Actions\Invoicing;
use App\Domains\Billing\Models\Invoice;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Throwable;
class GenerateInvoiceCode
{
/**
* @throws Throwable
*/
public function execute(int $clientId, ?string $prefix = null): string
{
$token = $prefix ? strtoupper(Str::slug($prefix)) : "C{$clientId}";
$yearMonth = now()->format('Ym');
$basePrefix = sprintf('INV-%s-%s-', $token, $yearMonth);
return DB::transaction(function () use ($basePrefix) {
// Find the last generated code with lock
$lastInvoice = Invoice::query()
->where('code', 'LIKE', "{$basePrefix}%")
->lockForUpdate()
->orderByDesc('id')
->first();
if (! $lastInvoice) {
$nextSequence = 1;
} else {
// Extract sequence number from the tail end of string
$parts = explode('-', $lastInvoice->code);
$lastSequence = (int) end($parts);
$nextSequence = $lastSequence + 1;
}
// Zero-pad sequence to 5 digits (e.g., 00001, 00002)
$paddedSequence = str_pad((string) $nextSequence, 5, '0', STR_PAD_LEFT);
return $basePrefix . $paddedSequence;
});
}
}

25
app/Domains/Billing/Actions/Invoicing/IssueInvoice.php

@ -0,0 +1,25 @@
<?php
namespace App\Domains\Billing\Actions\Invoicing;
use App\Domains\Billing\DTOs\Invoicing\InvoiceDTO;
use App\Domains\Billing\Enums\Invoicing\InvoiceStatus;
use Illuminate\Support\Facades\DB;
use Throwable;
class IssueInvoice
{
public function __construct(
protected UpdateInvoiceStatus $updateInvoiceStatus,
protected DraftInvoice $draftInvoice,
) {}
/**
* @throws Throwable
*/
public function execute(InvoiceDTO $invoiceDTO): void
{
$invoice= $this->draftInvoice->execute($invoiceDTO);
$this->updateInvoiceStatus->execute($invoice, InvoiceStatus::ISSUED);
}
}

38
app/Domains/Billing/Actions/Invoicing/UpdateInvoiceDetail.php

@ -0,0 +1,38 @@
<?php
namespace App\Domains\Billing\Actions\Invoicing;
use App\Domains\Billing\DTOs\Invoicing\UpdateInvoiceDetailDTO;
use App\Domains\Billing\Models\Invoice;
use Exception;
use Illuminate\Support\Facades\DB;
class UpdateInvoiceDetail
{
public function __construct(
protected AttachItemsToInvoice $attachItemsToInvoice
)
{}
/**
* @throws Exception
*/
public function execute(Invoice $invoice, UpdateInvoiceDetailDTO $dto): Invoice
{
try {
return DB::transaction(function() use ($invoice, $dto) {
$invoice->update(['client_detail' => $dto->client_detail]);
$invoice->items()->delete();
foreach ($dto->items as $item) {
$this->attachItemsToInvoice->execute(invoiceId: $invoice->id, item: $item);
}
return $invoice;
});
} catch (\Throwable $th) {
info($th->getMessage());
throw new Exception(__('domains/system.exception.something_wrong'));
}
}
}

14
app/Domains/Billing/Actions/Invoicing/UpdateInvoiceStatus.php

@ -0,0 +1,14 @@
<?php
namespace App\Domains\Billing\Actions\Invoicing;
use App\Domains\Billing\Enums\Invoicing\InvoiceStatus;
use App\Domains\Billing\Models\Invoice;
class UpdateInvoiceStatus
{
public function execute(Invoice $invoice, InvoiceStatus $status): void
{
$invoice->update(['status' => $status]);
}
}

12
app/Domains/Billing/DTOs/Catalog/ModifyProductDTO.php

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

12
app/Domains/Billing/DTOs/Catalog/RegisterProductDTO.php

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

20
app/Domains/Billing/DTOs/Invoicing/Context/AcademicContextDTO.php

@ -0,0 +1,20 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing\Context;
readonly class AcademicContextDTO implements ContextDTOInterface
{
public function __construct(
public string $registrationNumber,
public string $name,
public string $admissionTrackCode,
public string $programTrackCode,
public string $facultyCode,
public string $studyProgramCode,
public string $classYear,
public string $termCode,
public int $currentSemester
)
{
}
}

8
app/Domains/Billing/DTOs/Invoicing/Context/ContextDTOInterface.php

@ -0,0 +1,8 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing\Context;
interface ContextDTOInterface
{
}

28
app/Domains/Billing/DTOs/Invoicing/InvoiceDTO.php

@ -0,0 +1,28 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing;
use App\Domains\Billing\DTOs\Invoicing\Context\ContextDTOInterface;
use App\Domains\Billing\Enums\Invoicing\InvoiceType;
readonly class InvoiceDTO
{
/**
* @param string $name
* @param array $client_detail
* @param string $client_ref_id
* @param InvoiceType $type
* @param PricingDTO $pricing
* @param array<InvoiceItemDTO> $items
* @param int $client_id
*/
public function __construct(
public string $name,
public string $client_ref_id,
public array $client_detail,
public InvoiceType $type,
public PricingDTO $pricing,
public array $items,
public int $client_id,
) {}
}

38
app/Domains/Billing/DTOs/Invoicing/InvoiceItemDTO.php

@ -0,0 +1,38 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing;
use App\Domains\System\Support\ValueObjects\Money;
readonly class InvoiceItemDTO
{
public PricingDTO $pricing;
/**
* @param string $code
* @param string $name
* @param float $quantity
* @param int $product_id
* @param ?int $coa_id
* @param ?int $invoice_id
*/
public function __construct(
public string $code,
public string $name,
public float $quantity,
public int $product_id,
public ?int $coa_id,
public ?int $invoice_id = null
) {}
public function setPricing(Money $price, ?Money $discount = null): void
{
$netAmount = $price->toInt() * $this->quantity;
$this->pricing = new PricingDTO(
gross_amount: $price,
discount_amount: $discount ?: new Money(0),
net_amount: new Money($netAmount),
);
}
}

19
app/Domains/Billing/DTOs/Invoicing/PricingDTO.php

@ -0,0 +1,19 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing;
use App\Domains\System\Support\ValueObjects\Money;
readonly class PricingDTO
{
/**
* @param Money $gross_amount
* @param Money $discount_amount
* @param Money $net_amount
*/
public function __construct(
public Money $gross_amount,
public Money $discount_amount,
public Money $net_amount,
) {}
}

17
app/Domains/Billing/DTOs/Invoicing/UpdateInvoiceDetailDTO.php

@ -0,0 +1,17 @@
<?php
namespace App\Domains\Billing\DTOs\Invoicing;
use App\Domains\Billing\DTOs\Invoicing\Context\ContextDTOInterface;
readonly class UpdateInvoiceDetailDTO
{
/**
* @param ContextDTOInterface $client_detail
* @param array $items
*/
public function __construct(
public ContextDTOInterface $client_detail,
public array $items
) {}
}

27
app/Domains/Billing/Enums/Invoicing/InvoiceStatus.php

@ -2,10 +2,33 @@
namespace App\Domains\Billing\Enums\Invoicing;
enum InvoiceStatus: string
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 InvoiceStatus: string implements HasLabel, HasUiBadge
{
case PENDING = 'pending';
use HasPredicateMethod;
use InteractsWithLabels;
case DRAFT = 'draft';
case ISSUED = 'issued';
case PARTIALLY_PAID = 'partially_paid';
case PAID = 'paid';
case VOID = 'void';
/**
* @return string
*/
public function variant(): string
{
return match ($this) {
self::DRAFT => "secondary",
self::ISSUED => "info",
self::PARTIALLY_PAID => "warning",
self::PAID => "success",
self::VOID => "danger",
};
}
}

32
app/Domains/Billing/Enums/Invoicing/InvoiceType.php

@ -2,9 +2,39 @@
namespace App\Domains\Billing\Enums\Invoicing;
enum InvoiceType: string
use App\Domains\Billing\Support\Schemas\Invoice\CustomerInvoiceSchema;
use App\Domains\Billing\Support\Schemas\Invoice\StudentInvoiceSchema;
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 InvoiceType: string implements HasLabel, HasUiBadge
{
use HasPredicateMethod;
use InteractsWithLabels;
case STUDENT_INVOICE = 'student_invoice';
case VENDOR_BILL = 'vendor_bill';
case CREDIT_NOTE = 'credit_note';
public function schema(): ?CustomerInvoiceSchema
{
return match ($this) {
self::STUDENT_INVOICE => StudentInvoiceSchema::make(),
default => CustomerInvoiceSchema::make()
};
}
/**
* @return string
*/
public function variant(): string
{
return match ($this) {
self::STUDENT_INVOICE => 'primary',
self::VENDOR_BILL => 'tertiary',
self::CREDIT_NOTE => 'warning',
};
}
}

18
app/Domains/Billing/Models/Invoice.php

@ -2,11 +2,15 @@
namespace App\Domains\Billing\Models;
use App\Domains\Billing\Enums\Invoicing\InvoiceStatus;
use App\Domains\Billing\Enums\Invoicing\InvoiceType;
use App\Domains\System\Casts\MoneyCurrency;
use Database\Factories\Billing\InvoiceFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'code',
@ -24,4 +28,18 @@ use Illuminate\Database\Eloquent\Model;
class Invoice extends Model
{
use HasFactory;
protected $casts = [
'client_detail' => 'array',
'type' => InvoiceType::class,
'gross_amount' => MoneyCurrency::class,
'discount_amount' => MoneyCurrency::class,
'net_amount' => MoneyCurrency::class,
'status' => InvoiceStatus::class,
];
public function items(): HasMany
{
return $this->hasMany(InvoiceItem::class);
}
}

15
app/Domains/Billing/Models/InvoiceItem.php

@ -2,6 +2,7 @@
namespace App\Domains\Billing\Models;
use App\Domains\System\Casts\MoneyCurrency;
use Database\Factories\Billing\InvoiceItemFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@ -13,26 +14,26 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
'code',
'name',
'quantity',
'unit_price',
'gross_amount',
'discount_amount',
'net_amount',
'invoice_id',
'product_id',
'coa_id'
'chart_of_account_id'
])]
#[UseFactory(InvoiceItemFactory::class)]
class InvoiceItem extends Model
{
use HasFactory;
protected $casts = [
'gross_amount' => MoneyCurrency::class,
'discount_amount' => MoneyCurrency::class,
'net_amount' => MoneyCurrency::class,
];
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
}

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

@ -7,7 +7,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\UseFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
#[Fillable([
@ -15,18 +14,13 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
'name',
'pricing_type',
'pricing_id',
'coa_id'
'chart_of_account_id'
])]
#[UseFactory(ProductFactory::class)]
class Product extends Model
{
use HasFactory;
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class, 'coa_id');
}
public function pricing(): MorphTo
{
return $this->morphTo();

37
app/Domains/Billing/Policies/ProductPolicy.php

@ -0,0 +1,37 @@
<?php
namespace App\Domains\Billing\Policies;
use App\Domains\Billing\Models\Product;
use App\Domains\Identity\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ProductPolicy
{
use HandlesAuthorization;
public function viewAny(User $user): bool
{
return $user->can('product.viewAny');
}
public function view(User $user, Product $model): bool
{
return $user->can('product.view');
}
public function create(User $user): bool
{
return $user->can('product.create');
}
public function update(User $user, Product $model): bool
{
return $user->can('product.update');
}
public function delete(User $user, Product $model): bool
{
return $user->can('product.delete');
}
}

16
app/Domains/Billing/Queries/Catalog/SearchProductInfo.php

@ -0,0 +1,16 @@
<?php
namespace App\Domains\Billing\Queries\Catalog;
use App\Domains\Billing\Models\Product;
class SearchProductInfo
{
public static function fetch(string $model, string|int $model_id): Product
{
return Product::where('pricing_type', $model)
->where('pricing_id', $model_id)
->select(['id', 'code', 'name', 'chart_of_account_id'])
->first();
}
}

24
app/Domains/Billing/Support/Schemas/Invoice/CustomerInvoiceSchema.php

@ -0,0 +1,24 @@
<?php
namespace App\Domains\Billing\Support\Schemas\Invoice;
use App\UI\Support\Schemas\AbstractGroupSchema;
use App\UI\Support\Schemas\InputSchema;
class CustomerInvoiceSchema extends AbstractGroupSchema
{
private bool $canAddItems = true;
public function cannotAddItems(): self {
$this->canAddItems = false;
return $this;
}
public function isCanAddItems(): bool {
return $this->canAddItems;
}
public function isCannotAddItems(): bool {
return !$this->canAddItems;
}
}

118
app/Domains/Billing/Support/Schemas/Invoice/StudentInvoiceSchema.php

@ -0,0 +1,118 @@
<?php
namespace App\Domains\Billing\Support\Schemas\Invoice;
use App\Domains\Academic\Queries\Lookup\FacultyLookup;
use App\Domains\Academic\Queries\Lookup\StudyProgramLookup;
use App\Domains\Academic\Queries\Lookup\TermLookup;
use App\Domains\Admission\Queries\Lookup\AcademicProgramLookup;
use App\Domains\Admission\Queries\Lookup\AdmissionTrackLookup;
use App\Domains\Admission\Queries\Lookup\FeeTypeLookup;
use App\UI\Enums\InputType;
use App\UI\Support\Schemas\InputSchema;
class StudentInvoiceSchema extends CustomerInvoiceSchema
{
/**
* Build the complete fluent invoice input schema for student customer detail.
*/
public static function make(): static
{
return parent::make()
->cannotAddItems()
->addInput(
InputSchema::make()
->key('no_register')
->label(__('domains/billing/field.invoice.detail.no_register'))
->type(InputType::TEXTLINE)
->rules(['required', 'string'])
)
->addInput(
InputSchema::make()
->key('student_name')
->label(__('domains/billing/field.invoice.detail.student_name'))
->type(InputType::TEXTLINE)
->rules(['required', 'string'])
)
->addInput(
InputSchema::make()
->key('admission_track')
->label(__('domains/billing/field.invoice.detail.admission_track'))
->type(InputType::SELECT)
->rules(['required', 'string'])
->select2()
->withLabel()
->options(fn() => AdmissionTrackLookup::fetch())
)
->addInput(
InputSchema::make()
->key('academic_program')
->label(__('domains/billing/field.invoice.detail.academic_program'))
->type(InputType::SELECT)
->rules(['required', 'string'])
->select2()
->withLabel()
->options(fn() => AcademicProgramLookup::fetch())
)
->addInput(
InputSchema::make()
->key('faculty')
->label(__('domains/billing/field.invoice.detail.faculty'))
->type(InputType::SELECT)
->rules(['required', 'string'])
->select2()
->live()
->withLabel()
->options(fn () => FacultyLookup::fetch())
)
->addInput(
InputSchema::make()
->key('study_program')
->label(__('domains/billing/field.invoice.detail.study_program'))
->type(InputType::SELECT)
->rules(['required', 'string'])
->select2()
->live()
->dependsOn('faculty')
->setContextKeys(['faculty'])
->withLabel()
->options(fn ($context) => StudyProgramLookup::fetch($context))
)
->addInput(
InputSchema::make()
->key('class_year')
->label(__('domains/billing/field.invoice.detail.class_year'))
->type(InputType::TEXTLINE)
->rules(['required', 'string'])
)
->addInput(
InputSchema::make()
->key('term_entry')
->label(__('domains/billing/field.invoice.detail.term_entry'))
->type(InputType::SELECT)
->rules(['required'])
->select2()
->options(fn () => TermLookup::fetch())
)
->addInput(
InputSchema::make()
->key('semester')
->label(__('domains/billing/field.invoice.detail.semester'))
->type(InputType::SELECT)
->rules(['required'])
->select2()
->options(array_combine(range(1, 14), range(1, 14)))
)
->addInput(
InputSchema::make()
->key('fee_type')
->label(__('domains/billing/field.invoice.detail.fee_type'))
->type(InputType::SELECT)
->rules(['required'])
->select2()
->withLabel()
->options(fn() => FeeTypeLookup::fetch())
);
}
}

20
app/Domains/Billing/Support/Schemas/Invoice/VendorInvoiceSchema.php

@ -0,0 +1,20 @@
<?php
namespace App\Domains\Billing\Support\Schemas\Invoice;
use App\UI\Support\Schemas\AbstractGroupSchema;
use App\UI\Support\Schemas\InputSchema;
class VendorInvoiceSchema extends AbstractGroupSchema
{
/**
* Define the schema composition.
*/
public static function make(): static
{
return self::make()
->addInput(
InputSchema::make()
);
}
}

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

@ -35,7 +35,6 @@ class SynchronizeWithApi
$response = Http::withHeaders($header)->get($url);
$result = $response->json()['data'];
app(RunGenericImportPipeline::class)->execute(
rows: collect($result),
mapper: $mapper,

2
app/Domains/System/Casts/MoneyCurrency.php

@ -25,7 +25,7 @@ class MoneyCurrency implements CastsAttributes
}
if ($value instanceof Money) {
return $value->toFloat();
return $value->toInt();
}
if (is_numeric($value)) {

4
app/Domains/System/Providers/SystemServiceProvider.php

@ -10,6 +10,7 @@ use App\Domains\System\Listeners\Excel\SendImportReportEmail;
use App\Domains\System\Listeners\Files\RemoveUserFiles;
use App\Domains\System\Queries\GetSystemSettings;
use App\Domains\System\Traits\Provider\RegistersDomainEvents;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
@ -49,6 +50,9 @@ class SystemServiceProvider extends ServiceProvider
Carbon::macro('toUserTz', fn () => $this->copy()
->tz(config('app.display_timezone', 'UTC')));
Builder::macro('whereRelationUlid', function ($relation, $ulid) {
return $this->whereHas($relation, fn($builder) => $builder->where('ulid', $ulid));
});
Str::macro('initials', function (string $name, int $limit = 2) {
$words = explode(' ', trim($name));

26
app/Domains/System/Support/ValueObjects/Money.php

@ -3,12 +3,13 @@
namespace App\Domains\System\Support\ValueObjects;
use InvalidArgumentException;
use Livewire\Wireable;
use NumberFormatter;
use Stringable;
class Money implements Stringable
readonly class Money implements Stringable, Wireable
{
public function __construct(private readonly float $amount)
public function __construct(private int $amount)
{
if ($this->amount < 0) {
throw new InvalidArgumentException('Money cannot be negative.');
@ -25,7 +26,7 @@ class Money implements Stringable
return $format->formatCurrency($this->amount, $currency);
}
public function toFloat(): float
public function toInt(): int
{
return $this->amount;
}
@ -37,4 +38,23 @@ class Money implements Stringable
{
return $this->format();
}
/**
* @return mixed
*/
public function toLivewire(): mixed
{
return [
'amount' => $this->amount,
];
}
/**
* @param $value
* @return self
*/
public static function fromLivewire($value): static
{
return new static($value['amount']);
}
}

32
app/Http/Controllers/Web/Billing/InvoiceController.php

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Web\Billing;
use App\Attributes\LayoutData;
use App\Attributes\Seo;
use App\Http\Controllers\Controller;
use App\Http\DataTables\Billing\InvoiceDataTable;
use Illuminate\Http\Request;
class InvoiceController extends Controller
{
/**
* Handle the incoming request.
*/
#[Seo(
title: 'domains/billing/seo.invoice.title',
description: 'domains/billing/seo.invoice.description',
keywords: 'domains/billing/seo.invoice.keywords'
)]
#[LayoutData(
header: 'domains/billing/seo.invoice.title',
breadcrumbs: [
'ui/menu.dashboard' => 'dashboard',
'domains/billing/seo.invoice.title' => '',
],
)]
public function __invoke(InvoiceDataTable $dataTable)
{
return $dataTable->render('pages.billing.invoices.index');
}
}

1
app/Http/DataTables/Academic/FacultyDataTable.php

@ -23,7 +23,6 @@ class FacultyDataTable extends DataTable
->addColumn(
'action',
fn (Faculty $faculty) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'faculty-form-modal',
'permission' => auth()->user()->can('update', $faculty),

1
app/Http/DataTables/Academic/StudyProgramDataTable.php

@ -27,7 +27,6 @@ class StudyProgramDataTable extends DataTable
->addColumn(
'action',
fn (StudyProgram $studyProgram) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'studyprogram-form-modal',
'permission' => auth()->user()->can('update', $studyProgram),

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

@ -23,7 +23,6 @@ class TermDataTable extends DataTable
->addColumn(
'action',
fn (Term $term) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'term-form-modal',
'permission' => auth()->user()->can('update', $term),

1
app/Http/DataTables/Admission/AcademicProgramDataTable.php

@ -27,7 +27,6 @@ class AcademicProgramDataTable extends DataTable
->addColumn(
'action',
fn ($academicProgram) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'academicprogram-form-modal',
'permission' => auth()->user()->can('update', $academicProgram),

1
app/Http/DataTables/Admission/AdmissionScheduleDataTable.php

@ -25,7 +25,6 @@ class AdmissionScheduleDataTable extends DataTable
->editColumn(
'action',
fn (AdmissionSchedule $admissionSchedule) => view('components.datatables.action-button', [
'log' => true,
'view' => [
'url' => route('admission-schedules.view', ['id' => $admissionSchedule->ulid]),
'permission' => auth()->user()->can('view', $admissionSchedule),

1
app/Http/DataTables/Admission/AdmissionTrackDataTable.php

@ -27,7 +27,6 @@ class AdmissionTrackDataTable extends DataTable
->addColumn(
'action',
fn ($admissionTrack) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'admissiontrack-form-modal',
'permission' => auth()->user()->can('update', $admissionTrack),

1
app/Http/DataTables/Admission/FeeTypeDataTable.php

@ -24,7 +24,6 @@ class FeeTypeDataTable extends DataTable
->addColumn(
'action',
fn ($feeType) => view('components.datatables.action-button', [
'log' => true,
'delete' => [
'url' => null,
'title' => __('ui/button.delete'),

136
app/Http/DataTables/Billing/InvoiceDataTable.php

@ -0,0 +1,136 @@
<?php
namespace App\Http\DataTables\Billing;
use App\Domains\Billing\Models\Invoice;
use Illuminate\Database\Eloquent\Builder as QueryBuilder;
use Illuminate\Support\Facades\Blade;
use Yajra\DataTables\EloquentDataTable;
use Yajra\DataTables\Html\Builder as HtmlBuilder;
use Yajra\DataTables\Html\Button;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;
class InvoiceDataTable extends DataTable
{
/**
* Build the DataTable class.
*
* @param QueryBuilder<Invoice> $query Results from query() method.
*/
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->editColumn('net_amount', fn(Invoice $invoice) => $invoice->net_amount->format())
->editColumn('type', fn(Invoice $invoice) => view('components.badge', [
'label' => $invoice->type->label(),
'variant' => $invoice->type->variant(),
]))
->editColumn('status', fn(Invoice $invoice) => view('components.badge', [
'label' => $invoice->status->label(),
'variant' => $invoice->status->variant(),
]))
->addColumn(
'action',
fn (Invoice $invoice) => view('components.datatables.action-button', [
'view' => [
'url' => route('invoices.view', ['id' => $invoice]),
'permission' => true,
],
'edit' => [
'url' => route('invoices.update', ['invoice' => $invoice]),
'permission' => $invoice->status->isDraft(),
],
'table_name' => 'invoice-table',
'id' => $invoice->ulid,
])
)
->addIndexColumn();
}
/**
* Get the query source of dataTable.
*
* @return QueryBuilder<Invoice>
*/
public function query(Invoice $model): QueryBuilder
{
return $model->newQuery();
}
/**
* Optional method if you want to use the html builder.
*/
public function html(): HtmlBuilder
{
return $this->builder()
->setTableId('invoice-table')
->columns($this->getColumns())
->minifiedAjax()
->orderBy(1)
->layout([
'topStart' => [
'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1',
'features' => ['buttons', 'pageLength'],
],
'topEnd' => [
'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1',
'features' => ['search'],
],
'bottomStart' => 'info',
'bottomEnd' => 'paging',
])
->parameters([
'language' => [
'search' => '',
'searchPlaceholder' => __('ui/button.lookup'),
],
])
->buttons([
Button::make('add')
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
]);
}
/**
* Get the dataTable columns definition.
*/
public function getColumns(): array
{
return [
Column::computed('DT_RowIndex')
->title('#')
->searchable(false)
->orderable(false),
Column::make('code')
->title(__('domains/billing/field.invoice.code')),
Column::make('name')
->title(__('domains/billing/field.invoice.name')),
Column::make('type')
->title(__('domains/billing/field.invoice.type')),
Column::make('net_amount')
->title(__('domains/billing/field.invoice.net_amount')),
Column::make('status')
->title(__('domains/billing/field.invoice.status')),
Column::computed('action')
->title(__('ui/label.actions'))
->exportable(false)
->printable(false)
->width(60)
->addClass('text-center'),
];
}
/**
* Get the filename for export.
*/
protected function filename(): string
{
return 'InvoiceDataTable_' . date('YmdHis');
}
}

6
app/Http/DataTables/Billing/ProductDataTable.php

@ -20,7 +20,7 @@ class ProductDataTable extends DataTable
public function dataTable(QueryBuilder $query): EloquentDataTable
{
return (new EloquentDataTable($query))
->editColumn('chart_of_account.name', fn (Product $product) => $product->chartOfAccount?->name ?? '-')
->editColumn('chart_of_account.name', fn (Product $product) => $product->chartOfAccount?->code ?? '-')
->addColumn(
'action',
fn (Product $product) => view('components.datatables.action-button', [
@ -86,10 +86,6 @@ class ProductDataTable extends DataTable
'responsive' => true,
])
->buttons([
Button::make('add')
->action('$("#product-form-modal").modal("show");')
->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),
Button::make('reload')
->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
->addClass('btn-sm'),

1
app/Http/DataTables/Channel/ClientDataTable.php

@ -24,7 +24,6 @@ class ClientDataTable extends DataTable
->addColumn(
'action',
fn (Client $client) => view('components.datatables.action-button', [
'log' => true,
'edit' => [
'modal' => 'client-form-modal',
'permission' => auth()->user()->can('update', $client),

4
app/Http/Middleware/HandleSeoSetting.php

@ -30,6 +30,10 @@ class HandleSeoSetting
config(['seotools.opengraph.defaults.description' => $description]);
config(['seotools.json-ld.defaults.description' => $description]);
if (! in_array(app()->getLocale(), config('app.indexed_locales', []))) {
config(['seotools.meta.defaults.robots' => 'noindex, nofollow']);
}
return $next($request);
}
}

84
app/Livewire/Concerns/HasModelFill.php

@ -0,0 +1,84 @@
<?php
namespace App\Livewire\Concerns;
use BackedEnum;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use ReflectionException;
use ReflectionProperty;
trait HasModelFill
{
/**
* Override Livewire Form's native fill method.
* Safely hydrates attributes and automatically handles Collection <-> Array and BackedEnum conversions.
*
* @param mixed $values Data array or Eloquent Model instance.
* @return void
* @throws ReflectionException
*/
public function fill(mixed $values = []): void
{
$data = $values instanceof Model ? $values->attributesToArray() : (array) $values;
foreach (get_object_vars($this) as $property => $defaultValue) {
$hasModelValue = $values instanceof Model && isset($values->{$property});
if (! array_key_exists($property, $data) && ! $hasModelValue) {
continue;
}
$value = $values instanceof Model ? $values->{$property} : $data[$property];
$refProperty = new ReflectionProperty($this, $property);
$type = $refProperty->getType();
if($type) {
$typeName = null;
if ($type instanceof \ReflectionNamedType) {
$typeName = $type->getName();
} elseif ($type instanceof \ReflectionUnionType) {
$typeName = (string) $type;
}
if ($typeName === 'array' && $value instanceof Collection) {
$value = $value->toArray();
}
if (($typeName === Collection::class || is_subclass_of($typeName, Collection::class)) && is_array($value)) {
$value = collect($value);
}
if (enum_exists($typeName) && ! ($value instanceof BackedEnum) && $value !== null) {
$value = $typeName::tryFrom((string) $value);
}
}
// Assign hydrated value back to the Form property
$this->{$property} = $value;
}
}
/**
* Dehydrate form properties into a clean array representation suitable for DTOs or Models.
* Converts Collections to arrays and Enums to primitive values.
*
* @return array<string, mixed>
*/
public function toModelArray(): array
{
$result = [];
foreach (get_object_vars($this) as $property => $value) {
if ($value instanceof Collection) {
$result[$property] = $value->toArray();
} elseif ($value instanceof BackedEnum) {
$result[$property] = $value->value;
} else {
$result[$property] = $value;
}
}
return $result;
}
}

3
app/Livewire/Forms/Accounting/ChartOfAccountForm.php

@ -5,11 +5,14 @@ namespace App\Livewire\Forms\Accounting;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
use App\Domains\System\Enums\LifecycleStatus;
use App\Livewire\Concerns\HasModelFill;
use Livewire\Attributes\Validate;
use Livewire\Form;
class ChartOfAccountForm extends Form
{
use HasModelFill;
#[Validate('required|string|max:255', as: 'domains/accounting/field.coa.code')]
public string $code = '';

165
app/Livewire/Forms/Billing/InvoiceForm.php

@ -0,0 +1,165 @@
<?php
namespace App\Livewire\Forms\Billing;
use App\Domains\Billing\DTOs\Invoicing\InvoiceDTO;
use App\Domains\Billing\DTOs\Invoicing\InvoiceItemDTO;
use App\Domains\Billing\DTOs\Invoicing\PricingDTO;
use App\Domains\Billing\Enums\Invoicing\InvoiceStatus;
use App\Domains\Billing\Enums\Invoicing\InvoiceType;
use App\Domains\System\Support\ValueObjects\Money;
use App\Livewire\Concerns\HasModelFill;
use App\UI\Support\Schemas\InputSchemaField;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate;
use Livewire\Form;
class InvoiceForm extends Form
{
use HasModelFill;
#[Locked]
public string $id = '';
#[Validate('required|string|max:255', as: 'domains/billing/field.invoice.name')]
public string $name = '';
#[Validate('required', as: 'domains/billing/field.invoice.type')]
public ?InvoiceType $type = null;
#[Validate(['required', 'array', 'min:1'])]
public array $client_detail = [];
#[Validate]
public Money $gross_amount;
#[Validate]
public Money $discount_amount;
#[Validate]
public Money $net_amount;
#[Validate(['required', 'array', 'min:1'])]
public array $items = [];
#[Validate(as: 'domains/billing/field.invoice.type')]
public string $client_ref_id = '';
#[Validate('required', as: 'domains/billing/field.invoice.type')]
public string $client_id = '';
public function __construct($component, $propertyName)
{
parent::__construct($component, $propertyName);
$this->gross_amount = new Money(0);
$this->discount_amount = new Money(0);
$this->net_amount = new Money(0);
}
public function addItem(
string $code,
string $name,
int $quantity,
Money $price,
Money $discount,
int $product_id,
?int $coa_id
): void
{
$netAmount = $price->toInt() * $quantity;
$this->items[] = [
'code' => $code,
'name' => $name,
'quantity' => $quantity,
'gross_amount' => $price,
'discount_amount' => $discount,
'net_amount' => new Money($netAmount),
'product_id' => $product_id,
'chart_of_account_id' => $coa_id,
];
}
public function populatePricing(): void
{
$this->gross_amount = new Money(collect($this->items)->sum(fn($arr) => $arr['gross_amount']->toInt()));
$this->discount_amount = new Money(collect($this->items)->sum(fn($arr) => $arr['discount_amount']->toInt()));
$this->net_amount = new Money(collect($this->items)->sum(fn($arr) => $arr['net_amount']->toInt()));
}
public function updatedClientDetail($val, $prop): void
{
$formSchema = $this->type->schema()->getSchema();
$this->resetValidation();
foreach ($formSchema as $key => $schema) {
/** @var InputSchemaField $schema */
$schema = $schema->schema->getSchema();
if ($schema->dependsOn == $prop) {
$this->client_detail[$key] = '';
}
}
$this->validateOnly($prop);
}
public function resetSchema(): void
{
$this->reset('client_detail');
$schema = $this->type?->schema()->getSchema() ?? [];
foreach ($schema as $value) {
$this->client_detail[$value->key] = '';
}
$this->resetValidation();
}
public function rules(): array
{
return $this->type?->schema()?->getValidationRules('client_detail') ?? [];
}
public function validationAttributes(): array
{
return $this->type?->schema()?->getValidationAttributes('client_detail') ?? [];
}
public function toDTO(): InvoiceDTO
{
$items = [];
$detail = $this->type?->schema()?->resolveInputSnapshots($this->client_detail);
foreach ($this->items as $item) {
$itemDTO = new InvoiceItemDTO(
code: $item['code'],
name: $item['name'],
quantity: $item['quantity'],
product_id: $item['product_id'],
coa_id: $item['chart_of_account_id'],
);
$itemDTO->setPricing(
price: $item['gross_amount'],
discount: $item['discount_amount'],
);
$items[] = $itemDTO;
}
return new InvoiceDTO(
name: $this->name,
client_ref_id: $this->client_ref_id,
client_detail: $detail,
type: $this->type,
pricing: new PricingDTO(
gross_amount: $this->gross_amount,
discount_amount: $this->discount_amount,
net_amount: $this->net_amount,
),
items: $items,
client_id: $this->client_id
);
}
}

82
app/Livewire/Forms/Finance/InvoiceForm.php

@ -1,82 +0,0 @@
<?php
namespace App\Livewire\Forms\Finance;
use App\Domains\Finance\DTOs\PaymentProcessing\IssueInvoiceDTO;
use App\Domains\Finance\Enums\InvoiceStatus;
use App\Domains\Finance\Enums\InvoiceType;
use App\UI\Support\Schemas\InputSchemaField;
use Livewire\Attributes\Validate;
use Livewire\Form;
class InvoiceForm extends Form
{
#[Validate('required|string|max:255', as: 'domains/finance/field.invoice.name')]
public string $name = '';
#[Validate('required', as: 'domains/finance/field.invoice.type')]
public ?InvoiceType $type = null;
#[Validate]
public array $detail = [];
#[Validate('required', as: 'domains/finance/field.invoice.type')]
public string $client_id = '';
#[Validate('required', as: 'domains/finance/field.invoice.type')]
public string $coa_id = '';
#[Validate('required', as: 'domains/finance/field.invoice.type')]
public int $amount = 0;
public function updatedDetail($val, $prop): void
{
$formSchema = $this->type->schema()->getSchema();
$this->resetValidation();
foreach ($formSchema as $key => $schema) {
/** @var InputSchemaField $schema */
$schema = $schema->schema->getSchema();
if ($schema->dependsOn == $prop) {
$this->detail[$key] = '';
}
}
$this->validateOnly($prop);
}
public function updatedType(): void
{
$this->reset('detail');
$schema = $this->type?->getSchema() ?? [];
foreach ($schema as $value) {
$this->detail[$value->key] = '';
}
$this->resetValidation();
}
public function rules(): array
{
return $this->type?->schema()?->getValidationRules('detail') ?? [];
}
public function validationAttributes(): array
{
return $this->type?->schema()?->getValidationAttributes('detail') ?? [];
}
public function toDto(): IssueInvoiceDTO
{
return new IssueInvoiceDTO(
name: $this->name,
clientRefId: $this->client_id,
detail: $this->type->dtoTransform($this->detail),
type: $this->type,
status: InvoiceStatus::PENDING,
amount: $this->amount,
coaId: $this->coa_id,
clientId: $this->client_id,
);
}
}

27
app/UI/Support/Schemas/AbstractGroupSchema.php

@ -14,12 +14,37 @@ abstract class AbstractGroupSchema extends AbstractSchema
*/
public function addInput(InputSchema $input): static
{
$inputSchema = $input->getSchema();
$inputSchema = $input->withContext($this->context)->getSchema();
$this->fields[$inputSchema->key] = $input;
return $this;
}
/**
* Resolve input values along with option label snapshots.
*
* @param array<string, mixed> $inputValues
* @return array<string, mixed>
*/
public function resolveInputSnapshots(array $inputValues): array
{
$resolved = $inputValues;
foreach ($this->fields as $key => $schema) {
$fieldSchema = $schema->getSchema();
$value = $inputValues[$key] ?? null;
if ($fieldSchema->hasLabelSnapshot && ! blank($value)) {
$labelKey = $fieldSchema->labelKey;
// Calling resolveOptionLabel from InputSchema
$resolved[$labelKey] = $schema->resolveOptionLabel($value, $inputValues);
}
}
return $resolved;
}
/**
* Pass dynamic context down to all child fields.
*/

53
app/UI/Support/Schemas/InputSchema.php

@ -28,6 +28,9 @@ class InputSchema extends AbstractSchema
protected bool $live = false;
protected bool $withLabel = false;
protected string $labelKey = '';
/**
* Set the field unique key / name.
*/
@ -83,6 +86,54 @@ class InputSchema extends AbstractSchema
return $this;
}
/**
* Enable auto-attaching option label snapshot whenever selected option changes.
*
* @param string|null $customKey Optional custom key name (defaults to {key}_label).
*/
public function withLabel(?string $customKey = null): static
{
$this->withLabel = true;
$this->labelKey = $customKey ?: "{$this->key}_label";
return $this;
}
/**
* Resolve display label text for selected value ID from the dynamic options list.
*
* @param mixed $selectedValue Selected option key/ID value.
* @param array $extraContext Dynamic context passed to closure options.
* @return string|null
*/
public function resolveOptionLabel(mixed $selectedValue, array $extraContext = []): ?string
{
// Return null if no option selected
if (blank($selectedValue)) {
return null;
}
// Fetch options array or Collection
$options = $this->resolveOptions($extraContext);
if ($options instanceof Collection) {
$options = $options->toArray();
}
// Return matched label from option array or a fallback to raw value
return $options[$selectedValue] ?? (string) $selectedValue;
}
public function hasLabelSnapshot(): bool
{
return $this->withLabel;
}
public function getLabelKey(): ?string
{
return $this->labelKey ?: "{$this->key}_label";
}
public function setContextKeys(array|string $keys): static
{
$this->contextKeys = (array) $keys;
@ -154,7 +205,9 @@ class InputSchema extends AbstractSchema
default: $this->default,
isLive: $this->isLive(),
dependsOn: $this->depends,
hasLabelSnapshot: $this->hasLabelSnapshot(),
schema: $this,
labelKey: $this->getLabelKey(),
);
}
}

2
app/UI/Support/Schemas/InputSchemaField.php

@ -15,6 +15,8 @@ readonly class InputSchemaField
public mixed $default,
public bool $isLive,
public string $dependsOn,
public bool $hasLabelSnapshot = false,
public InputSchema $schema,
public ?string $labelKey = null,
) {}
}

2
config/app.php

@ -84,6 +84,8 @@ return [
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
'indexed_locales' => ['en', 'id'],
/*
|--------------------------------------------------------------------------
| Encryption Key

2
database/migrations/2026_07_06_031003_create_clients_table.php

@ -19,7 +19,7 @@ return new class extends Migration
$table->text('secret');
$table->text('auth_type');
$table->text('webhook');
$table->longText('credentials');
$table->longText('credentials')->default('{}');
$table->timestamps();
});
}

2
database/migrations/2026_09_08_022815_create_chart_of_accounts_table.php

@ -19,7 +19,7 @@ return new class extends Migration
$table->string('classification');
$table->string('normal_balance');
$table->foreignId('parent_id')->nullable();
$table->string('status');
$table->string('status')->default('active');
$table->timestamps();
});
}

6
database/migrations/2026_09_08_022818_create_invoice_items_table.php

@ -16,13 +16,15 @@ return new class extends Migration
$table->string('code');
$table->string('name');
$table->integer('quantity');
$table->integer('unit_price');
$table->integer('gross_amount');
$table->integer('discount_amount');
$table->integer('net_amount');
$table->foreignId('invoice_id')->constrained();
$table->foreignId('product_id')->constrained();
$table->foreignId('chart_of_account_id')->constrained();
$table->foreignId('chart_of_account_id')
->nullable()
->constrained()
->nullOnDelete();
$table->timestamps();
});
}

93
database/seeders/Accounting/ChartOfAccountSeeder.php

@ -0,0 +1,93 @@
<?php
namespace Database\Seeders\Accounting;
use App\Domains\Accounting\Enums\Ledger\AccountBalance;
use App\Domains\Accounting\Enums\Ledger\AccountClassification;
use App\Domains\Accounting\Models\ChartOfAccount;
use Illuminate\Database\Seeder;
/**
* Seeder to populate initial baseline Chart of Accounts for campus payment operations.
*/
class ChartOfAccountSeeder extends Seeder
{
/**
* Run the database seeds for baseline accounts.
*
* @return void
*/
public function run(): void
{
// Create parent Revenue header account (COA 41000 - Pendapatan Operasional)
$revenueParent = ChartOfAccount::updateOrCreate(
['code' => '41000'],
[
'name' => 'Pendapatan Operasional',
'classification' => AccountClassification::REVENUE,
'normal_balance' => AccountBalance::CREDIT,
'parent_id' => null,
]
);
// Define child sub-account definitions array matching campus operational ledger
$accounts = [
[
// Asset account for Bank BNI VA Receipts
'code' => '1102',
'name' => 'Bank BNI Penerimaan VA',
'classification' => AccountClassification::ASSET,
'normal_balance' => AccountBalance::DEBIT,
'parent_id' => null,
],
[
// Asset account for Student Receivables
'code' => '1105',
'name' => 'Piutang Mahasiswa',
'classification' => AccountClassification::ASSET,
'normal_balance' => AccountBalance::DEBIT,
'parent_id' => null,
],
[
// Sub-COA 41101: New Student Registration Fee (Pendapatan Pendaftaran Maba)
'code' => '41101',
'name' => 'Pendapatan Pendaftaran Maba-Reguler',
'classification' => AccountClassification::REVENUE,
'normal_balance' => AccountBalance::CREDIT,
'parent_id' => $revenueParent->id,
],
[
// Sub-COA 41200: Re-registration Fee (Pendapatan Kemahasiswaan / Daftar Ulang)
'code' => '41200',
'name' => 'Pendapatan Kemahasiswaan / Daftar Ulang',
'classification' => AccountClassification::REVENUE,
'normal_balance' => AccountBalance::CREDIT,
'parent_id' => $revenueParent->id,
],
[
// Sub-COA 41301: Regular Tuition Fee (Pendapatan UKT-Reguler)
'code' => '41301',
'name' => 'Pendapatan UKT-Reguler',
'classification' => AccountClassification::REVENUE,
'normal_balance' => AccountBalance::CREDIT,
'parent_id' => $revenueParent->id,
],
[
// Sub-COA 4201: Contra Revenue for Discounts (Diskon & Potongan Tagihan)
'code' => '4201',
'name' => 'Diskon PMDK & Potongan Tagihan',
'classification' => AccountClassification::REVENUE,
'normal_balance' => AccountBalance::DEBIT, // Debit offsets credit revenue
'parent_id' => null,
],
];
// Loop and upsert each account entry into database
foreach ($accounts as $account) {
ChartOfAccount::updateOrCreate(
['code' => $account['code']],
$account
);
}
}
}

83
database/seeders/Channel/ClientSeeder.php

@ -0,0 +1,83 @@
<?php
namespace Database\Seeders\Channel;
use App\Domains\Channel\Enums\Api\ApiType;
use App\Domains\Channel\Enums\Client\AuthType;
use App\Domains\Channel\Models\ApiEndpoint;
use App\Domains\Channel\Models\Client;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class ClientSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$siakad = Client::create([
'name' => 'SIAKAD',
'domain' => 'http://admin.siakad.test',
'webhook' => '/webhook',
'auth_type' => AuthType::NONE,
'credentials' => [],
'secret' => sha1('secret'.time())
]);
$pmb = Client::create([
'name' => 'PMB',
'domain' => 'http://pmb.test',
'webhook' => '/api/webhook',
'auth_type' => AuthType::NONE,
'credentials' => [],
'secret' => sha1('secret'.time())
]);
$apiEndpoints = [
[
'key' => ApiType::FACULTY,
'value' => '/InternalApi/list_data/fakultas',
'client_id' => $siakad->id
],
[
'key' => ApiType::STUDY_PROGRAM,
'value' => '/InternalApi/list_data/prodi',
'client_id' => $siakad->id
],
[
'key' => ApiType::TERM,
'value' => '/InternalApi/list_data/semester',
'client_id' => $siakad->id
],
[
'key' => ApiType::ADMISSION_TRACK,
'value' => '/api/jalur-pendaftaran',
'client_id' => $pmb->id
],
[
'key' => ApiType::ACADEMIC_PROGRAM,
'value' => '/api/sistem-kuliah',
'client_id' => $pmb->id
],
[
'key' => ApiType::ADMISSION_SCHEDULE,
'value' => '/api/jadwal-pendaftaran',
'client_id' => $pmb->id
],
[
'key' => ApiType::FEE_TYPE,
'value' => '/api/jenis-tagihan',
'client_id' => $pmb->id
],
[
'key' => ApiType::FEE_RATE,
'value' => '/api/biaya-pendaftaran',
'client_id' => $pmb->id
],
];
foreach ($apiEndpoints as $endpoint) {
ApiEndpoint::create($endpoint);
}
}
}

5
database/seeders/DatabaseSeeder.php

@ -2,7 +2,8 @@
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Database\Seeders\Accounting\ChartOfAccountSeeder;
use Database\Seeders\Channel\ClientSeeder;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
@ -15,5 +16,7 @@ class DatabaseSeeder extends Seeder
$this->call(RoleSeeder::class);
$this->call(UserSeeder::class);
$this->call(SystemSettingSeeder::class);
$this->call(ClientSeeder::class);
$this->call(ChartOfAccountSeeder::class);
}
}

6
lang/en.json

@ -0,0 +1,6 @@
{
"The email has already been verified.": "The email has already been verified.",
"The verification email has been sent.": "The verification email has been sent.",
"components.layouts.guest": "Guest Layout",
"components.layouts.app": "App Layout"
}

26
lang/en/domains/account/enum.php

@ -1,30 +1,8 @@
<?php
use App\Domains\Account\Enums\GenderOption;
return [
'gender_option' => [
GenderOption::MALE->value => 'Male',
GenderOption::FEMALE->value => 'Female',
],
'user_setting_key' => [
'notification' => 'Notification',
'language' => 'Language',
'timezone' => 'Timezone',
'options' => [
'language' => [
'en' => 'English',
'id' => 'Indonesian',
],
'notification' => [
'on' => 'On',
'off' => 'Off',
],
],
],
'user_settings' => [
'notification' => 'Notification',
'language' => 'Language',
'timezone' => 'Timezone',
'male' => 'Male',
'female' => 'Female',
],
];

10
lang/en/domains/account/pages.php

@ -3,15 +3,7 @@
return [
'profile' => [
'title' => 'Profile Information',
'description' => "Update your account's profile information and email address.",
],
'password' => [
'title' => 'Update Password',
'description' => 'Ensure your account is using a long, random password to stay secure.',
],
'delete_account' => [
'title' => 'Delete Account',
'description' => 'Once your account is deleted, all of its resources and data will be permanently deleted.',
'description' => 'Update your account\'s profile information and email address.',
],
'user_settings' => [
'title' => 'User Settings',

5
lang/en/domains/admission/exception.php

@ -0,0 +1,5 @@
<?php
return [
'fee_rate_not_found' => 'Admission fee rate not found.',
];

2
lang/en/domains/admission/field.php

@ -7,11 +7,9 @@ return [
're_registration_date_start' => 'Re-registration Start Date',
're_registration_date_end' => 'Re-registration End Date',
'track_name' => 'Track Name',
'status' => 'Status',
'term_name' => 'Term',
],
'admission_track' => [
'code' => 'Track Code',
'name' => 'Admission Track Name',
'status' => 'Status',
],

2
lang/en/domains/admission/seo.php

@ -9,7 +9,7 @@ return [
'admission_schedule_detail' => [
'title' => 'Admission Schedule Detail',
'description' => 'View details of the admission schedule.',
'keywords' => 'admission schedule detail, admission, schedule',
'keywords' => 'admission track schedule, admission schedule, new student admission schedule.',
],
'admission_track' => [
'title' => 'Admission Track Data',

6
lang/en/domains/auth/pages.php

@ -3,7 +3,7 @@
return [
'login' => [
'header' => 'Sign in to your account',
'no_account' => "Don't have an account?",
'no_account' => 'Don\'t have an account?',
'register_link' => 'Sign up',
],
'register' => [
@ -20,9 +20,7 @@ return [
'header' => 'Reset Password',
],
'verify_email' => [
'header' => 'Verify Email',
'subheader' => "Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.",
'resend_link' => 'A new verification link has been sent to the email address you provided during registration.',
'subheader' => 'Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.',
],
'confirm_password' => [
'header' => 'Confirm Password',

5
lang/en/domains/auth/seo.php

@ -21,11 +21,6 @@ return [
'description' => 'Set a new password for your account.',
'keywords' => 'reset password, auth',
],
'verify_email' => [
'title' => 'Verify Email',
'description' => 'Verify your email address to secure your account.',
'keywords' => 'verify email, auth',
],
'confirm_password' => [
'title' => 'Confirm Password',
'description' => 'Please confirm your password before continuing.',

10
lang/en/domains/billing.php

@ -0,0 +1,10 @@
<?php
return [
'messages' => [
'invoice' => [
'draft_success' => 'Invoice draft saved successfully.',
'issue_success' => 'Invoice issued successfully.',
],
],
];

8
lang/en/domains/billing/button.php

@ -0,0 +1,8 @@
<?php
return [
'issue_invoice' => 'Issue Invoice',
'issue_this_invoice' => 'Issue This Invoice',
'check_student_admission' => 'Check Student Admission',
'save_invoice_draft' => 'Save Invoice Draft',
];

16
lang/en/domains/billing/enum.php

@ -0,0 +1,16 @@
<?php
return [
'invoice_type' => [
'student_invoice' => 'Student Invoice',
'vendor_bill' => 'Vendor Bill',
'credit_note' => 'Credit Note',
],
'invoice_status' => [
'draft' => 'Draft',
'issued' => 'Issued',
'partially_paid' => 'Partially Paid',
'paid' => 'Paid',
'void' => 'Void',
],
];

30
lang/en/domains/billing/field.php

@ -6,4 +6,34 @@ return [
'name' => 'Product Name',
'coa' => 'Chart of Account (COA)',
],
'invoice' => [
'code' => 'Invoice Code',
'name' => 'Invoice Name',
'net_amount' => 'Net Amount',
'status' => 'Status',
'type' => 'Type',
'gross_amount' => 'Gross Amount',
'discount_amount' => 'Discount Amount',
'client_name' => 'Client Name',
'detail' => [
'no_register' => 'Registration Number',
'student_name' => 'Student Name',
'admission_track' => 'Admission Track',
'academic_program' => 'Academic Program',
'faculty' => 'Faculty',
'study_program' => 'Study Program',
'class_year' => 'Class Year',
'term_entry' => 'Term Entry',
'semester' => 'Semester',
'fee_type' => 'Fee Type',
],
],
'invoice_item' => [
'code' => 'Item Code',
'name' => 'Item Name',
'quantity' => 'Quantity',
'gross_price' => 'Gross Price',
'discounted_price' => 'Discounted Price',
'net_price' => 'Net Price',
],
];

5
lang/en/domains/billing/messages.php

@ -0,0 +1,5 @@
<?php
return [
'invoice_issued' => 'Invoice issued successfully.',
];

19
lang/en/domains/billing/section.php

@ -0,0 +1,19 @@
<?php
return [
'invoice' => [
'customer_detail' => [
'title' => 'Customer Details',
'subtitle' => 'Detailed information about the student or customer.',
],
'summary' => [
'title' => 'Invoice Summary',
'subtitle' => 'Breakdown of total amounts for this invoice.',
],
'items' => [
'title' => 'Invoice Items',
'subtitle' => 'List of products or services included in this invoice.',
],
'select_invoice_type' => 'Select Invoice Type',
],
];

8
lang/en/domains/billing/seo.php

@ -6,4 +6,12 @@ return [
'description' => 'Manage products and their chart of account mappings.',
'keywords' => 'product, products, billing, finance, coa',
],
'invoice' => [
'title' => 'Invoice Data',
'description' => 'Manage invoices and billing transactions.',
'keywords' => 'invoice, billing, transaction, finance',
],
'create_invoice' => [
'title' => 'Create Invoice',
],
];

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

@ -9,7 +9,6 @@ return [
'credentials' => [
'username' => 'Username',
'password' => 'Password',
'token' => 'Token',
'bearer' => 'Bearer Token',
],
'endpoint_url' => 'Endpoint URL',

12
lang/en/domains/identity/enum.php

@ -1,17 +1,9 @@
<?php
use App\Domains\Identity\Enums\RoleType;
use App\Domains\Identity\Enums\UserStatus;
return [
'user_status' => [
UserStatus::ACTIVE->value => 'Active',
UserStatus::INACTIVE->value => 'Inactive',
],
'role_type' => [
RoleType::SYSTEM_ADMIN->value => 'System Administrator',
RoleType::ADMIN->value => 'Administrator',
RoleType::USER->value => 'User',
'active' => 'Active',
'inactive' => 'Inactive',
],
'user_setting_key' => [
'notification' => 'Notification',

5
lang/en/domains/identity/messages.php

@ -2,14 +2,9 @@
return [
'success' => [
'password_reset_link_sent' => 'Password reset link has been sent.',
'user_role_updated' => 'User role has been updated.',
'user_status_updated' => 'User status has been updated.',
],
'exceptions' => [
'failed_to_send_password_reset_link' => 'Failed to send password reset link.',
'failed_to_update_user_role' => 'Failed to update user role.',
'failed_to_update_user_status' => 'Failed to update user status.',
'user_already_active' => 'This user is already active.',
'user_already_suspended' => 'This user is already suspended.',
'user_already_status' => 'User is already in this status.',

31
lang/en/domains/identity/notifications.php

@ -1,11 +1,6 @@
<?php
return [
'user_registered' => [
'subject' => 'Welcome!',
'body' => 'Your account is ready.',
'action' => 'Login',
],
'governance' => [
'user_suspended' => [
'subject' => 'Account Suspended',
@ -26,4 +21,30 @@ return [
'outro' => 'If you have any questions, please contact our support team.',
],
],
'auth' => [
'reset_password' => [
'subject' => 'Reset Password Notification',
'intro' => 'You are receiving this email because we received a password reset request for your account.',
'action' => 'Reset Password',
'outro' => 'This password reset link will expire in :count minutes. If you did not request a password reset, no further action is required.',
],
'sign_in_activity' => [
'subject' => 'New Sign-in Activity Detected',
'greeting' => 'Hello, :name!',
'intro' => 'A new sign-in was detected on your account at :time.',
'details' => [
'ip' => 'IP Address: :ip',
'browser' => 'Browser: :browser',
],
'warning' => 'If this was not you, please secure your account immediately.',
'action' => 'Go to Dashboard',
'outro' => 'Thank you for using our application!',
],
'verify_email' => [
'subject' => 'Verify Email Address',
'intro' => 'Please click the button below to verify your email address.',
'action' => 'Verify Email Address',
'outro' => 'If you did not create an account, no further action is required.',
],
],
];

3
lang/en/domains/identity/pages.php

@ -11,10 +11,7 @@ return [
],
'confirmation' => [
'toggle_status' => 'Are you sure you want to change the status of this user?',
'status_changed' => 'User status has been successfully updated.',
'status_unchanged' => 'User status was not changed.',
'send_password_reset' => 'Are you sure you want to send a password reset link to this user?',
'password_reset_success' => 'Password reset link has been successfully sent.',
],
],
];

5
lang/en/domains/identity/seo.php

@ -6,11 +6,6 @@ return [
'description' => 'Manage user accounts, roles, and permissions.',
'keywords' => 'user management, users, identity',
],
'user_detail' => [
'title' => 'Detail of :name',
'description' => 'Manage user accounts, roles, and permissions.',
'keywords' => 'user management, users, identity',
],
'role' => [
'title' => 'Role Management',
'description' => 'Manage user roles and permissions.',

7
lang/en/domains/system.php

@ -0,0 +1,7 @@
<?php
return [
'exception' => [
'something_wrong' => 'Something went wrong. Please try again later.',
],
];

31
lang/en/domains/system/enum.php

@ -1,26 +1,23 @@
<?php
use App\Domains\System\Enums\LifecycleStatus;
use App\Domains\System\Enums\SystemSettingKey;
return [
'lifecycle_status' => [
LifecycleStatus::ACTIVE->value => 'Active',
LifecycleStatus::INACTIVE->value => 'Inactive',
'active' => 'Active',
'inactive' => 'Inactive',
],
'system_setting_key' => [
SystemSettingKey::WEB_NAME->value => 'Website Name',
SystemSettingKey::WEB_DESCRIPTION->value => 'Website Description',
SystemSettingKey::WEB_LOGO->value => 'Website Logo',
SystemSettingKey::WEB_FAVICON->value => 'Website Favicon',
SystemSettingKey::WEB_PHONE->value => 'Website Phone',
SystemSettingKey::WEB_EMAIL->value => 'Website Email',
SystemSettingKey::WEB_ADDRESS->value => 'Website Address',
SystemSettingKey::DEFAULT_LANGUAGE->value => 'Default Language',
SystemSettingKey::TIMEZONE->value => 'Timezone',
SystemSettingKey::GOOGLE_TAG_MANAGER_ID->value => 'Google Tag Manager ID',
SystemSettingKey::GOOGLE_WEBMASTER_ID->value => 'Google Webmaster ID',
SystemSettingKey::CURRENT_TERM->value => 'Current Term',
'web-name' => 'Website Name',
'web-description' => 'Website Description',
'web-logo' => 'Website Logo',
'web-favicon' => 'Website Favicon',
'web-phone' => 'Website Phone',
'web-email' => 'Website Email',
'web-address' => 'Website Address',
'default_language' => 'Default Language',
'timezone' => 'Timezone',
'google-tag_manager_id' => 'Google Tag Manager ID',
'google-webmaster_id' => 'Google Webmaster ID',
'current_term' => 'Current Term',
],
'system_setting_key_options' => [
'default_language' => [

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

Loading…
Cancel
Save