9 Commits

27 changed files with 1976 additions and 16 deletions

View File

@@ -80,4 +80,9 @@ DUITKU_MERCHANT_CODE=
DUITKU_IS_SANDBOX=TRUE
WKHTML_PDF_BINARY=/var/www/aso/vendor/h4cc/wkhtmltopdf-amd64/bin/wkhtmltopdf-amd64
WKHTML_IMG_BINARY=
WKHTML_IMG_BINARY=
EMAIL = helpdesk@linksehat.com
PW_EMAIL = vsfcvwxcgldhhkdm
NAME_EMAIL = "LinkSehat"
PROVIDER_ONLINE_NOTIFICATION_EMAIL=helpdesk@linksehat.com

182
AGENTS.md Normal file
View File

@@ -0,0 +1,182 @@
# AGENTS.md
File ini berisi instruksi untuk AI coding agent yang bekerja di repository ini.
## Project Overview
Project name: ASO (Laravel + Modules)
Purpose: Platform operasional health insurance/managed care untuk pengelolaan member, corporate policy, claim, monitoring, livechat, dan pelaporan.
Main users: Tim internal operasional, portal client corporate, portal hospital.
Business domain: Healthtech / asuransi kesehatan / manajemen klaim dan layanan kesehatan.
## Tech Stack
Backend: PHP 8+, Laravel 9, `nwidart/laravel-modules`.
Frontend: React + TypeScript + Vite (utama di `frontend/dashboard` dan `frontend/client-portal`), plus aset root Laravel (`resources/`, Vite/Mix).
Database: MySQL/MariaDB (via Eloquent ORM + Laravel migrations).
Queue/background jobs: Laravel Queue (`jobs` table tersedia; default testing `sync`).
Cache: Laravel cache (driver mengikuti `.env`, testing pakai `array`).
Auth: Laravel Sanctum, Spatie Permission (`role`/`permission` middleware), sebagian flow JWT untuk integrasi tertentu.
Deployment: Build frontend ke `public/dashboard` dan `public/client-portal`; backend Laravel standar.
Package manager: Composer (PHP), npm/yarn/pnpm (frontend, mixed antar subproject).
## Local Setup
Install dependencies:
```bash
composer install
npm install
cd frontend/dashboard && yarn install
cd ../client-portal && yarn install
```
Run app:
```bash
php artisan serve
npm run dev
cd frontend/dashboard && yarn start
cd ../client-portal && yarn start
```
Run tests:
```bash
php artisan test
```
Run lint:
```bash
cd frontend/dashboard && yarn lint
cd ../client-portal && yarn lint
```
Run typecheck:
```bash
# Belum ada script typecheck standar di root/subproject.
# Jika diperlukan, gunakan tsc manual per frontend setelah konfirmasi tim.
```
Run build:
```bash
npm run production
cd frontend/dashboard && yarn build
cd ../client-portal && yarn build
```
Run migrations:
```bash
php artisan migrate
```
## Repository Structure
```text
app/ # Core Laravel app (models, services, middleware, controllers)
routes/ # Root web/api routes
Modules/ # Domain/module-based backend (Client, Internal, Linksehat, Primaya, HospitalPortal)
Modules/*/Routes/ # Route entry point tiap module
Modules/*/Http/Controllers/Api/
database/migrations/ # Root migrations
frontend/dashboard/ # React TS admin/internal dashboard
frontend/client-portal/ # React TS client portal
resources/ # Blade/views/assets untuk app Laravel utama
public/ # Public assets + output build frontend
tests/ # PHPUnit tests (Feature/Unit)
```
## Architecture Rules
- Tempatkan business logic di layer service/domain yang sudah ada (`app/Services`, `Modules/*/Services`) dan jaga controller tetap tipis.
- API layer mengikuti pemisahan module: endpoint module didefinisikan di `Modules/<ModuleName>/Routes/api.php` dengan controller module terkait.
- Untuk perubahan schema, gunakan migration baru; jangan ubah migration lama kecuali ada instruksi eksplisit.
- Untuk frontend, ikuti pattern existing per app (`src/pages`, `src/components`, `src/hooks`, `src/sections`) dan hindari cross-import antar app dashboard/client-portal.
- Testing backend utama menggunakan PHPUnit (`tests/Feature`, `tests/Unit`); tambah regression test untuk perubahan behavior penting.
## Coding Standards
- Ikuti pattern existing project sebelum membuat pattern baru.
- Buat perubahan kecil dan fokus.
- Jangan lakukan refactor yang tidak berhubungan.
- Jangan hardcode secrets.
- Jangan bypass validation.
- Jangan bypass auth/permission checks.
- Pilih code yang eksplisit dan mudah dibaca dibanding code yang terlalu clever.
- Tambahkan atau update test jika behavior berubah.
## Git Rules
- Jangan commit kecuali user secara eksplisit meminta.
- Jangan push kecuali user secara eksplisit meminta.
- Jangan rewrite git history kecuali user secara eksplisit meminta.
- Tampilkan file yang berubah di final response.
## Security Rules
- Jangan pernah print secrets.
- Jangan ubah nilai `.env` asli kecuali diminta secara eksplisit.
- Gunakan `.env-example`/`.env.example` untuk dokumentasi environment variables.
- Validasi auth, permissions, dan scoping corporate/member untuk protected data.
- Untuk endpoint API, pertahankan middleware auth yang ada (`auth:sanctum`, role/permission, middleware khusus module).
- Minta konfirmasi sebelum destructive data changes.
## Database Rules
- Gunakan migration system project ini.
- Jangan edit historical migrations kecuali project ini memang mengizinkan.
- Sertakan rollback/safety notes untuk schema changes.
- Pertimbangkan existing production data.
- Perhatikan migrasi dapat berasal dari root `database/migrations` dan modul tertentu.
## Testing Rules
Sebelum selesai, jalankan command yang relevan:
```bash
php artisan test
cd frontend/dashboard && yarn lint
cd frontend/client-portal && yarn lint
```
Jika command tidak bisa dijalankan, jelaskan alasannya dan command apa yang harus dijalankan manual.
## AI Agent Workflow
Untuk feature work:
1. Baca file ini.
2. Inspect pattern yang sudah ada.
3. Tulis plan singkat untuk pekerjaan non-trivial.
4. Tanya klarifikasi jika requirement kurang jelas.
5. Implement perubahan kecil dan fokus.
6. Jalankan test/lint/typecheck/build yang relevan.
7. Rangkum file yang berubah, command yang dijalankan, dan risiko.
Untuk debugging:
1. Reproduce atau inspect issue.
2. Identifikasi root cause sebelum mengubah code.
3. Buat fix minimal yang aman.
4. Tambahkan regression test jika memungkinkan.
5. Jalankan verification.
## Final Response Format
```text
Summary:
- ...
Files changed:
- ...
Commands run:
- ...
Risks / notes:
- ...
```

View File

@@ -135,4 +135,4 @@ Route::prefix('v1')->group(function() {
});
});
});

View File

@@ -13,4 +13,5 @@
Route::prefix('hospitalportal')->group(function() {
Route::get('/', 'HospitalPortalController@index');
Route::get('download-log/{id}', 'HospitalPortalController@downloadLog');
});

View File

@@ -0,0 +1,5 @@
<?php
return [
'name' => 'ProviderIntegrations',
];

View File

