Browse Source
- 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 DTOsmaster
167 changed files with 7151 additions and 713 deletions
File diff suppressed because it is too large
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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, '.'); |
||||
|
} |
||||
|
} |
||||
@ -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, '.'); |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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')); |
||||
|
} |
||||
|
} |
||||
@ -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')); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -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, |
|
||||
]); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
@ -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, |
||||
|
]); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Billing\Actions\Discounts; |
||||
|
|
||||
|
class ReloveDiscount |
||||
|
{ |
||||
|
public function execute(): void |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
]); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Billing\Actions\Invoicing; |
||||
|
|
||||
|
class DetachUnusedItemsFromInvoice |
||||
|
{ |
||||
|
public function execute(): void |
||||
|
{ |
||||
|
// |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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')); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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]); |
||||
|
} |
||||
|
} |
||||
@ -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, |
|
||||
) {} |
|
||||
} |
|
||||
@ -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, |
|
||||
) {} |
|
||||
} |
|
||||
@ -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 |
||||
|
) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Domains\Billing\DTOs\Invoicing\Context; |
||||
|
|
||||
|
interface ContextDTOInterface |
||||
|
{ |
||||
|
|
||||
|
} |
||||
@ -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, |
||||
|
) {} |
||||
|
} |
||||
@ -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), |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
) {} |
||||
|
} |
||||
@ -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 |
||||
|
) {} |
||||
|
} |
||||
@ -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'); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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()) |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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() |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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'); |
||||
|
} |
||||
|
} |
||||
@ -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'); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
); |
||||
|
} |
||||
|
} |
||||
@ -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, |
|
||||
); |
|
||||
} |
|
||||
} |
|
||||
@ -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 |
||||
|
); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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" |
||||
|
} |
||||
@ -1,30 +1,8 @@ |
|||||
<?php |
<?php |
||||
|
|
||||
use App\Domains\Account\Enums\GenderOption; |
|
||||
|
|
||||
return [ |
return [ |
||||
'gender_option' => [ |
'gender_option' => [ |
||||
GenderOption::MALE->value => 'Male', |
'male' => 'Male', |
||||
GenderOption::FEMALE->value => 'Female', |
'female' => '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', |
|
||||
], |
], |
||||
]; |
]; |
||||
|
|||||
@ -0,0 +1,5 @@ |
|||||
|
<?php |
||||
|
|
||||
|
return [ |
||||
|
'fee_rate_not_found' => 'Admission fee rate not found.', |
||||
|
]; |
||||
@ -0,0 +1,10 @@ |
|||||
|
<?php |
||||
|
|
||||
|
return [ |
||||
|
'messages' => [ |
||||
|
'invoice' => [ |
||||
|
'draft_success' => 'Invoice draft saved successfully.', |
||||
|
'issue_success' => 'Invoice issued successfully.', |
||||
|
], |
||||
|
], |
||||
|
]; |
||||
@ -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', |
||||
|
]; |
||||
@ -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', |
||||
|
], |
||||
|
]; |
||||
@ -0,0 +1,5 @@ |
|||||
|
<?php |
||||
|
|
||||
|
return [ |
||||
|
'invoice_issued' => 'Invoice issued successfully.', |
||||
|
]; |
||||
@ -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', |
||||
|
], |
||||
|
]; |
||||
@ -0,0 +1,7 @@ |
|||||
|
<?php |
||||
|
|
||||
|
return [ |
||||
|
'exception' => [ |
||||
|
'something_wrong' => 'Something went wrong. Please try again later.', |
||||
|
], |
||||
|
]; |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue