1
0

Giant Update

This commit is contained in:
Wian Drs
2026-07-31 13:57:11 +07:00
parent f68d967c8b
commit b0d08211ec
112 changed files with 6674 additions and 3 deletions
@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers\Billing;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Billing\IndexBillingRequest;
use App\Http\Requests\Billing\StoreBillingProfileRequest;
use App\Http\Requests\Billing\UpdateBillingProfileRequest;
use App\Http\Resources\Billing\BillingProfileResource;
use App\Services\Billing\BillingProfileService;
use Illuminate\Http\Request;
class BillingProfileController extends ApiController
{
public function __construct(protected BillingProfileService $service) {}
public function index(IndexBillingRequest $request)
{
return $this->successPaginated($this->service->list($request->validated(), $request->user(), $this->tenantId($request)), BillingProfileResource::class, 'Daftar profile tagihan berhasil diambil');
}
public function store(StoreBillingProfileRequest $request)
{
return $this->created(new BillingProfileResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))), 'Profile tagihan berhasil dibuat');
}
public function show(Request $request, int $id)
{
$profile = $this->service->find($id);
$this->authorizeTenant($request, $profile->tenant_id);
return $this->success(new BillingProfileResource($profile), 'Detail profile tagihan berhasil diambil');
}
public function update(UpdateBillingProfileRequest $request, int $id)
{
$profile = $this->service->find($id);
$this->authorizeTenant($request, $profile->tenant_id);
return $this->updated(new BillingProfileResource($this->service->update($profile, $request->validated(), $request->user())), 'Profile tagihan berhasil diperbarui');
}
public function destroy(Request $request, int $id)
{
$profile = $this->service->find($id);
$this->authorizeTenant($request, $profile->tenant_id);
$this->service->delete($profile);
return $this->deleted('Profile tagihan berhasil dihapus');
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
private function authorizeTenant(Request $request, int $tenantId): void
{
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
}
}
@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Billing;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Billing\IndexBillingRequest;
use App\Http\Requests\Billing\PayInvoiceRequest;
use App\Http\Requests\Billing\StoreInvoiceRequest;
use App\Http\Requests\Billing\UpdateInvoiceRequest;
use App\Http\Resources\Billing\InvoiceResource;
use App\Services\Billing\InvoiceService;
use Illuminate\Http\Request;
class InvoiceController extends ApiController
{
public function __construct(protected InvoiceService $service) {}
public function index(IndexBillingRequest $request)
{
return $this->successPaginated($this->service->list($this->scope($request), $request->validated(), $request->user(), $this->tenantId($request)), InvoiceResource::class, 'Daftar tagihan berhasil diambil');
}
public function options(Request $request)
{
return $this->success($this->service->options($request->user(), $this->tenantId($request)), 'Pilihan tagihan berhasil diambil');
}
public function store(StoreInvoiceRequest $request)
{
return $this->created(new InvoiceResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))), 'Tagihan berhasil dibuat');
}
public function show(Request $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->success(new InvoiceResource($invoice), 'Detail tagihan berhasil diambil');
}
public function update(UpdateInvoiceRequest $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->updated(new InvoiceResource($this->service->update($invoice, $request->validated(), $request->user())), 'Tagihan berhasil diperbarui');
}
public function pay(PayInvoiceRequest $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->updated(new InvoiceResource($this->service->pay($invoice, $request->validated(), $request->user())), 'Pembayaran manual berhasil dicatat');
}
public function cancel(Request $request, int $id)
{
$invoice = $this->service->find($id);
$this->authorizeTenant($request, $invoice->tenant_id);
return $this->updated(new InvoiceResource($this->service->cancel($invoice, $request->user())), 'Tagihan berhasil dibatalkan');
}
private function scope(Request $request): string
{
return $request->route()->defaults['invoice_scope'] ?? 'running';
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
private function authorizeTenant(Request $request, int $tenantId): void
{
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
}
}
@@ -0,0 +1,118 @@
<?php
namespace App\Http\Controllers\Customer;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Customer\ActivateCustomerRequest;
use App\Http\Requests\Customer\IndexCustomerRequest;
use App\Http\Requests\Customer\StoreCustomerRequest;
use App\Http\Requests\Customer\UpdateCustomerRequest;
use App\Http\Resources\Customer\CustomerResource;
use App\Services\Customer\CustomerService;
use Illuminate\Http\Request;
class CustomerController extends ApiController
{
public function __construct(protected CustomerService $service) {}
public function index(IndexCustomerRequest $request)
{
return $this->successPaginated(
$this->service->list($this->scope($request), $request->validated(), $request->user(), $this->tenantId($request)),
CustomerResource::class,
'Daftar customer berhasil diambil'
);
}
public function options(Request $request)
{
return $this->success(
$this->service->options($request->user(), $this->tenantId($request)),
'Pilihan relasi customer berhasil diambil'
);
}
public function store(StoreCustomerRequest $request)
{
return $this->created(
new CustomerResource($this->service->create($request->validated(), $request->user(), $this->tenantId($request))),
'Customer berhasil didaftarkan'
);
}
public function show(Request $request, int $id)
{
$customer = $this->service->find($id, $this->scope($request) === 'trash');
$this->authorizeTenant($request, $customer->tenant_id);
return $this->success(new CustomerResource($customer), 'Detail customer berhasil diambil');
}
public function update(UpdateCustomerRequest $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(
new CustomerResource($this->service->update($customer, $request->validated(), $request->user())),
'Data customer berhasil diperbarui'
);
}
public function activate(ActivateCustomerRequest $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(new CustomerResource($this->service->activate($customer, $request->validated())), 'Customer berhasil diaktifkan');
}
public function deactivate(Request $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(new CustomerResource($this->service->deactivate($customer)), 'Customer berhasil dinonaktifkan');
}
public function destroy(Request $request, int $id)
{
$customer = $this->service->find($id);
$this->authorizeTenant($request, $customer->tenant_id);
$this->service->trash($customer);
return $this->deleted('Customer dipindahkan ke Sampah');
}
public function restore(Request $request, int $id)
{
$customer = $this->service->find($id, true);
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated(new CustomerResource($this->service->restore($customer)), 'Customer berhasil dipulihkan');
}
public function forceDelete(Request $request, int $id)
{
$customer = $this->service->find($id, true);
$this->authorizeTenant($request, $customer->tenant_id);
$this->service->forceDelete($customer);
return $this->deleted('Customer dihapus permanen');
}
private function scope(Request $request): string
{
return $request->route()->defaults['customer_scope'] ?? 'orders';
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
private function authorizeTenant(Request $request, int $tenantId): void
{
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
}
}
@@ -0,0 +1,192 @@
<?php
namespace App\Http\Controllers\Notification;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Notification\IndexNotificationRequest;
use App\Http\Requests\Notification\SaveNotificationPreferencesRequest;
use App\Http\Requests\Notification\StoreCustomerContactRequest;
use App\Http\Requests\Notification\StoreNotificationBroadcastRequest;
use App\Http\Requests\Notification\StoreNotificationChannelRequest;
use App\Http\Requests\Notification\StoreNotificationTemplateRequest;
use App\Http\Resources\Notification\NotificationDeliveryResource;
use App\Models\Customer;
use App\Models\CustomerContact;
use App\Models\NotificationBroadcast;
use App\Models\NotificationChannel;
use App\Models\NotificationEvent;
use App\Models\NotificationTemplate;
use App\Services\Notification\NotificationService;
use Illuminate\Http\Request;
class NotificationController extends ApiController
{
public function __construct(protected NotificationService $service) {}
public function events()
{
return $this->success(NotificationEvent::where('enabled', true)->orderBy('category')->orderBy('name')->get(), 'Daftar event notifikasi berhasil diambil');
}
public function channels(Request $request)
{
return $this->success($this->service->channels($this->channel($request), $request->user(), $this->tenantId($request)), 'Konfigurasi channel berhasil diambil');
}
public function storeChannel(StoreNotificationChannelRequest $request)
{
abort_unless($request->validated('channel') === $this->channel($request), 422);
return $this->created($this->service->saveChannel($request->validated(), $request->user(), $this->tenantId($request)), 'Konfigurasi channel berhasil dibuat');
}
public function updateChannel(StoreNotificationChannelRequest $request, NotificationChannel $notificationChannel)
{
$this->authorizeTenant($request, $notificationChannel->tenant_id);
abort_unless($notificationChannel->channel === $this->channel($request), 404);
return $this->updated($this->service->saveChannel($request->validated(), $request->user(), $this->tenantId($request), $notificationChannel), 'Konfigurasi channel berhasil diperbarui');
}
public function testChannel(Request $request, NotificationChannel $notificationChannel)
{
$this->authorizeTenant($request, $notificationChannel->tenant_id);
abort_unless($notificationChannel->channel === $this->channel($request), 404);
return $this->success($this->service->testChannel($notificationChannel), 'Pengujian channel selesai');
}
public function templates(Request $request)
{
return $this->success($this->service->templates($this->channel($request), $request->user(), $this->tenantId($request)), 'Daftar template berhasil diambil');
}
public function storeTemplate(StoreNotificationTemplateRequest $request)
{
abort_unless($request->validated('channel') === $this->channel($request), 422);
return $this->created($this->service->saveTemplate($request->validated(), $request->user(), $this->tenantId($request)), 'Template berhasil dibuat');
}
public function updateTemplate(StoreNotificationTemplateRequest $request, NotificationTemplate $notificationTemplate)
{
if ($notificationTemplate->tenant_id) {
$this->authorizeTenant($request, $notificationTemplate->tenant_id);
}
abort_unless($notificationTemplate->channel === $this->channel($request), 404);
return $this->updated($this->service->saveTemplate($request->validated(), $request->user(), $this->tenantId($request), $notificationTemplate), 'Template berhasil diperbarui');
}
public function preferences(Request $request)
{
$tenantId = $request->user()->isMasterAdmin() && $request->integer('tenant_id')
? $request->integer('tenant_id')
: $this->tenantId($request);
return $this->success($this->service->preferences($request->user(), $tenantId), 'Preference notifikasi berhasil diambil');
}
public function savePreferences(SaveNotificationPreferencesRequest $request)
{
$this->service->savePreferences($request->validated('preferences'), $request->user(), $this->tenantId($request));
return $this->updated(null, 'Preference notifikasi berhasil disimpan');
}
public function deliveries(IndexNotificationRequest $request)
{
return $this->successPaginated(
$this->service->deliveries($this->channel($request), $request->validated(), $request->user(), $this->tenantId($request)),
NotificationDeliveryResource::class,
'Log pesan berhasil diambil',
);
}
public function broadcasts(IndexNotificationRequest $request)
{
return $this->success($this->service->broadcasts($request->validated(), $request->user(), $this->tenantId($request)), 'Daftar pesan siaran berhasil diambil');
}
public function previewBroadcast(StoreNotificationBroadcastRequest $request)
{
return $this->success($this->service->previewBroadcast($request->validated(), $request->user(), $this->tenantId($request)), 'Preview penerima berhasil dihitung');
}
public function storeBroadcast(StoreNotificationBroadcastRequest $request)
{
return $this->created($this->service->createBroadcast($request->validated(), $request->user(), $this->tenantId($request)), 'Pesan siaran masuk antrean');
}
public function showBroadcast(Request $request, NotificationBroadcast $notificationBroadcast)
{
$this->authorizeTenant($request, $notificationBroadcast->tenant_id);
return $this->success($notificationBroadcast->load(['template', 'channelConfig', 'recipients.customer:id,customer_code,name']), 'Detail siaran berhasil diambil');
}
public function approveBroadcast(Request $request, NotificationBroadcast $notificationBroadcast)
{
$this->authorizeTenant($request, $notificationBroadcast->tenant_id);
return $this->updated($this->service->approveBroadcast($notificationBroadcast, $request->user()), 'Pesan siaran berhasil disetujui');
}
public function myNotifications(IndexNotificationRequest $request)
{
return $this->success($this->service->userNotifications($request->user(), $request->validated()), 'Notifikasi aplikasi berhasil diambil');
}
public function markRead(Request $request, string $uuid)
{
$notification = $request->user()->userNotifications()->where('uuid', $uuid)->firstOrFail();
$notification->update(['read_at' => now()]);
return $this->updated($notification, 'Notifikasi ditandai sudah dibaca');
}
public function contacts(Request $request, Customer $customer)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->success($customer->contacts()->orderBy('type')->orderByDesc('is_primary')->get(), 'Kontak notifikasi berhasil diambil');
}
public function storeContact(StoreCustomerContactRequest $request, Customer $customer)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->created($this->service->saveContact($customer, $request->validated()), 'Kontak notifikasi berhasil dibuat');
}
public function updateContact(StoreCustomerContactRequest $request, Customer $customer, CustomerContact $contact)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->updated($this->service->saveContact($customer, $request->validated(), $contact), 'Kontak notifikasi berhasil diperbarui');
}
public function destroyContact(Request $request, Customer $customer, CustomerContact $contact)
{
$this->authorizeTenant($request, $customer->tenant_id);
abort_unless((int) $contact->customer_id === (int) $customer->id, 404);
$contact->delete();
return $this->deleted('Kontak notifikasi berhasil dihapus');
}
private function channel(Request $request): string
{
return $request->route()->defaults['notification_channel'];
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
private function authorizeTenant(Request $request, int $tenantId): void
{
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
}
}
@@ -0,0 +1,90 @@
<?php
namespace App\Http\Controllers\Notification;
use App\Http\Controllers\Controller;
use App\Models\NotificationChannel;
use App\Models\NotificationDelivery;
use App\Models\NotificationWebhookLog;
use Illuminate\Http\Request;
class NotificationWebhookController extends Controller
{
public function verify(Request $request, NotificationChannel $notificationChannel)
{
abort_unless(
$request->query('hub.mode') === 'subscribe'
&& hash_equals((string) data_get($notificationChannel->credentials, 'webhook_verify_token'), (string) $request->query('hub.verify_token')),
403,
);
return response($request->query('hub.challenge'), 200);
}
public function handle(Request $request, NotificationChannel $notificationChannel)
{
$valid = $this->validSignature($request, $notificationChannel);
$payload = $request->all();
$updates = $this->updates($payload);
$log = NotificationWebhookLog::create([
'notification_channel_id' => $notificationChannel->id,
'provider' => $notificationChannel->provider,
'event_type' => data_get($payload, 'entry.0.changes.0.field') ?? data_get($payload, 'event'),
'provider_message_id' => data_get($updates, '0.id'),
'signature_valid' => $valid,
'payload' => $payload,
]);
abort_unless($valid, 401);
foreach ($updates as $update) {
$delivery = NotificationDelivery::where('provider_message_id', $update['id'] ?? null)->first();
if (! $delivery) {
continue;
}
$status = match ($update['status'] ?? null) {
'delivered' => 'delivered',
'read' => 'read',
'failed' => 'failed',
default => 'sent',
};
$delivery->update([
'status' => $status,
'delivered_at' => $status === 'delivered' ? now() : $delivery->delivered_at,
'read_at' => $status === 'read' ? now() : $delivery->read_at,
'failed_at' => $status === 'failed' ? now() : $delivery->failed_at,
'error_code' => data_get($update, 'errors.0.code'),
'error_message' => data_get($update, 'errors.0.title'),
]);
}
$log->update(['processed_at' => now()]);
return response()->json(['success' => true]);
}
private function validSignature(Request $request, NotificationChannel $channel): bool
{
if ($channel->channel === 'whatsapp_official') {
$secret = data_get($channel->credentials, 'app_secret');
$signature = $request->header('X-Hub-Signature-256');
return $secret && $signature && hash_equals('sha256='.hash_hmac('sha256', $request->getContent(), $secret), $signature);
}
$secret = data_get($channel->credentials, 'webhook_secret');
return $secret && hash_equals((string) $secret, (string) $request->header('X-Webhook-Secret'));
}
private function updates(array $payload): array
{
$statuses = data_get($payload, 'entry.0.changes.0.value.statuses');
if (is_array($statuses)) {
return $statuses;
}
return [[
'id' => data_get($payload, 'message_id') ?? data_get($payload, 'id'),
'status' => data_get($payload, 'status') ?? data_get($payload, 'event'),
'errors' => data_get($payload, 'errors', []),
]];
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Payment;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Payment\IndexPaymentRequest;
use App\Http\Requests\Payment\SaveTenantPaymentSettingRequest;
use App\Http\Requests\Payment\StoreGatewayAccountRequest;
use App\Http\Resources\Payment\PaymentTransactionResource;
use App\Models\PaymentGatewayAccount;
use App\Services\Payment\PaymentGatewayService;
use Illuminate\Http\Request;
class PaymentGatewayController extends ApiController
{
public function __construct(protected PaymentGatewayService $service) {}
public function providers()
{
return $this->success($this->service->providers(), 'Daftar provider berhasil diambil');
}
public function settings(Request $request)
{
return $this->success($this->service->settings($request->user(), $this->tenantId($request)), 'Pengaturan payment gateway berhasil diambil');
}
public function storeAccount(StoreGatewayAccountRequest $request)
{
return $this->created($this->service->saveAccount($request->validated(), $request->user(), $this->tenantId($request)), 'Akun gateway berhasil dibuat');
}
public function updateAccount(StoreGatewayAccountRequest $request, PaymentGatewayAccount $account)
{
return $this->updated($this->service->saveAccount($request->validated(), $request->user(), $this->tenantId($request), $account), 'Akun gateway berhasil diperbarui');
}
public function saveSetting(SaveTenantPaymentSettingRequest $request)
{
return $this->updated($this->service->saveTenantSetting($request->validated(), $request->user(), $this->tenantId($request)), 'Mode pembayaran tenant berhasil disimpan');
}
public function transactions(IndexPaymentRequest $request)
{
return $this->successPaginated($this->service->transactions($request->validated(), $request->user(), $this->tenantId($request)), PaymentTransactionResource::class, 'Log pembayaran berhasil diambil');
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
}
@@ -0,0 +1,154 @@
<?php
namespace App\Http\Controllers\Topology;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Topology\IndexTopologyRequest;
use App\Http\Requests\Topology\StoreCustomerConnectionRequest;
use App\Http\Requests\Topology\StoreTopologyRequest;
use App\Http\Requests\Topology\UpdateTopologyRequest;
use App\Http\Resources\Topology\TopologyResource;
use App\Models\Customer;
use App\Models\TopologyNode;
use App\Models\TopologyPort;
use App\Services\Topology\TopologyService;
use Illuminate\Http\Request;
class TopologyController extends ApiController
{
public function __construct(protected TopologyService $service) {}
public function index(IndexTopologyRequest $request)
{
return $this->successPaginated(
$this->service->list($this->resource($request), $request->validated(), $request->user(), $this->tenantId($request)),
TopologyResource::class,
'Daftar topologi berhasil diambil',
);
}
public function store(StoreTopologyRequest $request)
{
return $this->created(
new TopologyResource($this->service->create($this->resource($request), $request->validated(), $request->user(), $this->tenantId($request))),
'Data topologi berhasil dibuat',
);
}
public function show(Request $request, int $id)
{
$model = $this->service->find($this->resource($request), $id);
$this->authorizeModel($request, $model);
return $this->success(new TopologyResource($model), 'Detail topologi berhasil diambil');
}
public function update(UpdateTopologyRequest $request, int $id)
{
$resource = $this->resource($request);
$model = $this->service->find($resource, $id);
$this->authorizeModel($request, $model, true);
return $this->updated(
new TopologyResource($this->service->update($resource, $model, $request->validated(), $request->user())),
'Data topologi berhasil diperbarui',
);
}
public function destroy(Request $request, int $id)
{
$resource = $this->resource($request);
$model = $this->service->find($resource, $id);
$this->authorizeModel($request, $model, true);
$this->service->delete($resource, $model);
return $this->deleted('Data topologi berhasil dihapus');
}
public function options(Request $request)
{
return $this->success($this->service->options($request->user(), $this->tenantId($request)), 'Pilihan topologi berhasil diambil');
}
public function map(IndexTopologyRequest $request)
{
return $this->success($this->service->map($request->validated(), $request->user(), $this->tenantId($request)), 'Data peta topologi berhasil diambil');
}
public function ports(Request $request, TopologyNode $node)
{
$this->authorizeTenant($request, $node->tenant_id);
return $this->success($this->service->ports($node), 'Daftar port berhasil diambil');
}
public function storePort(StoreTopologyRequest $request, TopologyNode $node)
{
$this->authorizeTenant($request, $node->tenant_id);
return $this->created($this->service->createPort($node, $request->validated()), 'Port berhasil dibuat');
}
public function updatePort(UpdateTopologyRequest $request, TopologyPort $port)
{
$this->authorizeTenant($request, $port->tenant_id);
return $this->updated($this->service->updatePort($port, $request->validated()), 'Port berhasil diperbarui');
}
public function destroyPort(Request $request, TopologyPort $port)
{
$this->authorizeTenant($request, $port->tenant_id);
$this->service->deletePort($port);
return $this->deleted('Port berhasil dihapus');
}
public function trace(Request $request, TopologyNode $node, string $direction)
{
abort_unless(in_array($direction, ['upstream', 'downstream'], true), 404);
$this->authorizeTenant($request, $node->tenant_id);
return $this->success($this->service->trace($node, $direction), 'Jalur topologi berhasil ditelusuri');
}
public function connectCustomer(StoreCustomerConnectionRequest $request)
{
return $this->created(
$this->service->connectCustomer($request->validated(), $request->user(), $this->tenantId($request)),
'Customer berhasil dihubungkan ke ODP',
);
}
public function customerPath(Request $request, Customer $customer)
{
$this->authorizeTenant($request, $customer->tenant_id);
return $this->success($this->service->customerPath($customer), 'Jalur customer berhasil ditelusuri');
}
private function resource(Request $request): string
{
return $request->route()->defaults['topology_resource'];
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
private function authorizeModel(Request $request, $model, bool $write = false): void
{
if ($model->tenant_id === null) {
abort_if($write && ! $request->user()->isMasterAdmin(), 403);
return;
}
$this->authorizeTenant($request, $model->tenant_id);
}
private function authorizeTenant(Request $request, int $tenantId): void
{
abort_unless($request->user()->isMasterAdmin() || $tenantId === $this->tenantId($request), 403);
}
}
@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Wallet;
use App\Http\Controllers\ApiController;
use App\Http\Requests\Wallet\StoreWithdrawalRequest;
use App\Http\Requests\Wallet\TransferWalletRequest;
use App\Models\Wallet;
use App\Services\Wallet\WalletService;
use Illuminate\Http\Request;
class WalletController extends ApiController
{
public function __construct(protected WalletService $service) {}
public function index(Request $request)
{
return $this->success($this->service->wallets($request->user(), $this->tenantId($request)), 'Daftar dompet berhasil diambil');
}
public function ledger(Request $request, Wallet $wallet)
{
return $this->success($this->service->ledger($wallet, $request->user(), $this->tenantId($request), $request->validate(['per_page' => ['nullable', 'integer', 'min:1', 'max:100']])), 'Mutasi dompet berhasil diambil');
}
public function transfer(TransferWalletRequest $request, Wallet $wallet)
{
return $this->created($this->service->transfer($wallet, Wallet::findOrFail($request->integer('destination_wallet_id')), $request->float('amount'), $request->user(), $this->tenantId($request)), 'Saldo berhasil dipindahkan');
}
public function withdraw(StoreWithdrawalRequest $request, Wallet $wallet)
{
return $this->created($this->service->requestWithdrawal($wallet, $request->validated(), $request->user(), $this->tenantId($request)), 'Permintaan penarikan berhasil dibuat');
}
private function tenantId(Request $request): ?int
{
return $request->attributes->get('tenant_id');
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexBillingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'string', 'max:30'],
'date_from' => ['nullable', 'date'],
'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'name', 'invoice_number', 'issue_date', 'isolation_date', 'total', 'balance', 'status', 'created_at', 'paid_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
class PayInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'amount' => ['required', 'numeric', 'gt:0'],
'payment_method' => ['nullable', 'string', 'max:50'],
'payment_reference' => ['nullable', 'string', 'max:255'],
'paid_at' => ['nullable', 'date'],
'notes' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreBillingProfileRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'name' => ['required', 'string', 'max:255'],
'schedule_type' => ['required', Rule::in(['fixed_date', 'installation_date'])],
'invoice_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28'],
'send_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28', 'gte:invoice_day'],
'warning_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28', 'gte:send_day'],
'isolation_day' => ['required_if:schedule_type,fixed_date', 'nullable', 'integer', 'between:1,28', 'gte:warning_day'],
'invoice_days_before' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90'],
'send_days_before' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90', 'lte:invoice_days_before'],
'warning_days_before' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90', 'lte:send_days_before'],
'isolation_days_after' => ['required_if:schedule_type,installation_date', 'nullable', 'integer', 'between:0,90'],
'tax_type' => ['required', Rule::in(['inclusive', 'exclusive', 'non_tax'])],
'tax_rate' => ['nullable', 'numeric', 'between:0,100'],
'status' => ['nullable', Rule::in(['active', 'inactive'])],
'notes' => ['nullable', 'string'],
];
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests\Billing;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreInvoiceRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'invoice_number' => ['nullable', 'string', 'max:100'],
'period_start' => ['required', 'date'],
'period_end' => ['required', 'date', 'after_or_equal:period_start'],
'issue_date' => ['required', 'date'],
'send_date' => ['nullable', 'date', 'after_or_equal:issue_date'],
'warning_date' => ['nullable', 'date', 'after_or_equal:send_date'],
'isolation_date' => ['nullable', 'date', 'after_or_equal:warning_date'],
'subtotal' => ['required', 'numeric', 'min:0'],
'notes' => ['nullable', 'string'],
'metadata' => ['nullable', 'array'],
'status' => ['nullable', Rule::in(['draft', 'published', 'sent', 'warning', 'overdue', 'paid', 'cancelled'])],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Billing;
class UpdateBillingProfileRequest extends StoreBillingProfileRequest
{
public function rules(): array
{
return collect(parent::rules())->map(function ($rules) {
$rules = is_array($rules) ? $rules : [$rules];
if (($index = array_search('required', $rules, true)) !== false) {
$rules[$index] = 'sometimes';
}
return $rules;
})->all();
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Billing;
class UpdateInvoiceRequest extends StoreInvoiceRequest
{
public function rules(): array
{
return collect(parent::rules())->map(function ($rules) {
$rules = is_array($rules) ? $rules : [$rules];
if (($index = array_search('required', $rules, true)) !== false) {
$rules[$index] = 'sometimes';
}
return $rules;
})->all();
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Customer;
use Illuminate\Foundation\Http\FormRequest;
class ActivateCustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'package_profile_id' => ['required', 'integer', 'exists:nas_package_profiles,id'],
'nas_mikrotik_id' => ['required', 'integer', 'exists:nas_mikrotiks,id'],
'billing_profile_id' => ['required', 'integer', 'exists:billing_profiles,id'],
'topology_id' => ['nullable', 'integer', 'min:1'],
'external_username' => ['nullable', 'string', 'max:255'],
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Customer;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexCustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'source' => ['nullable', Rule::in(['manual', 'mikrotik', 'radius'])],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'customer_code', 'name', 'email', 'whatsapp_number', 'status', 'order_stage', 'created_at', 'activated_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,48 @@
<?php
namespace App\Http\Requests\Customer;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreCustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'user_id' => ['nullable', 'integer', 'exists:users,id'],
'customer_code' => ['nullable', 'string', 'max:100'],
'name' => ['required', 'string', 'max:255'],
'identity_number' => ['nullable', 'string', 'max:100'],
'email' => ['nullable', 'email', 'max:255'],
'whatsapp_number' => ['nullable', 'string', 'max:50'],
'installation_address' => ['nullable', 'string'],
'provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
'kabupaten_id' => ['nullable', 'integer', 'exists:wilayah_kabupaten,id'],
'kecamatan_id' => ['nullable', 'integer', 'exists:wilayah_kecamatan,id'],
'desa_id' => ['nullable', 'integer', 'exists:wilayah_desa,id'],
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
'longitude' => ['nullable', 'numeric', 'between:-180,180'],
'package_profile_id' => ['nullable', 'integer', 'exists:nas_package_profiles,id'],
'nas_mikrotik_id' => ['nullable', 'integer', 'exists:nas_mikrotiks,id'],
'billing_profile_id' => ['nullable', 'integer', 'min:1'],
'topology_id' => ['nullable', 'integer', 'min:1'],
'external_username' => ['nullable', 'string', 'max:255'],
'source' => ['nullable', Rule::in(['manual', 'mikrotik', 'radius'])],
'order_stage' => ['nullable', Rule::in(['registration', 'data_completion', 'ready_activation', 'activation_failed'])],
'notes' => ['nullable', 'string'],
'images' => ['nullable', 'array', 'max:20'],
'images.*.stored_file_id' => ['required', 'integer', 'exists:stored_files,id'],
'images.*.category' => ['required', Rule::in(['house', 'installation', 'modem', 'odp', 'cable', 'identity', 'other'])],
'images.*.caption' => ['nullable', 'string', 'max:255'],
'images.*.is_cover' => ['nullable', 'boolean'],
'images.*.sort_order' => ['nullable', 'integer', 'min:0', 'max:1000'],
];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Http\Requests\Customer;
class UpdateCustomerRequest extends StoreCustomerRequest
{
public function rules(): array
{
$rules = parent::rules();
$rules['name'] = ['sometimes', 'required', 'string', 'max:255'];
return $rules;
}
}
+6 -2
View File
@@ -13,11 +13,13 @@ class StoreFileRequest extends FormRequest
public function rules(): array
{
$maxSize = $this->input('category') === 'customer-image' ? 500 : 20480;
return [
'file' => [
'required',
'file',
'max:20480',
"max:{$maxSize}",
'mimes:jpg,jpeg,png,webp,gif,pdf,doc,docx,xls,xlsx,csv,txt,zip',
],
'category' => ['nullable', 'string', 'max:80', 'regex:/^[a-z0-9_-]+$/'],
@@ -27,7 +29,9 @@ class StoreFileRequest extends FormRequest
public function messages(): array
{
return [
'file.max' => 'Ukuran file maksimal 20 MB.',
'file.max' => $this->input('category') === 'customer-image'
? 'Gambar customer maksimal 500 KB setelah kompresi.'
: 'Ukuran file maksimal 20 MB.',
'file.mimes' => 'Jenis file tidak didukung.',
'category.regex' => 'Kategori hanya boleh berisi huruf kecil, angka, garis bawah, atau tanda hubung.',
];
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
class IndexNotificationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'string', 'max:30'],
'unread' => ['nullable', 'boolean'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
];
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SaveNotificationPreferencesRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'preferences' => ['required', 'array'],
'preferences.*.tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'preferences.*.notification_event_id' => ['required', 'integer', 'exists:notification_events,id'],
'preferences.*.notification_channel_id' => ['nullable', 'integer', 'exists:notification_channels,id'],
'preferences.*.channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'preferences.*.enabled' => ['required', 'boolean'],
'preferences.*.priority' => ['nullable', 'integer', 'between:1,20'],
'preferences.*.send_delay_minutes' => ['nullable', 'integer', 'between:0,10080'],
'preferences.*.quiet_hours_start' => ['nullable', 'date_format:H:i'],
'preferences.*.quiet_hours_end' => ['nullable', 'date_format:H:i'],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreCustomerContactRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'type' => ['required', Rule::in(['email', 'whatsapp', 'telegram'])],
'value' => ['required', 'string', 'max:255'],
'label' => ['nullable', 'string', 'max:100'],
'is_primary' => ['nullable', 'boolean'],
'is_verified' => ['nullable', 'boolean'],
'can_receive_transactional' => ['nullable', 'boolean'],
'can_receive_broadcast' => ['nullable', 'boolean'],
'opted_out_at' => ['nullable', 'date'],
];
}
}
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNotificationBroadcastRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'name' => ['required_without:preview', 'string', 'max:255'],
'channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'notification_template_id' => ['nullable', 'integer', 'exists:notification_templates,id'],
'notification_channel_id' => ['nullable', 'integer', 'exists:notification_channels,id'],
'subject' => ['nullable', 'string', 'max:255'],
'body' => ['required_without:notification_template_id', 'nullable', 'string', 'max:20000'],
'scheduled_at' => ['nullable', 'date'],
'filters' => ['nullable', 'array'],
'filters.customer_status' => ['nullable', 'array'],
'filters.customer_status.*' => [Rule::in(['order', 'active', 'inactive', 'unmanaged'])],
'filters.source' => ['nullable', Rule::in(['manual', 'nas'])],
'filters.package_profile_id' => ['nullable', 'integer'],
'filters.billing_profile_id' => ['nullable', 'integer'],
'filters.topology_node_id' => ['nullable', 'integer', 'exists:topology_nodes,id'],
'filters.provinsi_id' => ['nullable', 'integer'],
'filters.kabupaten_id' => ['nullable', 'integer'],
'filters.kecamatan_id' => ['nullable', 'integer'],
'filters.desa_id' => ['nullable', 'integer'],
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNotificationChannelRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'provider' => ['nullable', 'string', 'max:50'],
'name' => ['required', 'string', 'max:255'],
'enabled' => ['nullable', 'boolean'],
'is_default' => ['nullable', 'boolean'],
'credentials' => ['nullable', 'array'],
'credentials.*' => ['nullable', 'string', 'max:4000'],
'settings' => ['nullable', 'array'],
];
}
}
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Requests\Notification;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreNotificationTemplateRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'notification_event_id' => ['required', 'integer', 'exists:notification_events,id'],
'notification_channel_id' => ['nullable', 'integer', 'exists:notification_channels,id'],
'channel' => ['required', Rule::in(['system', 'whatsapp_official', 'whatsapp_unofficial', 'email', 'telegram'])],
'name' => ['required', 'string', 'max:255'],
'subject' => ['nullable', 'string', 'max:255'],
'body' => ['required', 'string', 'max:20000'],
'provider_template_name' => ['nullable', 'string', 'max:255'],
'provider_template_id' => ['nullable', 'string', 'max:255'],
'language' => ['nullable', 'string', 'max:10'],
'approval_status' => ['nullable', Rule::in(['not_required', 'pending', 'approved', 'rejected'])],
'enabled' => ['nullable', 'boolean'],
];
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Requests\Payment;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexPaymentRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:100'], 'status' => ['nullable', 'string', 'max:30'],
'provider_code' => ['nullable', 'string', 'max:50'], 'purpose' => ['nullable', 'string', 'max:30'],
'date_from' => ['nullable', 'date'], 'date_to' => ['nullable', 'date', 'after_or_equal:date_from'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'transaction_number', 'amount', 'status', 'created_at', 'paid_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\Payment;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SaveTenantPaymentSettingRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'payment_mode' => ['required', Rule::in(['disabled', 'tenant_gateway', 'platform_gateway'])],
'tenant_gateway_account_id' => ['nullable', 'integer', 'exists:payment_gateway_accounts,id'],
'platform_gateway_account_id' => ['nullable', 'integer', 'exists:payment_gateway_accounts,id'],
'fee_bearer' => ['required', Rule::in(['customer', 'tenant', 'platform'])],
'default_expiry_minutes' => ['required', 'integer', 'min:5', 'max:10080'],
'enabled_methods' => ['nullable', 'array'],
'enabled_methods.*' => ['string', 'max:50'],
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\Payment;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreGatewayAccountRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'payment_provider_id' => ['required', 'integer', 'exists:payment_providers,id'],
'tenant_id' => ['nullable', 'integer', 'exists:tenants,id'],
'scope' => ['required', Rule::in(['tenant', 'platform'])],
'name' => ['required', 'string', 'max:150'],
'environment' => ['required', Rule::in(['sandbox', 'production'])],
'credentials' => ['nullable', 'array'],
'credentials.*' => ['nullable', 'string', 'max:4000'],
'webhook_secret' => ['nullable', 'string', 'max:4000'],
'settings' => ['nullable', 'array'],
'enabled' => ['sometimes', 'boolean'],
'is_default' => ['sometimes', 'boolean'],
];
}
}
@@ -0,0 +1,29 @@
<?php
namespace App\Http\Requests\Topology;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class IndexTopologyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'search' => ['nullable', 'string', 'max:255'],
'status' => ['nullable', 'string', 'max:30'],
'device_type_id' => ['nullable', 'integer', 'exists:topology_device_types,id'],
'link_type' => ['nullable', Rule::in(['fiber', 'wireless', 'ethernet', 'logical'])],
'bbox' => ['nullable', 'regex:/^-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?$/'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'sort_by' => ['nullable', Rule::in(['id', 'code', 'name', 'status', 'category', 'link_type', 'installed_at', 'created_at'])],
'sort_direction' => ['nullable', Rule::in(['asc', 'desc'])],
];
}
}
@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\Topology;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreCustomerConnectionRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'customer_id' => ['required', 'integer', 'exists:customers,id'],
'odp_node_id' => ['required', 'integer', 'exists:topology_nodes,id'],
'odp_port_id' => ['nullable', 'integer', 'exists:topology_ports,id'],
'drop_cable_link_id' => ['nullable', 'integer', 'exists:topology_links,id'],
'connected_at' => ['nullable', 'date'],
'status' => ['nullable', Rule::in(['active', 'inactive'])],
'metadata' => ['nullable', 'array'],
];
}
}
@@ -0,0 +1,89 @@
<?php
namespace App\Http\Requests\Topology;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreTopologyRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return match ($this->route()->defaults['topology_resource'] ?? null) {
'device-type' => [
...$this->tenantRule(),
'code' => ['required', 'string', 'max:50'],
'name' => ['required', 'string', 'max:255'],
'category' => ['required', Rule::in(['site', 'termination', 'closure', 'distribution', 'splitter', 'other'])],
'can_have_ports' => ['nullable', 'boolean'],
'can_have_splitters' => ['nullable', 'boolean'],
'icon' => ['nullable', 'string', 'max:100'],
'color' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'],
'status' => ['nullable', Rule::in(['active', 'inactive'])],
],
'node' => [
...$this->tenantRule(),
'device_type_id' => ['required', 'integer', 'exists:topology_device_types,id'],
'parent_node_id' => ['nullable', 'integer', 'exists:topology_nodes,id'],
'code' => ['required', 'string', 'max:100'],
'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'],
'status' => ['nullable', Rule::in(['active', 'inactive', 'maintenance', 'damaged'])],
'latitude' => ['required', 'numeric', 'between:-90,90'],
'longitude' => ['required', 'numeric', 'between:-180,180'],
'address' => ['nullable', 'string'],
'provinsi_id' => ['nullable', 'integer', 'exists:wilayah_provinsi,id'],
'kabupaten_id' => ['nullable', 'integer', 'exists:wilayah_kabupaten,id'],
'kecamatan_id' => ['nullable', 'integer', 'exists:wilayah_kecamatan,id'],
'desa_id' => ['nullable', 'integer', 'exists:wilayah_desa,id'],
'capacity' => ['nullable', 'integer', 'min:0'],
'installed_at' => ['nullable', 'date'],
'diagram_x' => ['nullable', 'numeric'],
'diagram_y' => ['nullable', 'numeric'],
'metadata' => ['nullable', 'array'],
],
'link' => [
...$this->tenantRule(),
'source_node_id' => ['required', 'integer', 'exists:topology_nodes,id', 'different:target_node_id'],
'source_port_id' => ['nullable', 'integer', 'exists:topology_ports,id'],
'target_node_id' => ['required', 'integer', 'exists:topology_nodes,id'],
'target_port_id' => ['nullable', 'integer', 'exists:topology_ports,id'],
'code' => ['required', 'string', 'max:100'],
'name' => ['required', 'string', 'max:255'],
'link_type' => ['required', Rule::in(['fiber', 'wireless', 'ethernet', 'logical'])],
'fiber_type' => ['nullable', 'string', 'max:50'],
'core_count' => ['nullable', 'integer', 'min:1'],
'cable_length' => ['nullable', 'numeric', 'min:0'],
'measured_length' => ['nullable', 'numeric', 'min:0'],
'path_geojson' => ['nullable', 'array'],
'path_geojson.type' => ['required_with:path_geojson', Rule::in(['LineString'])],
'path_geojson.coordinates' => ['required_with:path_geojson', 'array', 'min:2'],
'path_geojson.coordinates.*' => ['array', 'size:2'],
'path_geojson.coordinates.*.*' => ['numeric'],
'status' => ['nullable', Rule::in(['active', 'inactive', 'maintenance', 'damaged'])],
'installed_at' => ['nullable', 'date'],
'metadata' => ['nullable', 'array'],
],
'port' => [
'name' => ['required', 'string', 'max:100'],
'port_number' => ['required', 'integer', 'min:1'],
'port_type' => ['required', Rule::in(['fiber', 'ethernet', 'pon', 'uplink', 'splitter', 'other'])],
'direction' => ['required', Rule::in(['input', 'output', 'bidirectional'])],
'capacity' => ['nullable', 'integer', 'min:1'],
'status' => ['nullable', Rule::in(['available', 'used', 'reserved', 'damaged'])],
'metadata' => ['nullable', 'array'],
],
default => [],
};
}
private function tenantRule(): array
{
return ['tenant_id' => ['nullable', 'integer', 'exists:tenants,id']];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Topology;
class UpdateTopologyRequest extends StoreTopologyRequest
{
public function rules(): array
{
return collect(parent::rules())->map(function ($rules) {
$rules = is_array($rules) ? $rules : [$rules];
if (($index = array_search('required', $rules, true)) !== false) {
$rules[$index] = 'sometimes';
}
return $rules;
})->all();
}
}
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Requests\Wallet;
use Illuminate\Foundation\Http\FormRequest;
class StoreWithdrawalRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'amount' => ['required', 'numeric', 'min:10000'], 'fee' => ['nullable', 'numeric', 'min:0'],
'bank_code' => ['required', 'string', 'max:30'], 'account_number' => ['required', 'string', 'max:50'],
'account_name' => ['required', 'string', 'max:150'],
];
}
}
@@ -0,0 +1,18 @@
<?php
namespace App\Http\Requests\Wallet;
use Illuminate\Foundation\Http\FormRequest;
class TransferWalletRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return ['destination_wallet_id' => ['required', 'integer', 'exists:wallets,id'], 'amount' => ['required', 'numeric', 'min:1']];
}
}
@@ -0,0 +1,14 @@
<?php
namespace App\Http\Resources\Billing;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class BillingProfileResource extends JsonResource
{
public function toArray(Request $request): array
{
return [...parent::toArray($request), 'tenant' => $this->whenLoaded('tenant')];
}
}
@@ -0,0 +1,19 @@
<?php
namespace App\Http\Resources\Billing;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class InvoiceResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
...parent::toArray($request),
'tenant' => $this->whenLoaded('tenant'),
'customer' => $this->whenLoaded('customer'),
'billing_profile' => $this->whenLoaded('billingProfile'),
];
}
}
@@ -0,0 +1,50 @@
<?php
namespace App\Http\Resources\Customer;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class CustomerResource extends JsonResource
{
public function toArray(Request $request): array
{
$data = parent::toArray($request);
unset($data['images'], $data['cover_image']);
return [
...$data,
'tenant' => $this->whenLoaded('tenant'),
'provinsi' => $this->whenLoaded('provinsi'),
'kabupaten' => $this->whenLoaded('kabupaten'),
'kecamatan' => $this->whenLoaded('kecamatan'),
'desa' => $this->whenLoaded('desa'),
'package_profile' => $this->whenLoaded('packageProfile'),
'nas_mikrotik' => $this->whenLoaded('nasMikrotik'),
'billing_profile' => $this->whenLoaded('billingProfile'),
'images' => $this->whenLoaded('images', fn () => $this->images->map(fn ($image) => [
'id' => $image->id,
'stored_file_id' => $image->stored_file_id,
'uuid' => $image->file?->uuid,
'original_name' => $image->file?->original_name,
'category' => $image->category,
'caption' => $image->caption,
'is_cover' => $image->is_cover,
'sort_order' => $image->sort_order,
])->values()),
'cover_image' => $this->whenLoaded('coverImage', fn () => $this->coverImage ? [
'stored_file_id' => $this->coverImage->stored_file_id,
'uuid' => $this->coverImage->file?->uuid,
'original_name' => $this->coverImage->file?->original_name,
'size' => $this->coverImage->file?->size,
] : null),
'full_installation_address' => collect([
$this->installation_address,
$this->desa?->nama,
$this->kecamatan?->nama,
$this->kabupaten?->nama,
$this->provinsi?->nama,
])->filter()->implode(', '),
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources\Notification;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class NotificationDeliveryResource extends JsonResource
{
public function toArray(Request $request): array
{
$destination = (string) $this->destination;
$masked = strlen($destination) > 6
? substr($destination, 0, 3).str_repeat('*', max(3, strlen($destination) - 6)).substr($destination, -3)
: str_repeat('*', strlen($destination));
return [
...parent::toArray($request),
'destination' => $masked,
'message' => $this->whenLoaded('message'),
'channel_config' => $this->whenLoaded('channelConfig'),
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources\Payment;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PaymentTransactionResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id, 'uuid' => $this->uuid, 'transaction_number' => $this->transaction_number,
'tenant' => $this->whenLoaded('tenant'), 'user' => $this->whenLoaded('user'),
'customer' => $this->whenLoaded('customer'), 'invoice' => $this->whenLoaded('invoice'),
'provider' => $this->account?->provider, 'gateway_scope' => $this->gateway_scope,
'purpose' => $this->purpose, 'method' => $this->method, 'amount' => $this->amount,
'provider_fee' => $this->provider_fee, 'platform_fee' => $this->platform_fee,
'total_amount' => $this->total_amount, 'currency' => $this->currency, 'status' => $this->status,
'provider_status' => $this->provider_status, 'provider_reference' => $this->provider_reference,
'expires_at' => $this->expires_at, 'paid_at' => $this->paid_at, 'created_at' => $this->created_at,
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources\Topology;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class TopologyResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
...parent::toArray($request),
'tenant' => $this->whenLoaded('tenant'),
'device_type' => $this->whenLoaded('deviceType'),
'parent' => $this->whenLoaded('parent'),
'ports' => $this->whenLoaded('ports'),
'source_node' => $this->whenLoaded('sourceNode'),
'target_node' => $this->whenLoaded('targetNode'),
'source_port' => $this->whenLoaded('sourcePort'),
'target_port' => $this->whenLoaded('targetPort'),
];
}
}