@@ -0,0 +1,723 @@
<?php
namespace Modules\ProviderIntegrations\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Models\Benefit;
use App\Models\CorporateBenefit;
use App\Models\Member;
use App\Models\MemberPlan;
use App\Models\Organization;
use App\Models\Plan;
use App\Models\Provider;
use App\Models\RequestLog;
use App\Models\RequestLogBenefit;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Modules\HospitalPortal\Http\Controllers\Api\RequestLogController;
use Throwable;
class ProviderOnlineController extends Controller
{
public function getHeaderKey(Request $request)
{
$request->validate([
'username' => 'required|string',
'password' => 'required|string',
'kodeprovider' => 'required|string',
]);
$organization = Organization::query()->where('code', $request->kodeprovider)->first();
if (!$organization) {
return $this->headerError('Kode provider tidak ditemukan');
}
$provider = Provider::query()->firstOrNew([
'organization_id' => $organization->id,
'username' => $request->username,
]);
if ($provider->exists) {
$isPasswordValid = Hash::check($request->password, $provider->password) || $provider->password === $request->password;
if (!$isPasswordValid) {
return $this->headerError('Username atau password tidak valid');
}
} else {
$provider->password = Hash::make($request->password);
}
$provider->code = $request->kodeprovider;
$provider->status = 'active';
$provider->header_token = Str::random(64);
$provider->token = Str::random(64);
$provider->save();
return response()->json([
'header-token' => $provider->header_token,
'userid' => $provider->id,
'usertoken' => $provider->token,
'kodeprovider' => $organization->code,
'namaprovider' => $organization->name,
'errornumber' => 0,
'messagestring' => 'Success',
]);
}
public function checkEligibilitasPeserta(Request $request)
{
$request->validate([
'nokartu' => 'required|string',
'kodeprovider' => 'required|string',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$member = Member::query()
->with(['person', 'currentCorporate', 'currentPolicy', 'memberPlans.plan'])
->where('member_id', $request->nokartu)
->first();
if (!$member) {
return $this->statusError('Peserta tidak ditemukan');
}
$benefits = $member->memberPlans
->map(function (MemberPlan $memberPlan) {
$plan = $memberPlan->plan;
if (!$plan || empty($plan->code)) {
return null;
}
return [
'kodebenefit' => $plan->code,
'namabenefit' => $plan->corporate_plan_id,
'planid' => $plan->code,
];
})
->filter()
->unique('kodebenefit')
->values();
return response()->json([
'Status' => $this->okStatus(),
'Data' => [
'nokartu' => $member->member_id,
'memberid' => (string) $member->id,
'namapeserta' => $member->full_name,
'nomorbpjs' => $member->bpjs_id,
'jeniskelamin' => $member->gender_code,
'tanggallahir' => $this->isoDate($member->birth_date),
'hubungankeluarga' => $member->relation_with_principal,
'namaperusahaan' => optional($member->currentCorporate)->name,
'pesertavip' => 'N',
'namapenjamin' => optional($organization)->name,
'nomorpolis' => optional($member->currentPolicy)->code,
'tglmulaipolis' => $this->isoDate(optional($member->currentPolicy)->start),
'tglberakhirpolis' => $this->isoDate(optional($member->currentPolicy)->end),
'phone' => optional($member->person)->phone,
'email' => $member->email,
],
'Benefit' => $benefits,
]);
}
public function createPendaftaran(Request $request)
{
$request->validate([
'kodeprovider' => 'required|string',
'kodebenefit' => 'required|string',
'statusrujukan' => 'nullable|string',
'nomorrujukan' => 'nullable|string',
'keterangan' => 'nullable|string',
'nomorsep' => 'nullable|string',
'nokartu' => 'required|string',
'kelaskamar' => 'nullable|string',
'cobbpjs' => 'nullable|numeric',
'notransaksiprovider' => 'nullable|string',
'inacbgscode' => 'nullable|string',
'inacbgsamount' => 'nullable|numeric',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$member = Member::query()
->with(['person', 'currentCorporate', 'currentPolicy'])
->where('member_id', $request->nokartu)
->first();
if (!$member) {
return $this->statusError('Peserta tidak ditemukan');
}
$plan = $this->resolvePlan($member, $request->kodebenefit);
$generatedLogCode = $this->generateNextRequestLogCode($organization, $member);
$requestLog = RequestLog::query()->create([
'code' => $generatedLogCode,
'organization_id' => $organization->id,
'member_id' => $member->id,
'plan_id' => optional($plan)->id,
'policy_id' => optional($member->currentPolicy)->id ?? 0,
'payment_type' => 'cashless',
'service_code' => $request->kodebenefit,
'type_of_member' => 'member',
'status' => 'approved',
'source' => 'api',
'keterangan' => $request->keterangan,
'hak_kamar_pasien' => $request->kelaskamar ?? '',
'penempatan_kamar' => $request->kelaskamar,
'total_cob' => $request->cobbpjs,
'nominal' => 0,
'import_system' => 0,
'nomor_sep' => $request->nomorsep,
'inacbgs_code' => $request->inacbgscode,
'inacbgs_amount' => $request->inacbgsamount,
'no_transaksi_provider' => $request->notransaksiprovider,
'diagnosis' => '',
'reason' => '',
'reason_final' => '',
'catatan' => '',
'nomor_rujukan' => $request->nomorrujukan,
'status_rujukan' => $request->statusrujukan,
'submission_date' => now(),
'admission_date' => now(),
]);
$limitSubBenefit = collect();
if ($plan) {
$limitSubBenefit = CorporateBenefit::query()
->with('benefit')
->where('plan_id', $plan->id)
->get()
->map(function (CorporateBenefit $corporateBenefit) {
return [
'kodesubbenefit' => optional($corporateBenefit->benefit)->code,
'namasubbenefit' => optional($corporateBenefit->benefit)->description,
'batasan' => (string) ($corporateBenefit->limit_amount ?? 0),
];
})
->filter(function (array $item) {
return !empty($item['kodesubbenefit']);
});
}
$this->sendProviderOnlineEmail('pendaftaran baru', $requestLog, $organization);
return response()->json([
'Status' => $this->okStatus(),
'Data' => [$this->buildClaimHeader($requestLog, $organization)],
'LimitSubBenefit' => $limitSubBenefit->values(),
]);
}
public function createPengesahan(Request $request)
{
$request->validate([
'noklaim' => 'required|string',
'kodeprovider' => 'required|string',
'tanggalkeluar' => 'nullable|date',
'kodediagnosa' => 'nullable|string',
'inacbgscode' => 'nullable|string',
'inacbgsamount' => 'nullable|numeric',
'daftarbiaya' => 'nullable|array',
'daftarbiaya.*.kodesubbenefit' => 'required_with:daftarbiaya|string',
'daftarbiaya.*.biayaaju' => 'required_with:daftarbiaya|numeric',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$requestLog = RequestLog::query()
->with(['member.person', 'member.currentCorporate', 'member.currentPolicy', 'plan'])
->where('code', $request->noklaim)
->where('organization_id', $organization->id)
->first();
if (!$requestLog) {
return $this->statusError('Nomor klaim tidak ditemukan');
}
$benefitsByCode = $this->resolveBenefitCodes($request->daftarbiaya ?? []);
if (isset($benefitsByCode['error'])) {
return $this->statusError($benefitsByCode['error']);
}
$requestLog->update([
'status_final_log' => 'approve',
'final_log' => true,
'discharge_date' => $request->tanggalkeluar,
'diagnosis' => $request->kodediagnosa,
'inacbgs_code' => $request->inacbgscode ?? $requestLog->inacbgs_code,
'inacbgs_amount' => $request->inacbgsamount ?? $requestLog->inacbgs_amount,
]);
foreach (($request->daftarbiaya ?? []) as $biaya) {
$benefit = $benefitsByCode[$biaya['kodesubbenefit']];
RequestLogBenefit::query()->updateOrCreate(
[
'request_log_id' => $requestLog->id,
'benefit_id' => $benefit->id,
],
[
'amount_incurred' => $biaya['biayaaju'],
'amount_approved' => 0,
'amount_not_approved' => 0,
'excess_paid' => 0,
'keterangan' => null,
]
);
}
$requestLog->load(['requestLogBenefits.benefit']);
$biayaResponse = $requestLog->requestLogBenefits->map(function (RequestLogBenefit $requestLogBenefit) use ($requestLog) {
return [
'noklaim' => $requestLog->code,
'kodesubbenefit' => optional($requestLogBenefit->benefit)->code,
'namasubbenefit' => optional($requestLogBenefit->benefit)->description,
'kodebenefit' => optional($requestLog->plan)->code,
'namabenefit' => optional($requestLog->plan)->corporate_plan_id,
'biayaaju' => (float) ($requestLogBenefit->amount_incurred ?? 0),
'jaminanasuransi' => (float) ($requestLogBenefit->amount_approved ?? 0),
'jaminanpeserta' => (float) ($requestLogBenefit->excess_paid ?? 0),
'keterangan' => $requestLogBenefit->keterangan,
];
})->values();
$this->sendProviderOnlineEmail('pengesahan', $requestLog, $organization);
return response()->json([
'Status' => $this->okStatus(),
'Data' => [$this->buildClaimHeader($requestLog, $organization)],
'Biaya' => $biayaResponse,
]);
}
public function upsertBillingSementara(Request $request)
{
$request->validate([
'noklaim' => 'required|string',
'kodeprovider' => 'required|string',
'tanggalkeluar' => 'nullable|date',
'kodediagnosa' => 'nullable|string',
'daftarbiaya' => 'nullable|array',
'daftarbiaya.*.kodesubbenefit' => 'required_with:daftarbiaya|string',
'daftarbiaya.*.biayaaju' => 'required_with:daftarbiaya|numeric',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$requestLog = RequestLog::query()
->where('code', $request->noklaim)
->where('organization_id', $organization->id)
->first();
if (!$requestLog) {
return $this->statusError('Nomor klaim tidak ditemukan');
}
$benefitsByCode = $this->resolveBenefitCodes($request->daftarbiaya ?? []);
if (isset($benefitsByCode['error'])) {
return $this->statusError($benefitsByCode['error']);
}
$requestLog->update([
'discharge_date' => $request->tanggalkeluar,
'diagnosis' => $request->kodediagnosa,
]);
foreach (($request->daftarbiaya ?? []) as $biaya) {
$benefit = $benefitsByCode[$biaya['kodesubbenefit']];
RequestLogBenefit::query()->updateOrCreate(
[
'request_log_id' => $requestLog->id,
'benefit_id' => $benefit->id,
],
[
'amount_incurred' => $biaya['biayaaju'],
'amount_approved' => 0,
'amount_not_approved' => 0,
'excess_paid' => 0,
'keterangan' => null,
]
);
}
$this->sendProviderOnlineEmail('upsert billing sementara', $requestLog, $organization);
return response()->json([
'Status' => $this->okStatus(),
]);
}
public function getRincianBiayaKlaim(Request $request)
{
$request->validate([
'noklaim' => 'required|string',
'kodeprovider' => 'required|string',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$requestLog = RequestLog::query()
->with(['member.person', 'member.currentCorporate', 'member.currentPolicy', 'plan', 'requestLogBenefits.benefit'])
->where('code', $request->noklaim)
->where('organization_id', $organization->id)
->first();
if (!$requestLog) {
return $this->statusError('Nomor klaim tidak ditemukan');
}
$biaya = $requestLog->requestLogBenefits->map(function (RequestLogBenefit $requestLogBenefit) use ($requestLog) {
return [
'noklaim' => $requestLog->code,
'kodesubbenefit' => optional($requestLogBenefit->benefit)->code,
'namasubbenefit' => optional($requestLogBenefit->benefit)->description,
'kodebenefit' => optional($requestLog->plan)->code,
'namabenefit' => optional($requestLog->plan)->corporate_plan_id,
'biayaaju' => (float) ($requestLogBenefit->amount_incurred ?? 0),
'jaminanasuransi' => (float) ($requestLogBenefit->amount_approved ?? 0),
'jaminanpeserta' => (float) ($requestLogBenefit->excess_paid ?? 0),
'keterangan' => $requestLogBenefit->keterangan,
];
})->values();
return response()->json([
'Status' => $this->okStatus(),
'Data' => [$this->buildClaimHeader($requestLog, $organization)],
'Biaya' => $biaya,
]);
}
public function downloadStrukPendaftaran(Request $request)
{
$request->validate([
'noklaim' => 'required|string',
'kodeprovider' => 'required|string',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$requestLog = RequestLog::query()
->where('code', $request->noklaim)
->where('organization_id', $organization->id)
->first();
if (!$requestLog) {
return $this->statusError('Nomor klaim tidak ditemukan');
}
try {
return (new RequestLogController())->downlodLog($requestLog->id);
} catch (Throwable $e) {
$fallbackUrl = url('api/v1/hospitalportal/download-log/' . $requestLog->id);
return $this->statusError(
'Gagal generate PDF struk pendaftaran: ' . $e->getMessage() . '. request_log_id=' . $requestLog->id . ', fallback_url=' . $fallbackUrl,
500
);
}
}
public function downloadStrukPengesahan(Request $request)
{
$request->validate([
'noklaim' => 'required|string',
'kodeprovider' => 'required|string',
'p_user_no' => 'required',
'p_token' => 'required|string',
]);
[, $organization, $authError] = $this->resolveProvider($request->kodeprovider, $request->p_user_no, $request->p_token);
if ($authError) {
return $authError;
}
$requestLog = RequestLog::query()
->where('code', $request->noklaim)
->where('organization_id', $organization->id)
->first();
if (!$requestLog) {
return $this->statusError('Nomor klaim tidak ditemukan');
}
try {
return (new RequestLogController())->downlodFinalLog($requestLog->id);
} catch (Throwable $e) {
$fallbackUrl = url('api/v1/hospitalportal/download-final-log/' . $requestLog->id);
return $this->statusError(
'Gagal generate PDF struk pengesahan: ' . $e->getMessage() . '. request_log_id=' . $requestLog->id . ', fallback_url=' . $fallbackUrl,
500
);
}
}
private function resolveProvider(string $kodeProvider, $userId, string $token): array
{
$organization = Organization::query()->where('code', $kodeProvider)->first();
if (!$organization) {
return [null, null, $this->statusError('Kode provider tidak ditemukan')];
}
$provider = Provider::query()
->where('id', $userId)
->where('organization_id', $organization->id)
->where('code', $kodeProvider)
->where('status', 'active')
->first();
if (!$provider) {
return [null, null, $this->statusError('User provider tidak ditemukan')];
}
if ((string) $provider->token !== (string) $token) {
return [null, null, $this->statusError('Token tidak valid')];
}
return [$provider, $organization, null];
}
private function resolvePlan(Member $member, string $serviceCode): ?Plan
{
$memberPlan = $member->memberPlans()
->with('plan')
->where('status', 'active')
->orderByDesc('id')
->get()
->first(function (MemberPlan $item) use ($serviceCode) {
return optional($item->plan)->service_code === $serviceCode;
});
if ($memberPlan && $memberPlan->plan) {
return $memberPlan->plan;
}
return optional($member->memberPlans()->with('plan')->orderByDesc('id')->first())->plan;
}
private function resolveBenefitCodes(array $daftarBiaya): array
{
$codes = collect($daftarBiaya)
->pluck('kodesubbenefit')
->filter()
->unique()
->values();
if ($codes->isEmpty()) {
return [];
}
$benefits = Benefit::query()
->whereIn('code', $codes)
->get()
->keyBy('code');
$missing = $codes->filter(function (string $code) use ($benefits) {
return !$benefits->has($code);
});
if ($missing->isNotEmpty()) {
return [
'error' => 'Kode sub benefit tidak valid: ' . $missing->implode(', '),
];
}
return $benefits->all();
}
private function buildClaimHeader(RequestLog $requestLog, Organization $organization): array
{
$requestLog->loadMissing(['member.currentCorporate', 'member.currentPolicy', 'plan']);
return [
'noklaim' => $requestLog->code,
'namapeserta' => optional($requestLog->member)->full_name,
'tanggallahir' => $this->isoDate(optional($requestLog->member)->birth_date),
'nokartu' => optional($requestLog->member)->member_id,
'nopolis' => optional(optional($requestLog->member)->currentPolicy)->code,
'nobpjs' => optional($requestLog->member)->bpjs_id,
'nosep' => $requestLog->nomor_sep,
'nomorrujukan' => $requestLog->nomor_rujukan,
'planid' => optional($requestLog->plan)->code,
'masapolis' => optional(optional($requestLog->member)->currentPolicy)->end
? Carbon::parse($requestLog->member->currentPolicy->end)->format('d/m/Y')
: null,
'namaperusahaan' => optional(optional($requestLog->member)->currentCorporate)->name,
'namapenjamin' => $organization->name,
'tanggalmasuk' => $this->isoDate($requestLog->admission_date),
'tanggalkeluar' => $this->isoDate($requestLog->discharge_date),
'hakkamar' => $requestLog->penempatan_kamar,
'hakicu' => null,
'nosuratjaminan' => $requestLog->code,
'namapegawai' => null,
'namabenefit' => optional($requestLog->plan)->corporate_plan_id,
'kodediagnosa' => $requestLog->diagnosis,
'keterangan' => $requestLog->keterangan,
'catatanTC1' => null,
'catatanTC2' => null,
'catatanTC3' => null,
'catatanTC4' => null,
'catatanTC5' => null,
'catatanTC6' => null,
'catatanTC7' => null,
'catatanTC8' => null,
'catatanTC9' => null,
'catatanTC10' => null,
'statusrujukan' => $requestLog->status_rujukan,
'statusklaim' => 0,
'notransaksiprovider' => $requestLog->no_transaksi_provider,
'inacbgscode' => $requestLog->inacbgs_code,
'inacbgsamount' => (float) ($requestLog->inacbgs_amount ?? 0),
];
}
private function sendProviderOnlineEmail(string $eventName, RequestLog $requestLog, Organization $organization): void
{
$email = trim((string) ($organization->email ?: env('PROVIDER_ONLINE_NOTIFICATION_EMAIL', 'helpdesk@linksehat.com')));
if (!$email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return;
}
$name = $organization->name ?: 'Provider';
$subject = sprintf('[ProviderOnline] %s - %s', ucfirst($eventName), $requestLog->code);
$body = sprintf(
'<p>Halo %s,</p><p>Event <strong>%s</strong> berhasil diproses untuk nomor klaim <strong>%s</strong>.</p><p>Waktu: %s</p>',
e($name),
e($eventName),
e($requestLog->code),
now()->format('Y-m-d H:i:s')
);
try {
Helper::sendEmail([
'email' => $email,
'name' => $name,
'subject' => $subject,
'body' => $body,
]);
} catch (Throwable $throwable) {
Log::warning('ProviderOnline notification email failed', [
'event' => $eventName,
'request_log_id' => $requestLog->id,
'request_log_code' => $requestLog->code,
'organization_id' => $organization->id,
'error' => $throwable->getMessage(),
]);
}
}
private function okStatus(): array
{
return [
'errornumber' => 0,
'messagestring' => 'Success',
];
}
private function statusError(string $message, int $statusCode = 400)
{
return response()->json([
'Status' => [
'errornumber' => 1,
'messagestring' => $message,
],
'Data' => null,
], $statusCode);
}
private function headerError(string $message, int $statusCode = 400)
{
return response()->json([
'header-token' => null,
'userid' => 0,
'usertoken' => null,
'kodeprovider' => null,
'namaprovider' => null,
'errornumber' => 1,
'messagestring' => $message,
], $statusCode);
}
private function isoDate($date): ?string
{
if (empty($date)) {
return null;
}
return Carbon::parse($date)->toISOString();
}
private function generateNextRequestLogCode(Organization $organization, Member $member): string
{
$data = [
'source' => 'H',
'provideCode' => $organization->code ?? '',
'date' => date('ymd'),
'policy' => optional($member->currentPolicy)->code ?? '-',
'member_code' => $member->member_id ?? '-',
];
$lastNumericCode = RequestLog::query()
->select(DB::raw('MAX(CAST(SUBSTRING_INDEX(code, ".", -1) AS SIGNED)) as max_numeric_code'))
->whereRaw('SUBSTRING_INDEX(code, ".", -1) REGEXP "^[0-9]+$"')
->value('max_numeric_code');
$nextNumber = $lastNumericCode ? ((int) $lastNumericCode + 1) : 1;
return $this->makeRequestLogCode($nextNumber, $data);
}
private function makeRequestLogCode(int $nextNumber, array $data): string
{
$nextNumber = max(1, $nextNumber);
return implode('.', [
'LOG',
$data['source'],
$data['provideCode'],
$data['date'],
$data['policy'],
$data['member_code'],
str_pad((string) $nextNumber, 5, '0', STR_PAD_LEFT),
]);
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Modules\ProviderIntegrations\Providers;
use Illuminate\Support\ServiceProvider;
class ProviderIntegrationsServiceProvider extends ServiceProvider
{
protected $moduleName = 'ProviderIntegrations';
protected $moduleNameLower = 'providerintegrations';
public function boot()
{
$this->registerTranslations();
$this->registerConfig();
$this->registerViews();
$this->loadMigrationsFrom(module_path($this->moduleName, 'Database/Migrations'));
}
public function register()
{
$this->app->register(RouteServiceProvider::class);
}
protected function registerConfig()
{
$this->publishes([
module_path($this->moduleName, 'Config/config.php') => config_path($this->moduleNameLower . '.php'),
], 'config');
$this->mergeConfigFrom(
module_path($this->moduleName, 'Config/config.php'),
$this->moduleNameLower
);
}
public function registerViews()
{
$viewPath = resource_path('views/modules/' . $this->moduleNameLower);
$sourcePath = module_path($this->moduleName, 'Resources/views');
$this->publishes([
$sourcePath => $viewPath,
], ['views', $this->moduleNameLower . '-module-views']);
$this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->moduleNameLower);
}
public function registerTranslations()
{
$langPath = resource_path('lang/modules/' . $this->moduleNameLower);
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, $this->moduleNameLower);
$this->loadJsonTranslationsFrom($langPath, $this->moduleNameLower);
return;
}
$this->loadTranslationsFrom(module_path($this->moduleName, 'Resources/lang'), $this->moduleNameLower);
$this->loadJsonTranslationsFrom(module_path($this->moduleName, 'Resources/lang'), $this->moduleNameLower);
}
public function provides()
{
return [];
}
private function getPublishableViewPaths(): array
{
$paths = [];
foreach (config('view.paths') as $path) {
if (is_dir($path . '/modules/' . $this->moduleNameLower)) {
$paths[] = $path . '/modules/' . $this->moduleNameLower;
}
}
return $paths;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Modules\ProviderIntegrations\Providers;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
protected $moduleNamespace = 'Modules\\ProviderIntegrations\\Http\\Controllers';
public function boot()
{
parent::boot();
}
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->moduleNamespace)
->group(module_path('ProviderIntegrations', '/Routes/web.php'));
}
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->moduleNamespace)
->group(module_path('ProviderIntegrations', '/Routes/api.php'));
}
}

View File

@@ -0,0 +1,15 @@
<?php
use Modules\ProviderIntegrations\Http\Controllers\Api\ProviderOnlineController;
Route::prefix('v1/bridging-service/ProviderOnline')->group(function () {
Route::post('AddHeaderKey', [ProviderOnlineController::class, 'getHeaderKey']);
Route::post('HeaderKey', [ProviderOnlineController::class, 'getHeaderKey']);
Route::post('EligibilitasPeserta', [ProviderOnlineController::class, 'checkEligibilitasPeserta']);
Route::post('Pendaftaran', [ProviderOnlineController::class, 'createPendaftaran']);
Route::post('Pengesahan', [ProviderOnlineController::class, 'createPengesahan']);
Route::post('BillingSementara', [ProviderOnlineController::class, 'upsertBillingSementara']);
Route::post('RincianBiayaKlaim', [ProviderOnlineController::class, 'getRincianBiayaKlaim']);
Route::post('StrukPendaftaran', [ProviderOnlineController::class, 'downloadStrukPendaftaran']);
Route::post('StrukPengesahan', [ProviderOnlineController::class, 'downloadStrukPengesahan']);
});

View File

@@ -0,0 +1,7 @@
<?php
Route::prefix('providerintegrations')->group(function () {
Route::get('/', function () {
return response()->json(['module' => 'ProviderIntegrations']);
});
});

View File

@@ -0,0 +1,21 @@
{
"name": "nwidart/providerintegrations",
"description": "",
"authors": [
{
"name": "Nicolas Widart",
"email": "n.widart@gmail.com"
}
],
"extra": {
"laravel": {
"providers": [],
"aliases": {}
}
},
"autoload": {
"psr-4": {
"Modules\\ProviderIntegrations\\": ""
}
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "ProviderIntegrations",
"alias": "providerintegrations",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\ProviderIntegrations\\Providers\\ProviderIntegrationsServiceProvider"
],
"files": []
}

32
app/Models/Provider.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Provider extends Model
{
use HasFactory;
protected $table = 'providers';
protected $fillable = [
'organization_id',
'username',
'password',
'code',
'status',
'header_token',
'token',
];
protected $hidden = [
'password',
];
public function organization()
{
return $this->belongsTo(Organization::class, 'organization_id', 'id');
}
}

View File

@@ -23,8 +23,10 @@ class RequestLog extends Model
'invoice_no',
'billing_no',
'submission_date',
'admission_date',
'discharge_date',
'member_id',
'plan_id',
'payment_type',
'service_code',
'type_of_member',
@@ -38,6 +40,12 @@ class RequestLog extends Model
'claim_id',
'organization_id',
'keterangan',
'nomor_sep',
'status_rujukan',
'nomor_rujukan',
'no_transaksi_provider',
'inacbgs_code',
'inacbgs_amount',
'hak_kamar_pasien',
'penempatan_kamar',
'catatan',
@@ -272,6 +280,11 @@ class RequestLog extends Model
return $this->belongsTo(Service::class, 'service_code', 'code');
}
public function plan()
{
return $this->belongsTo(Plan::class, 'plan_id');
}
public function requestLogBenefits()
{
return $this->hasMany(RequestLogBenefit::class, 'request_log_id');

View File

@@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('providers', function (Blueprint $table) {
$table->id();
$table->foreignId('organization_id')->constrained('organizations')->cascadeOnDelete();
$table->string('username');
$table->string('password');
$table->string('code')->nullable();
$table->string('status')->default('active');
$table->string('header_token', 255)->nullable();
$table->string('token', 255)->nullable();
$table->timestamps();
$table->unique(['organization_id', 'username']);
$table->index(['organization_id', 'code', 'status']);
$table->index('token');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('providers');
}
};

View File

@@ -0,0 +1,94 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('request_logs', function (Blueprint $table) {
if (!Schema::hasColumn('request_logs', 'plan_id')) {
$table->unsignedBigInteger('plan_id')->nullable()->after('member_id');
$table->index('plan_id');
}
if (!Schema::hasColumn('request_logs', 'admission_date')) {
$table->dateTime('admission_date')->nullable()->after('submission_date');
}
if (!Schema::hasColumn('request_logs', 'nomor_sep')) {
$table->string('nomor_sep')->nullable()->after('keterangan');
}
if (!Schema::hasColumn('request_logs', 'status_rujukan')) {
$table->string('status_rujukan')->nullable()->after('nomor_sep');
}
if (!Schema::hasColumn('request_logs', 'nomor_rujukan')) {
$table->string('nomor_rujukan')->nullable()->after('status_rujukan');
}
if (!Schema::hasColumn('request_logs', 'no_transaksi_provider')) {
$table->string('no_transaksi_provider')->nullable()->after('nomor_rujukan');
}
if (!Schema::hasColumn('request_logs', 'inacbgs_code')) {
$table->string('inacbgs_code')->nullable()->after('no_transaksi_provider');
}
if (!Schema::hasColumn('request_logs', 'inacbgs_amount')) {
$table->decimal('inacbgs_amount', 18, 2)->nullable()->after('inacbgs_code');
}
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('request_logs', function (Blueprint $table) {
if (Schema::hasColumn('request_logs', 'inacbgs_amount')) {
$table->dropColumn('inacbgs_amount');
}
if (Schema::hasColumn('request_logs', 'inacbgs_code')) {
$table->dropColumn('inacbgs_code');
}
if (Schema::hasColumn('request_logs', 'no_transaksi_provider')) {
$table->dropColumn('no_transaksi_provider');
}
if (Schema::hasColumn('request_logs', 'nomor_rujukan')) {
$table->dropColumn('nomor_rujukan');
}
if (Schema::hasColumn('request_logs', 'status_rujukan')) {
$table->dropColumn('status_rujukan');
}
if (Schema::hasColumn('request_logs', 'nomor_sep')) {
$table->dropColumn('nomor_sep');
}
if (Schema::hasColumn('request_logs', 'admission_date')) {
$table->dropColumn('admission_date');
}
if (Schema::hasColumn('request_logs', 'plan_id')) {
$table->dropIndex(['plan_id']);
$table->dropColumn('plan_id');
}
});
}
};

242
docs/ai-agent-guide.id.md Normal file
View File

@@ -0,0 +1,242 @@
# Panduan Penggunaan AI Agent (ASO)
Panduan ini menjelaskan cara tim ASO menyiapkan dan memakai AI coding agent secara aman di repository ini.
Tujuan utamanya adalah memberi AI konteks project, command yang benar, dan batasan safety agar AI membantu lebih cepat tanpa merusak codebase.
## Prinsip Utama
1. Human tetap pemilik keputusan final.
2. AI wajib mengikuti konvensi project ASO.
3. AI tidak boleh commit/push kecuali diminta eksplisit.
4. AI harus membuat rencana singkat sebelum perubahan besar/berisiko.
5. AI harus menjalankan verifikasi relevan sebelum menyatakan selesai.
6. Human wajib review area kritikal: security, auth/permission, scoping corporate/member, migration, payments, data deletion, production config.
7. Repository ini wajib memiliki `AGENTS.md` di root.
## Konteks Project Ini
ASO adalah platform operasional health insurance/managed care untuk pengelolaan corporate, member, claim, monitoring, livechat, dan pelaporan.
Stack utama saat ini:
- Backend: Laravel 9 (`app/`) + modular backend `nwidart/laravel-modules` (`Modules/`)
- Frontend: `frontend/dashboard` dan `frontend/client-portal` (React + TS + Vite), plus assets root Laravel (`resources/`, Vite/Mix)
- Auth/Permission: Sanctum + Spatie Permission (`role`, `permission`) + beberapa flow JWT untuk integrasi tertentu
- Testing backend: PHPUnit (`tests/Feature`, `tests/Unit`)
## AI Agent Ready untuk Repo ASO
Repository ini dianggap AI Agent Ready jika AI bisa cepat menjawab:
- Struktur backend modular dan batas tiap module
- Command setup/run/test/lint/build yang benar untuk root + dua frontend app
- Aturan peletakan business logic
- Aturan auth/permission/scoping corporate-member pada endpoint terlindungi
- Aturan migration aman
- Aturan git (tidak commit/push tanpa instruksi)
## File Minimum yang Harus Ada
```text
.
├── AGENTS.md
├── README.md
├── .env-example (dan/atau .env.example)
└── docs/
└── ai-agent-guide.id.md
```
Disarankan ditambah:
```text
docs/architecture.md
docs/testing.md
docs/database.md
docs/api-contracts.md
```
## Command Operasional (Aktual Repo Ini)
Install dependencies:
```bash
composer install
npm install
cd frontend/dashboard && yarn install
cd ../client-portal && yarn install
```
Menjalankan aplikasi lokal:
```bash
php artisan serve
npm run dev
cd frontend/dashboard && yarn start
cd ../client-portal && yarn start
```
Testing backend:
```bash
php artisan test
```
Lint frontend:
```bash
cd frontend/dashboard && yarn lint
cd ../client-portal && yarn lint
```
Build frontend:
```bash
npm run production
cd frontend/dashboard && yarn build
cd ../client-portal && yarn build
```
Migration:
```bash
php artisan migrate
```
Catatan:
- Script `typecheck` standar belum tersedia konsisten di root/subproject; jika perlu, jalankan `tsc` manual sesuai kebutuhan task.
- Repo ini memiliki lockfile campuran (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`). Ikuti lockfile dan package manager yang sudah dipakai pada subproject yang disentuh.
## Struktur Repository Penting
```text
app/ # Core Laravel app
routes/ # Root routes (web/api)
Modules/ # Module domain (Client, Internal, Linksehat, Primaya, HospitalPortal)
Modules/*/Routes/api.php # API route per module
database/migrations/ # Root migrations
frontend/dashboard/ # Internal/admin frontend
frontend/client-portal/ # Client portal frontend
resources/ # View/assets Laravel utama
public/ # Public assets + output build frontend
tests/ # PHPUnit tests
```
## Aturan Arsitektur untuk Agent
- Gunakan pattern yang sudah ada sebelum menambah pattern baru.
- Jaga controller tetap tipis; letakkan logic di service/domain yang sudah ada (`app/Services`, `Modules/*/Services`).
- Untuk API module, tambahkan route dan controller dalam module yang sesuai; hindari menaruh logic module di tempat acak.
- Jangan cross-import antar frontend app (`dashboard` vs `client-portal`) kecuali memang sudah ada kontrak eksplisit.
- Jika mengubah perilaku endpoint protected, review middleware terkait (`auth:sanctum`, role/permission, middleware custom module).
## Workflow AI Harian
### 1. Mulai Task
- Baca `AGENTS.md`.
- Pahami pattern existing di area yang akan diubah.
- Buat plan singkat untuk task non-trivial.
- Klarifikasi requirement jika masih ambigu.
### 2. Feature Work
- Implement perubahan kecil dan fokus.
- Hindari refactor unrelated.
- Tambah/update test saat behavior berubah.
- Jalankan verifikasi relevan.
### 3. Bug Fixing
- Reproduce/inspeksi issue terlebih dahulu.
- Temukan root cause.
- Terapkan fix minimal dan aman.
- Tambah regression test jika memungkinkan.
### 4. Review Sebelum PR
Periksa:
- correctness
- security
- auth/permission/scoping
- migration safety
- missing tests
- lint/type/build impact
- perubahan unrelated
## Definition of Done untuk Task AI
Task AI belum selesai sampai:
- Perubahan mengikuti pattern existing.
- Tidak ada perubahan unrelated.
- Test/lint/build relevan sudah dijalankan atau alasannya dijelaskan.
- Risiko migration/auth/scoping dijelaskan jika relevan.
- File berubah dan command yang dijalankan dicatat.
## Safety Rules (Wajib)
AI agent tidak boleh:
- Commit/push/rewrite history tanpa instruksi eksplisit.
- Mengubah `.env` asli tanpa instruksi eksplisit.
- Print secrets ke output/chat.
- Menghapus data/folder besar tanpa konfirmasi.
- Mengubah schema DB tanpa migration baru dan catatan safety.
- Menambah dependency besar tanpa alasan jelas dan dampak.
## Prompt Siap Pakai untuk Repo Ini
### Setup / Sinkronisasi Dokumentasi
```text
Baca AGENTS.md dan docs/ai-agent-guide.id.md.
Sinkronkan dokumentasi AI agent dengan kondisi codebase saat ini.
Validasi command setup/test/lint/build berdasarkan file konfigurasi aktual.
Jangan commit perubahan.
```
### Feature Work
```text
Implement feature berikut: [jelaskan].
Rules:
- Baca AGENTS.md terlebih dahulu.
- Ikuti pattern existing repo ASO.
- Jangan commit.
- Jalankan verifikasi relevan.
Output akhir wajib: Summary, Files changed, Commands run, Risks/notes.
```
### Bug Fixing
```text
Fix bug berikut: [jelaskan].
Gunakan systematic debugging:
1. Reproduce/inspeksi issue
2. Temukan root cause
3. Terapkan fix minimal aman
4. Tambah regression test jika memungkinkan
5. Jalankan verifikasi
Jangan commit.
```
## Format Final Response Standar
Gunakan format ini:
```text
Summary:
- ...
Files changed:
- ...
Commands run:
- ...
Risks / notes:
- ...
```

242
docs/ai-agent-guide.md Normal file
View File

@@ -0,0 +1,242 @@
# AI Agent Usage Guide (ASO)
This guide explains how the ASO team should prepare and use AI coding agents safely in this repository.
The main goal is to provide the AI with correct project context, commands, and safety boundaries so it can help faster without damaging the codebase.
## Core Principles
1. Humans make the final decisions.
2. AI must follow ASO project conventions.
3. AI must not commit/push unless explicitly asked.
4. AI should create a short plan before large/risky changes.
5. AI must run relevant verification checks before saying work is done.
6. Humans must review critical areas: security, auth/permission, corporate/member scoping, migrations, payments, data deletion, and production config.
7. This repository must have `AGENTS.md` at the root.
## Project Context
ASO is an operational health insurance/managed-care platform for managing corporates, members, claims, monitoring, livechat, and reporting.
Current main stack:
- Backend: Laravel 9 (`app/`) + modular backend with `nwidart/laravel-modules` (`Modules/`)
- Frontend: `frontend/dashboard` and `frontend/client-portal` (React + TypeScript + Vite), plus Laravel root assets (`resources/`, Vite/Mix)
- Auth/Permission: Sanctum + Spatie Permission (`role`, `permission`) + some JWT flows for specific integrations
- Backend testing: PHPUnit (`tests/Feature`, `tests/Unit`)
## AI Agent Ready in ASO Repo
This repository is considered AI Agent Ready when an AI can quickly answer:
- Modular backend structure and module boundaries
- Correct setup/run/test/lint/build commands for root + both frontend apps
- Where business logic should live
- Auth/permission/corporate-member scoping rules on protected endpoints
- Safe migration rules
- Git rules (no commit/push without instruction)
## Minimum Required Files
```text
.
├── AGENTS.md
├── README.md
├── .env-example (and/or .env.example)
└── docs/
└── ai-agent-guide.id.md
```
Recommended additions:
```text
docs/architecture.md
docs/testing.md
docs/database.md
docs/api-contracts.md
```
## Operational Commands (Current Repo)
Install dependencies:
```bash
composer install
npm install
cd frontend/dashboard && yarn install
cd ../client-portal && yarn install
```
Run locally:
```bash
php artisan serve
npm run dev
cd frontend/dashboard && yarn start
cd ../client-portal && yarn start
```
Backend tests:
```bash
php artisan test
```
Frontend lint:
```bash
cd frontend/dashboard && yarn lint
cd ../client-portal && yarn lint
```
Frontend build:
```bash
npm run production
cd frontend/dashboard && yarn build
cd ../client-portal && yarn build
```
Migrations:
```bash
php artisan migrate
```
Notes:
- A standard `typecheck` script is not consistently available across root/subprojects; run manual `tsc` only when needed for the task.
- This repo has mixed lockfiles (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`). Follow the lockfile and package manager already used in the touched subproject.
## Key Repository Structure
```text
app/ # Core Laravel app
routes/ # Root routes (web/api)
Modules/ # Domain modules (Client, Internal, Linksehat, Primaya, HospitalPortal)
Modules/*/Routes/api.php # API route entry per module
database/migrations/ # Root migrations
frontend/dashboard/ # Internal/admin frontend
frontend/client-portal/ # Client portal frontend
resources/ # Main Laravel views/assets
public/ # Public assets + frontend build output
tests/ # PHPUnit tests
```
## Architecture Rules for Agents
- Follow existing patterns before introducing new ones.
- Keep controllers thin; place logic in existing service/domain layers (`app/Services`, `Modules/*/Services`).
- For module APIs, add routes/controllers within the correct module; avoid placing module logic in random locations.
- Do not cross-import between frontend apps (`dashboard` vs `client-portal`) unless there is an explicit existing contract.
- When changing protected endpoint behavior, review related middleware (`auth:sanctum`, role/permission, module-specific middleware).
## Daily AI Workflow
### 1. Start a Task
- Read `AGENTS.md`.
- Understand existing patterns in the area you will change.
- Create a short plan for non-trivial work.
- Clarify ambiguous requirements.
### 2. Feature Work
- Keep changes small and focused.
- Avoid unrelated refactors.
- Add/update tests when behavior changes.
- Run relevant verification commands.
### 3. Bug Fixing
- Reproduce/inspect the issue first.
- Find the root cause.
- Apply a minimal, safe fix.
- Add a regression test when possible.
### 4. Pre-PR Review
Check:
- correctness
- security
- auth/permission/scoping
- migration safety
- missing tests
- lint/type/build impact
- unrelated changes
## Definition of Done for AI Tasks
AI work is not done until:
- Changes follow existing patterns.
- No unrelated files are changed.
- Relevant test/lint/build checks are run or failures are explained.
- Migration/auth/scoping risks are explained when relevant.
- Changed files and commands run are documented.
## Safety Rules (Mandatory)
AI agents must not:
- Commit/push/rewrite history without explicit instruction.
- Modify real `.env` values without explicit instruction.
- Print secrets in chat/output.
- Delete large data/folders without confirmation.
- Change DB schema without new migrations and safety notes.
- Add major dependencies without clear justification and impact.
## Ready-to-Use Prompts for This Repo
### Setup / Documentation Sync
```text
Read AGENTS.md and docs/ai-agent-guide.id.md.
Sync AI-agent documentation with the current codebase state.
Validate setup/test/lint/build commands against actual config files.
Do not commit changes.
```
### Feature Work
```text
Implement this feature: [describe].
Rules:
- Read AGENTS.md first.
- Follow existing ASO repository patterns.
- Do not commit.
- Run relevant verification commands.
Final output must include: Summary, Files changed, Commands run, Risks/notes.
```
### Bug Fixing
```text
Fix this bug: [describe].
Use systematic debugging:
1. Reproduce/inspect issue
2. Find root cause
3. Apply minimal safe fix
4. Add regression test if possible
5. Run verification
Do not commit.
```
## Standard Final Response Format
Use this format:
```text
Summary:
- ...
Files changed:
- ...
Commands run:
- ...
Risks / notes:
- ...
```

View File

@@ -0,0 +1,165 @@
{
"info": {
"name": "ProviderOnline v1 Flow",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{ "key": "base_url", "value": "http://127.0.0.1:8000" },
{ "key": "username", "value": "provtest" },
{ "key": "password", "value": "provpass" },
{ "key": "kodeprovider", "value": "MAN000000000Q" },
{ "key": "nokartu", "value": "WS2026021" },
{ "key": "kodebenefit", "value": "OP-GPSPHG001" },
{ "key": "p_user_no", "value": "1" },
{ "key": "p_token", "value": "" },
{ "key": "noklaim", "value": "" }
],
"item": [
{
"name": "1. HeaderKey",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/HeaderKey",
"body": {
"mode": "raw",
"raw": "{\n \"username\": \"{{username}}\",\n \"password\": \"{{password}}\",\n \"kodeprovider\": \"{{kodeprovider}}\"\n}"
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"pm.test('200', () => pm.response.to.have.status(200));",
"const j = pm.response.json();",
"pm.environment.set('p_user_no', String(j.userid));",
"pm.environment.set('p_token', j.usertoken);"
]
}
}
]
},
{
"name": "2. EligibilitasPeserta",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/EligibilitasPeserta",
"body": {
"mode": "raw",
"raw": "{\n \"nokartu\": \"{{nokartu}}\",\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
}
},
{
"name": "3. Pendaftaran",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/Pendaftaran",
"body": {
"mode": "raw",
"raw": "{\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"kodebenefit\": \"{{kodebenefit}}\",\n \"statusrujukan\": \"Y\",\n \"nomorrujukan\": \"RJ-20260515-0001\",\n \"keterangan\": \"Rawat jalan kontrol\",\n \"nomorsep\": \"SEP-20260515-0001\",\n \"nokartu\": \"{{nokartu}}\",\n \"kelaskamar\": \"KELAS-1\",\n \"cobbpjs\": 0,\n \"notransaksiprovider\": \"TRX-RS-20260515-0001\",\n \"inacbgscode\": \"E-4-10-I\",\n \"inacbgsamount\": 3200000,\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
},
"event": [
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
"const j = pm.response.json();",
"if (j && j.Data && j.Data[0] && j.Data[0].noklaim) pm.environment.set('noklaim', j.Data[0].noklaim);"
]
}
}
]
},
{
"name": "4. BillingSementara",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/BillingSementara",
"body": {
"mode": "raw",
"raw": "{\n \"noklaim\": \"{{noklaim}}\",\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"tanggalkeluar\": \"2026-05-15T10:00:00Z\",\n \"kodediagnosa\": \"I10\",\n \"daftarbiaya\": [\n { \"kodesubbenefit\": \"OPCONS1\", \"biayaaju\": 1500000 },\n { \"kodesubbenefit\": \"OPDIAG1\", \"biayaaju\": 1700000 }\n ],\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
}
},
{
"name": "5. Pengesahan",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/Pengesahan",
"body": {
"mode": "raw",
"raw": "{\n \"noklaim\": \"{{noklaim}}\",\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"tanggalkeluar\": \"2026-05-15T10:00:00Z\",\n \"kodediagnosa\": \"I10\",\n \"inacbgscode\": \"E-4-10-I\",\n \"inacbgsamount\": 3200000,\n \"daftarbiaya\": [\n { \"kodesubbenefit\": \"OPCONS1\", \"biayaaju\": 1500000 },\n { \"kodesubbenefit\": \"OPDIAG1\", \"biayaaju\": 1700000 }\n ],\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
}
},
{
"name": "6. RincianBiayaKlaim",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/RincianBiayaKlaim",
"body": {
"mode": "raw",
"raw": "{\n \"noklaim\": \"{{noklaim}}\",\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
}
},
{
"name": "7. StrukPendaftaran",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/StrukPendaftaran",
"body": {
"mode": "raw",
"raw": "{\n \"noklaim\": \"{{noklaim}}\",\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
}
},
{
"name": "8. StrukPengesahan",
"request": {
"method": "POST",
"header": [
{ "key": "Content-Type", "value": "application/json" },
{ "key": "Accept", "value": "application/json" }
],
"url": "{{base_url}}/api/v1/bridging-service/ProviderOnline/StrukPengesahan",
"body": {
"mode": "raw",
"raw": "{\n \"noklaim\": \"{{noklaim}}\",\n \"kodeprovider\": \"{{kodeprovider}}\",\n \"p_user_no\": {{p_user_no}},\n \"p_token\": \"{{p_token}}\"\n}"
}
}
}
]
}

View File

@@ -3,5 +3,6 @@
"Client": true,
"Linksehat": true,
"HospitalPortal": true,
"Primaya": true
}
"Primaya": true,
"ProviderIntegrations": true
}

View File

@@ -216,8 +216,9 @@
<div class="triangle2"></div>
<div class="triangle1"></div>
@php
if(!empty($logoPerusahaan->path)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents(storage_path('app/public/' . $logoPerusahaan->path)));
$logoPath = !empty($logoPerusahaan->path) ? storage_path('app/public/' . $logoPerusahaan->path) : null;
if (!empty($logoPath) && is_file($logoPath)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath));
echo '<img class="logo_company-' . now()->timestamp . '" src="' . $imgSrc . '">';
}
@endphp

View File

@@ -216,8 +216,9 @@
<div class="triangle2"></div>
<div class="triangle1"></div>
@php
if(!empty($logoPerusahaan->path)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents(storage_path('app/public/' . $logoPerusahaan->path)));
$logoPath = !empty($logoPerusahaan->path) ? storage_path('app/public/' . $logoPerusahaan->path) : null;
if (!empty($logoPath) && is_file($logoPath)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath));
echo '<img class="logo_company-' . now()->timestamp . '" src="' . $imgSrc . '">';
}
@endphp

View File

@@ -210,8 +210,9 @@
<div class="triangle2"></div>
<div class="triangle1"></div>
@php
if(!empty($logoPerusahaan->path)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents(storage_path('app/public/' . $logoPerusahaan->path)));
$logoPath = !empty($logoPerusahaan->path) ? storage_path('app/public/' . $logoPerusahaan->path) : null;
if (!empty($logoPath) && is_file($logoPath)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath));
echo '<img class="logo_company-' . now()->timestamp . '" src="' . $imgSrc . '">';
}
@endphp

View File

@@ -210,8 +210,9 @@
<div class="triangle2"></div>
<div class="triangle1"></div>
@php
if(!empty($logoPerusahaan->path)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents(storage_path('app/public/' . $logoPerusahaan->path)));
$logoPath = !empty($logoPerusahaan->path) ? storage_path('app/public/' . $logoPerusahaan->path) : null;
if (!empty($logoPath) && is_file($logoPath)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath));
echo '<img class="logo_company-' . now()->timestamp . '" src="' . $imgSrc . '">';
}
@endphp

View File

@@ -224,8 +224,9 @@
<div class="triangle2"></div>
<div class="triangle1"></div>
@php
if(!empty($logoPerusahaan->path)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents(storage_path('app/public/' . $logoPerusahaan->path)));
$logoPath = !empty($logoPerusahaan->path) ? storage_path('app/public/' . $logoPerusahaan->path) : null;
if (!empty($logoPath) && is_file($logoPath)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath));
echo '<img class="logo_company-' . now()->timestamp . '" src="' . $imgSrc . '">';
}
@endphp

View File

@@ -222,8 +222,9 @@
<div class="triangle2"></div>
<div class="triangle1"></div>
@php
if(!empty($logoPerusahaan->path)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents(storage_path('app/public/' . $logoPerusahaan->path)));
$logoPath = !empty($logoPerusahaan->path) ? storage_path('app/public/' . $logoPerusahaan->path) : null;
if (!empty($logoPath) && is_file($logoPath)) {
$imgSrc = 'data:image/png;base64,' . base64_encode(file_get_contents($logoPath));
echo '<img class="logo_company-' . now()->timestamp . '" src="' . $imgSrc . '">';
}
@endphp

View File

@@ -0,0 +1,34 @@
<?php
namespace Tests\Feature;
use Illuminate\Http\Request;
use Tests\TestCase;
class ProviderOnlineRouteTest extends TestCase
{
public function test_provider_online_routes_are_mapped_to_provider_integrations_controller(): void
{
$routes = [
'/api/v1/bridging-service/ProviderOnline/AddHeaderKey' => 'getHeaderKey',
'/api/v1/bridging-service/ProviderOnline/HeaderKey' => 'getHeaderKey',
'/api/v1/bridging-service/ProviderOnline/EligibilitasPeserta' => 'checkEligibilitasPeserta',
'/api/v1/bridging-service/ProviderOnline/Pendaftaran' => 'createPendaftaran',
'/api/v1/bridging-service/ProviderOnline/Pengesahan' => 'createPengesahan',
'/api/v1/bridging-service/ProviderOnline/BillingSementara' => 'upsertBillingSementara',
'/api/v1/bridging-service/ProviderOnline/RincianBiayaKlaim' => 'getRincianBiayaKlaim',
'/api/v1/bridging-service/ProviderOnline/StrukPendaftaran' => 'downloadStrukPendaftaran',
'/api/v1/bridging-service/ProviderOnline/StrukPengesahan' => 'downloadStrukPengesahan',
];
foreach ($routes as $uri => $method) {
$route = app('router')->getRoutes()->match(Request::create($uri, 'POST'));
$this->assertSame(
'Modules\\ProviderIntegrations\\Http\\Controllers\\Api\\ProviderOnlineController@' . $method,
$route->getActionName(),
'Route mismatch for ' . $uri
);
}
}
}