diff --git a/.env.example b/.env.example index e3c2bcb..a5d0e60 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ -# Mode Live: required for Gemini Copilot and Gemini Vision extraction. +# Optional: enables unverified Gemini Copilot responses. # Keep server-side only. Never prefix with NEXT_PUBLIC_. GEMINI_API_KEY= @@ -6,8 +6,12 @@ GEMINI_API_KEY= # Example: mysql://nexora_user:ENCODED_PASSWORD@127.0.0.1:3306/nexora MYSQL_URL= -# Optional Google Cloud configuration. The current scanner uses Gemini Vision; -# these values declare a Document AI processor for a future/custom adapter. +# Protects workspace and AI endpoints. Generate each value independently and +# keep them server-side only. The browser session expires after 8 hours. +NEXORA_ACCESS_TOKEN= +NEXORA_SESSION_SECRET= + +# Reserved for future integrations. The current document scanner is simulation-only. GOOGLE_CLOUD_PROJECT= GOOGLE_CLOUD_LOCATION=us DOCUMENT_AI_PROCESSOR_ID= diff --git a/README.md b/README.md index 6f2ac58..89c80b6 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-[![Production Live](https://img.shields.io/badge/Production-Live%20Platform-2563EB?style=for-the-badge&logo=vercel&logoColor=white)](https://nexora-chi-cyan.vercel.app) +[![Preview](https://img.shields.io/badge/Status-Demo%20Preview-F59E0B?style=for-the-badge&logo=vercel&logoColor=white)](https://nexora-chi-cyan.vercel.app) [![Google Cloud AI Builder Cup](https://img.shields.io/badge/Google%20Cloud-AI%20Builder%20Cup%202026-4285F4?style=for-the-badge&logo=googlecloud&logoColor=white)](https://cloud.google.com) [![Track 1: BFSI](https://img.shields.io/badge/Track%201-BFSI%20Smart%20Risk%20%26%20Fraud-059669?style=for-the-badge&logo=google&logoColor=white)](https://cloud.google.com) [![CI Build Status](https://img.shields.io/github/actions/workflow/status/fabrianivan/nexora/ci.yml?branch=main&label=Build%20%26%20CI&style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/fabrianivan/nexora/actions) @@ -31,28 +31,28 @@ --- -Multi-country corporate ownership, document intelligence, fraud detection, and tax compliance workspace for Indonesia, Singapore, Malaysia, and the United States. Built for Google Cloud AI Builder Cup, Track 1: BFSI. +Nexora is currently a demo interface for a multi-country corporate tax workspace. It is not a production tax, KYC, fraud, filing, or payments service. Production: -## Capabilities +## Current behavior -- Corporate entity CRUD with country-specific Zod validation, ownership graph, 100% ownership checks, and circular ownership detection. -- PDF/JPG/PNG attachment workflow up to 10 MB. Gemini Vision extraction is available only in Live Mode; Manual Mode uses the identical editable schema. -- Local fraud rules for TBTS indicators, IRAS Missing Trader risk, missing MyInvois UUID, split invoicing, non-deductible costs, and Z-score anomalies. -- Deterministic corporate tax, indirect tax, withholding/treaty, and BEPS Pillar Two simulator. Calculations never depend on an LLM. -- Editable fiscal reconciliation and validated draft exports for SPT 1771, Form C-S/Form C, and Form 1120 in JSON/XML plus a printable PDF summary. -- MySQL persistence through server-only API routes and a repository abstraction, with full workspace JSON import/export. +- The current UI loads synthetic company examples and marks them as demo data. UBO identity fields are redacted. +- Corporate workspace state can be saved to MySQL through the authenticated workspace API. Configure `MYSQL_URL` and the access/session secrets to enable it. +- The document scanner is a simulation. It does not upload or inspect a file, check a government registry, or verify compliance. +- Copilot output is unverified, has no retrieved citations, and must not be used as tax advice. Tax and legal claims: **PERLU VERIFIKASI AHLI PAJAK.** +- The persona switcher changes the demo view only. It is not per-user or per-entity authorization. +- Billing, tax submission, external registry lookups, and payments are not connected. -Nexora starts with an empty workspace. It contains no demo companies, fake scans, seeded invoices, or fabricated AI responses. +The application displays a persistent warning that demo scores and sample company data are not production verification. ## Operating Modes -**Manual Mode** is the default when `GEMINI_API_KEY` is absent. Every primary workflow remains available through manual forms. Copilot is disabled with a clear message, and the scanner never invents extracted values. +**Development preview** is available without auth configuration, but workspace persistence and AI endpoints remain unavailable. It uses synthetic data only. -**Live Mode** is enabled when `GEMINI_API_KEY` is present on the server. Gemini Vision can extract a user-uploaded attachment and Gemini can explain deterministic simulator results. API errors fail closed and return the user to manual entry. +In production, configure `NEXORA_ACCESS_TOKEN` and `NEXORA_SESSION_SECRET`. The browser exchanges the access token for an eight-hour, HttpOnly, SameSite session cookie. Workspace, scanner, and Copilot APIs reject requests without a valid session. The access token is shared; configure an identity provider and per-user/tenant authorization before multi-user production use. -The current implementation calls the Gemini API directly. `DOCUMENT_AI_PROCESSOR_ID`, BigQuery, and Vertex AI settings are optional configuration points and are not claimed as active integrations unless a deployment adds the corresponding adapter. +When `GEMINI_API_KEY` is configured, Copilot can call Gemini. Its response is still unverified and no search or citation retrieval is connected. Scanner output remains simulated. ## Architecture @@ -81,7 +81,7 @@ cp .env.example .env.local npm run dev ``` -Set `MYSQL_URL` in `.env.local` to a MySQL database the server can reach. The app creates the `nexora_workspaces` table on first use; the equivalent DDL is in `sql/schema.mysql.sql`. Open . Leave `GEMINI_API_KEY` empty to verify the complete Manual Mode flow. +Set `MYSQL_URL`, `NEXORA_ACCESS_TOKEN`, and `NEXORA_SESSION_SECRET` in `.env.local` to enable authenticated persistence. The server creates the `nexora_workspaces` table on first use. In development without auth configuration, the app opens in demo preview and does not save changes. Open . ```bash npm run lint @@ -109,10 +109,10 @@ e2e/ Playwright smoke test ## Security and Data -- Input is validated by Zod; uploads accept only PDF/JPEG/PNG and are capped at 10 MB. -- AI endpoints use an in-memory per-instance rate limit and request timeouts. -- Uploaded bytes are sent to Gemini only after the user explicitly requests extraction. Attachments are not persisted; structured fields and attachment metadata are stored in MySQL. -- The current build uses one shared workspace and has no sign-in, tenant isolation, or row-level ownership. Add authentication and tenant scoping before exposing a configured instance publicly. +- Workspace, scan simulation, and Copilot endpoints require a signed server session; the access token is shared by all operators. +- Workspace payloads are schema-validated and capped at 5 MB. AI/simulation routes have a basic per-process rate limit. +- The scanner does not accept or inspect file bytes. Do not enter real personal or tax data into the demo. +- There is no user directory, per-user role assignment, tenant isolation, audit log, or row-level ownership. Do not expose real customer data until these controls are implemented. ## Cloud Run @@ -123,15 +123,11 @@ docker build -t nexora . docker run --rm -p 8080:8080 -e PORT=8080 nexora ``` -Or deploy with `gcloud run deploy nexora --source . --region asia-southeast2 --allow-unauthenticated` and provide `MYSQL_URL` and (optionally) `GEMINI_API_KEY` through Secret Manager/environment configuration. The MySQL instance must be reachable from Cloud Run. +Or deploy with `gcloud run deploy nexora --source . --region asia-southeast2 --allow-unauthenticated` and provide `MYSQL_URL`, `NEXORA_ACCESS_TOKEN`, and `NEXORA_SESSION_SECRET` through Secret Manager/environment configuration. The MySQL instance must be reachable from Cloud Run. ## Known Limitations -- Workspace persistence requires `MYSQL_URL`; the workspace is stored as one JSON document in MySQL. The current build has no authentication or tenant isolation. -- Gemini citations are displayed as official regulatory source links; no Vertex AI Search/RAG index is connected in this version. -- Gemini's per-field confidence is model-reported and is not independently calibrated. Missing or invalid extraction output is sent to manual entry; no confidence score is fabricated. -- Risk scores are a UI heuristic based on local findings, not a calibrated probability of fraud. TBTS tax comparisons use a configured standard-rate assumption and require verification against applicable exceptions. -- Z-Score analysis uses e-invoices for each entity separately and requires at least five invoices for that entity. -- The PDF output uses the browser print dialog, not an authority-issued filing form. -- Payment channels are informational only. Nexora does not create billing codes, file returns, or move funds. -- Tax rates are configurable assumptions and require professional review before real filing. +- Workspace persistence requires MySQL and stores one shared JSON payload; multi-tenant storage is not implemented. +- The demo contains tax rates, compliance scores, ownership, and transaction examples that have not been legally or operationally verified. +- Copilot has no retrieval-backed citations. All tax and legal content: **PERLU VERIFIKASI AHLI PAJAK.** +- No official billing, return submission, registry, KYC/PEP screening, or payment integration exists. diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index 7f49adb..691e726 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1,13 +1,36 @@ import { expect, test } from '@playwright/test'; -test('manual mode supports creating an entity and opening document flow', async ({ page }) => { +async function authenticate(page: import('@playwright/test').Page) { + const response = await page.request.post('/api/session', { data: { token: 'test-access-token' } }); + expect(response.ok()).toBeTruthy(); +} + +test('demo labels are visible and primary navigation switches workspace', async ({ page }) => { + await authenticate(page); + await page.goto('/'); + await expect(page.getByRole('status')).toContainText('DATA DEMO / SIMULASI'); + await page.getByRole('button', { name: 'Calculation', exact: true }).first().click(); + await expect(page.getByRole('heading', { name: /Kertas Kerja Rekonsiliasi Fiskal/ })).toBeVisible(); +}); + +test('workspace API rejects unauthenticated requests', async ({ request }) => { + const response = await request.get('/api/workspace'); + expect(response.status()).toBe(401); + expect((await request.post('/api/copilot', { data: { prompt: 'test' } })).status()).toBe(401); + expect((await request.post('/api/scan-document', { data: { fileName: 'test.pdf' } })).status()).toBe(401); +}); + +test('mobile navigation remains available at narrow viewport', async ({ page }) => { + await authenticate(page); + await page.setViewportSize({ width: 390, height: 844 }); await page.goto('/'); - await expect(page.getByText('Mode Manual', { exact: true })).toBeVisible(); - await page.getByLabel('Nama legal').fill('PT Uji Kepatuhan'); - await page.getByLabel('NPWP / UEN / TIN / EIN').fill('1234567890123456'); - await page.getByLabel('Alamat').fill('Jalan Uji Nomor 1 Jakarta'); - await page.getByRole('button', { name: 'Simpan entitas' }).click(); - await expect(page.getByText('PT Uji Kepatuhan').first()).toBeVisible(); - await page.getByRole('button', { name: 'Dokumen' }).click(); - await expect(page.getByText('Smart Document & e-Invoice Scanner')).toBeVisible(); + await expect(page.getByRole('navigation', { name: 'Navigasi utama' })).toBeVisible(); + await page.getByRole('navigation', { name: 'Navigasi utama' }).getByRole('button', { name: 'Payment' }).click(); + await expect(page.getByRole('heading', { name: /Tax Payment & Settlement/ })).toBeVisible(); +}); + +test('security response headers are set', async ({ request }) => { + const response = await request.get('/'); + expect(response.headers()['x-frame-options']).toBe('DENY'); + expect(response.headers()['x-content-type-options']).toBe('nosniff'); }); diff --git a/next.config.mjs b/next.config.mjs index 4de717a..3260995 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,6 +2,21 @@ const nextConfig = { output: 'standalone', reactStrictMode: true, + async headers() { + return [ + { + source: '/:path*', + headers: [ + { key: 'X-Content-Type-Options', value: 'nosniff' }, + { key: 'X-Frame-Options', value: 'DENY' }, + { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, + { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, + { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' }, + ], + }, + { source: '/api/:path*', headers: [{ key: 'Cache-Control', value: 'no-store' }, { key: 'X-Robots-Tag', value: 'noindex' }] }, + ]; + }, }; export default nextConfig; diff --git a/src/app/api/copilot/route.ts b/src/app/api/copilot/route.ts index 292617d..d33a390 100644 --- a/src/app/api/copilot/route.ts +++ b/src/app/api/copilot/route.ts @@ -1,187 +1,39 @@ import { NextRequest, NextResponse } from 'next/server'; import { MULTI_COUNTRY_DATA, CountryCode } from '@/data/multiCountryTaxData'; +import { isSessionValid } from '@/lib/server/session'; +import { rateLimit } from '@/lib/server/rateLimit'; function getSystemInstruction(country: CountryCode): string { const cfg = MULTI_COUNTRY_DATA[country] || MULTI_COUNTRY_DATA.ID; - return `Anda adalah Nexora Tax Global Tax Copilot, asisten konsultan pajak internasional, auditor forensik keuangan, dan spesialis Transfer Pricing / Tax Treaty (P3B/DTA) berbasis Google Cloud Vertex AI & Vertex AI Search. -Saat ini pengguna sedang berada pada yurisdiksi: ${cfg.flag} ${cfg.name} (${cfg.authorityFull}). -Entitas Aktif: ${cfg.company.name} (${cfg.company.taxIdLabel}: ${cfg.company.taxIdValue}). -Sistem e-Invoice: ${cfg.eInvoiceSystem}. -Pajak Tidak Langsung: ${cfg.indirectTaxName}. -Tarif Pajak Korporasi Normal: ${(cfg.corporateTaxNormalRate * 100).toFixed(1)}% (${cfg.smeSpecialRateLabel}). + return `Anda adalah asisten umum untuk pertanyaan pajak ${cfg.name}. Informasi konteks ini hanya label yurisdiksi, bukan sumber hukum atau data resmi. -Berikan jawaban yang terstruktur, akurat, mengutip dasar hukum perpajakan negara terkait (Indonesia: UU HPP & Coretax DJP; Singapura: Singapore Income Tax Act & GST Act IRAS; Malaysia: Income Tax Act 1967 & LHDN MyInvois; Amerika Serikat: Internal Revenue Code IRC & IRS Regulations; Lintas Negara: OECD BEPS 2.0 & Tax Treaty DTA/P3B).`; +Berikan jawaban umum. Jangan mengarang tarif, pasal, tenggat, atau sitasi. Kutip sumber hanya jika sumber tersebut benar-benar tersedia dalam konteks; jika tidak, nyatakan keterbatasan dan tulis: PERLU VERIFIKASI AHLI PAJAK.`; } -function generateMultiCountryFallback(prompt: string, country: CountryCode) { - const q = prompt.toLowerCase(); +function generateMultiCountryFallback(_prompt: string, country: CountryCode) { const cfg = MULTI_COUNTRY_DATA[country] || MULTI_COUNTRY_DATA.ID; - - // Singapore specific queries - if (country === 'SG' || q.includes('iras') || q.includes('singapura') || q.includes('pte') || q.includes('missing trader')) { - if (q.includes('missing trader') || q.includes('mtf') || q.includes('lion city') || q.includes('gst')) { - return { - engine: 'Vertex AI Gemini 2.5 Flash + IRAS GST Forensics Engine', - confidence: 99.3, - citations: [ - 'Singapore Goods and Services Tax Act (Cap. 117A) Section 20(2A)', - 'IRAS e-Tax Guide: GST — Missing Trader Fraud (Knowledge Principle)', - ], - reply: `### Protokol Mitigasi IRAS Missing Trader Fraud (MTF) — **Nusantara FinTech Pte. Ltd.** - -Sistem **Nexora Tax** menandai invoice \`LCHT/2026/08/0192\` dari **Lion City Hardware Trading LLP** (Nilai: **S$ 45,000**, Input GST: **S$ 4,410**) dengan **Skor Risiko 95/100 (CRITICAL)**. - ---- - -#### 1. Temuan Forensik Document AI & IRAS Registry: -- **Status Registrasi GST**: Nomor \`M9-9991827-K\` berstatus *Cancelled/Suspended* di registri IRAS. -- **Overcharge Tarif GST**: Vendor menagihkan GST sebesar **9,80% (S$ 4,410)**, melebihi tarif resmi **9% (S$ 4,050)**. -- **Risiko Hukum Section 20(2A) GST Act**: Di bawah *Knowledge Principle*, IRAS berhak menolak klaim Input GST dan mengenakan penalti jika bisnis mengklaim pajak masukan dari rantai transaksi yang terindikasi *Missing Trader Fraud*. - -#### 2. Tindakan Mitigasi Segera: -1. **Keluarkan dari GST Form F5 (Box 7)**: Jangan klaim Input GST sebesar **S$ 4,410** pada pelaporan kuartal ini. -2. **Verifikasi UEN & PEPPOL ID via InvoiceNow**: Wajibkan seluruh vendor mengirimkan e-Invoice tervalidasi melalui jaringan **IMDA InvoiceNow**.`, - }; - } - - return { - engine: 'Vertex AI Gemini 2.5 Flash + Vertex AI Search (IRAS Tax Corpus)', - confidence: 99.4, - citations: [ - 'Singapore Income Tax Act 1947 Section 43(6) — Partial Tax Exemption (PTE)', - 'IRAS Enterprise Innovation Scheme (EIS) — 400% Enhanced Tax Deduction', - 'Singapore–Indonesia Avoidance of Double Taxation Agreement (DTA)', - ], - reply: `### Simulasi Pajak Korporasi Singapura (IRAS Form C-S) & Optimasi EIS - -Untuk **Nusantara FinTech Pte. Ltd.** (UEN: \`202319482K\`): -- **Chargeable Income (Sebelum PTE)**: **S$ 341,500** -- **Tarif Pajak Korporasi Standar (Headline Rate)**: **17%** (S$ 58,055 jika tanpa insentif) - ---- - -#### 1. Perhitungan Partial Tax Exemption (PTE) Singapura: -- **75% Exemption** atas S$ 10,000 pertama = **S$ 7,500 bebas pajak** -- **50% Exemption** atas S$ 190,000 berikutnya = **S$ 95,000 bebas pajak** -- **Total Penghasilan Bebas Pajak (PTE)**: **S$ 102,500** -- **Net Chargeable Income Setelah PTE**: \`S$ 341,500 - S$ 102,500\` = **S$ 239,000** -- **Pajak Korporasi Terutang (17%)**: \`17% × S$ 239,000\` = **S$ 40,630** *(Effective Tax Rate hanya **11,9%** — hemat **S$ 17,425**!)*. - -#### 2. Tambahan Penghematan via Enterprise Innovation Scheme (EIS): -Atas pengeluaran R&D & AI sebesar **S$ 85,000**, perusahaan berhak mengklaim **400% Tax Deduction** di bawah skema EIS IRAS, menghemat pajak tambahan hingga **S$ 27,425**.`, - }; - } - - // Malaysia specific queries - if (country === 'MY' || q.includes('lhdn') || q.includes('myinvois') || q.includes('malaysia')) { - return { - engine: 'Vertex AI Gemini 2.5 Flash + LHDN MyInvois Compliance Engine', - confidence: 99.2, - citations: [ - 'Income Tax Act 1967 (Malaysia) Section 82B & Section 120(1)(d)', - 'LHDN MyInvois e-Invoice Guideline (Version 4.0) & Specific Guideline', - 'Income Tax Act 1967 Schedule 1 Part I Para 2A (SME Tiered Tax Rate)', - ], - reply: `### Analisis Kepatuhan LHDN MyInvois & Pajak Korporasi SME Malaysia - -Untuk **Nusantara Digital Solutions Sdn. Bhd.** (TIN: \`C25849102090\`): - ---- - -#### 1. Risiko Invoice Tanpa Validasi UUID MyInvois (\`MY-TRX-089\` — RM 65,000) -- Di bawah mandat **LHDN MyInvois (Continuous Transaction Control)**, setiap pengeluaran bisnis wajib didukung oleh e-Invoice yang telah divalidasi oleh LHDN (memiliki **Unique Identifier Number / UUID** dan kode QR terverifikasi). -- Invoice manual dari **Klang Valley Enterprise Sdn. Bhd.** senilai **RM 65,000** tidak memiliki UUID MyInvois. Jika tetap diklaim sebagai pengurang pajak di **Form C**, LHDN akan melakukan *add-back* (koreksi positif) dan mengenakan penalti **Section 113(2) ITA 1967**. - -#### 2. Perhitungan Tarif Pajak Berjenjang SME Malaysia (Chargeable Income RM 709,000): -- **RM 150,000 pertama @ 15%** = **RM 22,500** -- **RM 450,000 berikutnya (RM 150k–RM 600k) @ 17%** = **RM 76,500** -- **Sisa RM 109,000 (> RM 600k) @ 24%** = **RM 26,160** -- **Total Pajak Terutang**: **RM 125,160** *(Hemat **RM 45,000** dibanding tarif korporasi standar 24% sebesar RM 170,160)*.`, - }; - } - - // US specific queries - if (country === 'US' || q.includes('irs') || q.includes('1099') || q.includes('w-9') || q.includes('schedule m-1')) { - return { - engine: 'Vertex AI Gemini 2.5 Flash + IRS IRC Tax Engine', - confidence: 99.1, - citations: [ - 'Internal Revenue Code (IRC) §274(a) & §274(n) — Disallowance of Entertainment & 50% Meal Limit', - 'IRC §3406 — Mandatory 24% Backup Withholding on Missing/Invalid TIN', - 'IRC §1441 & §1442 — 30% Foreign Withholding & Form W-8BEN-E Treaty Relief', - ], - reply: `### Audit Kepatuhan IRS Form 1120 (Schedule M-1) & Backup Withholding (IRC §3406) - -Untuk **Nusantara AI Technologies, Inc.** (Federal EIN: \`88-4920193\`): - ---- - -#### 1. Koreksi Fiskal Schedule M-1 (Meals & Entertainment — \`US-TRX-061\`) -Berdasarkan **IRC §274** (pasca-TCJA): -- **Tiket VIP / Hiburan Klien ($14,000)**: **100% Non-Deductible** (Koreksi Fiskal Positif **$14,000**). -- **Jamuan Makan Bisnis dengan Klien ($10,000)**: Hanya **50% Deductible** (Koreksi Fiskal Positif **$5,000**). -- **Total Schedule M-1 Book-to-Tax Add-Back**: **$19,000**. - -#### 2. Pelanggaran Backup Withholding 24% (\`US-TRX-089\` — $62,000) -- Pembayaran jasa kontraktor **$62,000** kepada **Apex Growth Partners** dilakukan tanpa **Form W-9** yang lolos *IRS TIN Matching*. -- Sesuai **IRC §3406**, pembayar wajib memotong **24% Backup Withholding ($14,880)** dan menyetorkannya melalui **IRS Form 945**. Untuk vendor luar negeri (misal anak usaha di Indonesia/Singapura), wajib mengumpulkan **Form W-8BEN-E** untuk menurunkan tarif potong dari **30%** menjadi **10%** (sesuai Tax Treaty US–Indonesia).`, - }; - } - - // Cross-border / Tax Treaty queries - if (q.includes('treaty') || q.includes('p3b') || q.includes('dta') || q.includes('lintas negara') || q.includes('multi negara') || q.includes('transfer pricing')) { - return { - engine: 'Vertex AI Gemini 2.5 Flash + Global OECD BEPS & DTA Treaty Engine', - confidence: 99.5, - citations: [ - 'P3B (Tax Treaty) Indonesia–Singapura, Indonesia–Malaysia, & Indonesia–Amerika Serikat', - 'PER-25/PJ/2018 tentang Tata Cara Penerapan Persetujuan Penghindaran Pajak Berganda (Form DGT)', - 'PMK 172 Tahun 2023 tentang Penerapan Prinsip Kewajaran dan Kelaziman Usaha (Transfer Pricing)', - ], - reply: `### Analisis Perpajakan Lintas Negara (Cross-Border Tax Treaty / P3B & Transfer Pricing) - -**Nexora Tax** memantau kewajiban pemotongan pajak lintas yurisdiksi (**Withholding Tax**) untuk transaksi grup antara **🇮🇩 Indonesia, 🇸🇬 Singapura, 🇲🇾 Malaysia, dan 🇺🇸 Amerika Serikat**: - -| Rute Transaksi Lintas Negara | Tarif Domestik Tanpa Dokumen | Tarif Tax Treaty (DTA / P3B) | Dokumen Wajib yang Divalidasi Document AI | -| :--- | :---: | :---: | :--- | -| **🇮🇩 ID $\\rightarrow$ 🇸🇬 SG** (Jasa Cloud / Royalti) | 20% (PPh Pasal 26) | **10% (Royalti) / 0% (Laba Usaha tanpa BUT)** | **Form DGT (e-SKD)** + *Certificate of Residence (COR) IRAS* | -| **🇸🇬 SG $\\rightarrow$ 🇺🇸 US** (Lisensi AI / Royalti) | 10% (IRAS Section 45) | **10% / Bebas untuk Software Copyrighted Article** | **IRAS Section 45 Filing** + Kontrak Lisensi | -| **🇮🇩 ID $\\rightarrow$ 🇺🇸 US** (Langganan SaaS / Royalti) | 20% (PPh Pasal 26) | **10% (Pasal 13 Tax Treaty RI–AS)** | **Form DGT** + **IRS Form 6166 (US Residency)** | -| **🇺🇸 US $\\rightarrow$ 🇮🇩 ID** (Pembayaran Klien US) | 30% (IRC §1442 FDAP) | **10% / 0% (Business Profits)** | **IRS Form W-8BEN-E** | - -**Rekomendasi Kepatuhan**: Pastikan **e-SKD (Form DGT)** diterbitkan **sebelum** batas waktu penyampaian SPT Masa PPh Pasal 26 agar penghematan tarif dari 20% ke 10%/0% tidak dibatalkan saat pemeriksaan.`, - }; - } - - // Default Indonesia / General response return { - engine: `Vertex AI Gemini 2.5 Flash + Vertex AI Search (${cfg.authorityShort})`, - confidence: 99.2, - citations: [ - `${cfg.authorityFull} — ${cfg.filingFormName}`, - `Cross-Border Tax Treaty Relief: ${cfg.crossBorderTreatyNote.requiredForm}`, - ], - reply: `### Analisis Kepatuhan Pajak Multi-Negara — **${cfg.flag} ${cfg.name} (${cfg.company.name})** - -Berdasarkan buku besar aktif pada sistem **${cfg.eInvoiceSystem}**: -- **Identitas Pajak**: \`${cfg.company.taxIdLabel}: ${cfg.company.taxIdValue}\` -- **Tax Compliance Score**: **${cfg.company.taxHealthScore}/100** (Risiko ${cfg.riskNoticeName}: **${cfg.company.auditRiskProb}%**) -- **Pajak Tidak Langsung**: **${cfg.indirectTaxName}** | **Tarif Pajak Badan**: **${(cfg.corporateTaxNormalRate * 100).toFixed(0)}%** (${cfg.smeSpecialRateLabel}) -- **Optimasi Lintas Negara (Tax Treaty)**: Gunakan **${cfg.crossBorderTreatyNote.requiredForm}** untuk memangkas tarif pemotongan dari **${cfg.crossBorderTreatyNote.standardWhtRate}** menjadi **${cfg.crossBorderTreatyNote.treatyWhtRate}**. - -Silakan klik salah satu tombol pertanyaan cepat di atas atau ubah bendera negara di bilah atas untuk mengaudit entitas di negara lain!`, + engine: 'Simulasi lokal Nexora (tanpa sumber eksternal)', + confidence: null, + citations: [], + verification: 'simulation', + reply: `Saya belum dapat memverifikasi jawaban pajak ${cfg.name} dari sumber resmi. Hubungkan model AI dan tinjau setiap klaim dengan penasihat pajak. PERLU VERIFIKASI AHLI PAJAK.`, }; } export async function POST(req: NextRequest) { + if (!isSessionValid(req)) return NextResponse.json({ error: 'Autentikasi diperlukan.' }, { status: 401 }); + if (!rateLimit('copilot:session', 20)) return NextResponse.json({ error: 'Batas permintaan tercapai. Coba lagi sebentar.' }, { status: 429 }); try { const body = await req.json(); - const { prompt, apiKey, country = 'ID' } = body as { + const { prompt, country = 'ID' } = body as { prompt: string; - apiKey?: string; country?: CountryCode; }; - const effectiveKey = process.env.GEMINI_API_KEY?.trim() || apiKey?.trim(); + if (typeof prompt !== 'string' || prompt.trim().length === 0 || prompt.length > 4000 || !(country in MULTI_COUNTRY_DATA)) { + return NextResponse.json({ error: 'Prompt atau yurisdiksi tidak valid.' }, { status: 400 }); + } + const effectiveKey = process.env.GEMINI_API_KEY?.trim(); if (effectiveKey) { try { @@ -214,13 +66,12 @@ export async function POST(req: NextRequest) { data?.candidates?.[0]?.content?.parts?.[0]?.text || generateMultiCountryFallback(prompt, country).reply; return NextResponse.json({ - reply: text, - citations: [ - `${MULTI_COUNTRY_DATA[country].authorityFull}`, - `OECD BEPS 2.0 & Tax Treaty (${MULTI_COUNTRY_DATA[country].crossBorderTreatyNote.requiredForm})`, - ], - engine: `Live Google Gemini 2.5 Flash (${MULTI_COUNTRY_DATA[country].name} Jurisdiction)`, - confidence: 99.6, + reply: `⚠️ Keluaran AI belum diverifikasi oleh sumber resmi. PERLU VERIFIKASI AHLI PAJAK.\n\n${text}`, + citations: [], + engine: 'Gemini API (sumber hukum tidak diambil atau diverifikasi)', + confidence: null, + verification: 'unverified-ai-output', + disclaimer: 'Jawaban AI belum diverifikasi. Tarif, aturan, dan tenggat pajak: PERLU VERIFIKASI AHLI PAJAK.', }); } } catch { @@ -228,7 +79,16 @@ export async function POST(req: NextRequest) { } } - return NextResponse.json(generateMultiCountryFallback(prompt || '', country)); + const simulation = generateMultiCountryFallback(prompt, country); + return NextResponse.json({ + ...simulation, + engine: 'Simulasi lokal Nexora (tanpa sumber eksternal)', + confidence: null, + citations: [], + verification: 'simulation', + disclaimer: 'Ini simulasi dengan data contoh, bukan nasihat atau verifikasi pajak. Tarif, aturan, dan tenggat: PERLU VERIFIKASI AHLI PAJAK.', + reply: `⚠️ SIMULASI — bukan nasihat pajak dan tidak memeriksa sumber resmi. PERLU VERIFIKASI AHLI PAJAK.\n\n${simulation.reply}`, + }); } catch { return NextResponse.json( { error: 'Gagal memproses permintaan AI Copilot Multi-Negara' }, diff --git a/src/app/api/scan-document/route.ts b/src/app/api/scan-document/route.ts index 48871ea..450e426 100644 --- a/src/app/api/scan-document/route.ts +++ b/src/app/api/scan-document/route.ts @@ -1,7 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; import { MULTI_COUNTRY_DATA, CountryCode, formatCurrency } from '@/data/multiCountryTaxData'; +import { isSessionValid } from '@/lib/server/session'; +import { rateLimit } from '@/lib/server/rateLimit'; export async function POST(req: NextRequest) { + if (!isSessionValid(req)) return NextResponse.json({ error: 'Autentikasi diperlukan.' }, { status: 401 }); + if (!rateLimit('document-simulation:session', 10)) return NextResponse.json({ error: 'Batas pemindaian tercapai. Coba lagi sebentar.' }, { status: 429 }); try { const body = await req.json(); const { @@ -18,6 +22,10 @@ export async function POST(req: NextRequest) { country?: CountryCode; }; + if (typeof fileName !== 'string' || fileName.trim().length === 0 || fileName.length > 200 || !['ID', 'SG', 'MY', 'US'].includes(country)) { + return NextResponse.json({ error: 'Metadata dokumen atau yurisdiksi tidak valid.' }, { status: 400 }); + } + const cfg = MULTI_COUNTRY_DATA[country] || MULTI_COUNTRY_DATA.ID; const dpp = Number(customDpp) || (country === 'ID' ? 28500000 : 25000); const expectedTax = Math.round(dpp * cfg.indirectTaxRate); @@ -39,25 +47,26 @@ export async function POST(req: NextRequest) { title: `Ekstraksi Document AI (${cfg.name}) — ${fileName}`, docType: 'e-Faktur PPN', fileName, - uploadDate: `Baru saja (${cfg.authorityShort} Live OCR)`, + uploadDate: `Simulasi (${cfg.authorityShort})`, + simulation: true, vendorName: vendor, npwp16: isSuspicious ? `INVALID-${country}-TAXID` : cfg.company.taxIdValue, nitku: isSuspicious ? 'UNVERIFIED-ENDPOINT' : cfg.company.subIdValue, docNumber: docNum, - docDate: '18 Oktober 2026', + docDate: 'Tanggal tidak diekstrak (simulasi)', dpp, taxType: `${cfg.indirectTaxName}`, taxRate: `${((reportedTax / dpp) * 100).toFixed(2)}%`, taxAmount: reportedTax, totalAmount: dpp + reportedTax, - status: isSuspicious ? 'RISIKO_FRAUD' : 'PATUH', - confidenceScore: 99.3, + status: isSuspicious ? 'SIMULASI_BUTUH_TINJAUAN' : 'SIMULASI_TANPA_VERIFIKASI', + confidenceScore: 0, extractedFields: [ { label: `Sistem e-Invoice (${cfg.eInvoiceSystem})`, value: fileName, validation: 'valid', - note: `Dipindai menggunakan Google Cloud Document AI (${cfg.name} Processor)`, + note: `Simulasi ekstraksi untuk ${cfg.name}; file belum diunggah dan tidak ada registry yang diakses.`, }, { label: `${cfg.company.taxIdLabel} Vendor`, @@ -65,7 +74,7 @@ export async function POST(req: NextRequest) { validation: isSuspicious ? 'error' : 'valid', note: isSuspicious ? `PERINGATAN: Identitas pajak vendor gagal validasi di portal ${cfg.authorityShort}` - : `Aktif dan patuh di registri ${cfg.authorityShort}`, + : `Belum diverifikasi pada registri ${cfg.authorityShort}; nilai ini hanya data simulasi.`, }, { label: 'Dasar Pengenaan Pajak (Taxable Base)', @@ -83,7 +92,7 @@ export async function POST(req: NextRequest) { expectedTax, country )} (Selisih ${formatCurrency(diff, country)})` - : `Akurat 100% sesuai tarif resmi ${cfg.indirectTaxName}`, + : `Perhitungan aritmetika simulasi; tarif dan perlakuan pajak perlu verifikasi ahli.`, }, ], aiAuditSummary: isSuspicious @@ -94,14 +103,14 @@ export async function POST(req: NextRequest) { expectedTax, country )}). Berisiko memicu ${cfg.riskNoticeName}.` - : `DOKUMEN VALID & PATUH (${cfg.authorityShort}): Seluruh atribut ${cfg.eInvoiceSystem} pada ${fileName} telah sesuai regulasi ${cfg.name}. Nilai pajak ${formatCurrency( + : `SIMULASI — bukan verifikasi kepatuhan (${cfg.authorityShort}): ${fileName} tidak diunggah atau diperiksa terhadap ${cfg.eInvoiceSystem}. Nilai contoh ${formatCurrency( reportedTax, country )} siap dimasukkan ke ${cfg.filingFormName}.`, - regulationRef: `${cfg.authorityFull} — Standar Validasi ${cfg.eInvoiceSystem}`, + regulationRef: 'Tidak ada sumber resmi yang diakses; hasil hanya simulasi.', recommendedAction: isSuspicious - ? `Tahan klaim pajak masukan dan minta revisi dokumen melalui ${cfg.eInvoiceSystem}.` - : `Setujui dan masukkan otomatis ke draf ${cfg.filingFormName}.`, + ? 'Tinjau dokumen dan lakukan validasi manual sebelum mengambil keputusan.' + : 'Jangan menyetujui dokumen dari hasil simulasi ini; lakukan validasi manual.', }); } catch { return NextResponse.json( diff --git a/src/app/api/session/route.ts b/src/app/api/session/route.ts new file mode 100644 index 0000000..347a708 --- /dev/null +++ b/src/app/api/session/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { accessTokenMatches, createSession, isSessionValid, sessionCookieName, sessionIsConfigured } from '@/lib/server/session'; +import { rateLimit } from '@/lib/server/rateLimit'; + +export async function GET(request: NextRequest) { + if (!sessionIsConfigured()) return NextResponse.json({ configured: false, authenticated: false }, { status: 503 }); + return NextResponse.json({ configured: true, authenticated: isSessionValid(request) }); +} + +export async function POST(request: NextRequest) { + if (!sessionIsConfigured()) return NextResponse.json({ error: 'Akses belum dikonfigurasi. Atur NEXORA_ACCESS_TOKEN dan NEXORA_SESSION_SECRET.' }, { status: 503 }); + if (!rateLimit('session-login', 10, 60_000)) return NextResponse.json({ error: 'Terlalu banyak percobaan. Coba lagi dalam satu menit.' }, { status: 429 }); + let body: unknown; + try { body = await request.json(); } catch { return NextResponse.json({ error: 'Request harus berupa JSON.' }, { status: 400 }); } + const token = typeof body === 'object' && body !== null ? (body as { token?: unknown }).token : undefined; + if (!accessTokenMatches(token)) return NextResponse.json({ error: 'Token akses tidak valid.' }, { status: 401 }); + + const response = NextResponse.json({ authenticated: true }); + response.cookies.set(sessionCookieName, createSession(), { + httpOnly: true, + secure: request.nextUrl.protocol === 'https:', + sameSite: 'strict', + path: '/', + maxAge: 8 * 60 * 60, + }); + return response; +} + +export async function DELETE() { + const response = NextResponse.json({ authenticated: false }); + response.cookies.set(sessionCookieName, '', { httpOnly: true, sameSite: 'strict', path: '/', maxAge: 0 }); + return response; +} diff --git a/src/app/api/status/route.ts b/src/app/api/status/route.ts index 9661aab..39efa78 100644 --- a/src/app/api/status/route.ts +++ b/src/app/api/status/route.ts @@ -1,3 +1,3 @@ export async function GET() { - return Response.json({ mode: process.env.GEMINI_API_KEY?.trim() ? 'live' : 'manual', gemini: Boolean(process.env.GEMINI_API_KEY?.trim()), documentAi: Boolean(process.env.GOOGLE_CLOUD_PROJECT && process.env.DOCUMENT_AI_PROCESSOR_ID), bigQuery: Boolean(process.env.BIGQUERY_DATASET), mysqlConfigured: Boolean(process.env.MYSQL_URL?.trim()) }); + return Response.json({ mode: process.env.GEMINI_API_KEY?.trim() ? 'ai-unverified' : 'simulation', gemini: Boolean(process.env.GEMINI_API_KEY?.trim()), documentAi: false, documentScanner: 'simulation-only', bigQuery: Boolean(process.env.BIGQUERY_DATASET), mysqlConfigured: Boolean(process.env.MYSQL_URL?.trim()) }); } diff --git a/src/app/api/workspace/route.ts b/src/app/api/workspace/route.ts index 2cd2785..21c720e 100644 --- a/src/app/api/workspace/route.ts +++ b/src/app/api/workspace/route.ts @@ -1,10 +1,12 @@ import { NextRequest } from 'next/server'; import { workspaceSchema } from '@/lib/domain'; import { workspaceRepository } from '@/lib/mysql'; +import { isSessionValid } from '@/lib/server/session'; export const runtime = 'nodejs'; -export async function GET() { +export async function GET(request: NextRequest) { + if (!isSessionValid(request)) return Response.json({ error: 'Autentikasi diperlukan.' }, { status: 401 }); if (!process.env.MYSQL_URL?.trim()) return Response.json({ error: 'Database belum dikonfigurasi. Tambahkan MYSQL_URL.' }, { status: 503 }); try { const workspace = await workspaceRepository.load(); @@ -15,6 +17,7 @@ export async function GET() { } export async function PUT(request: NextRequest) { + if (!isSessionValid(request)) return Response.json({ error: 'Autentikasi diperlukan.' }, { status: 401 }); if (!process.env.MYSQL_URL?.trim()) return Response.json({ error: 'Database belum dikonfigurasi. Tambahkan MYSQL_URL.' }, { status: 503 }); const reader = request.body?.getReader(); if (!reader) return Response.json({ error: 'Request body kosong.' }, { status: 400 }); diff --git a/src/app/page.tsx b/src/app/page.tsx index 920da60..9fc9dd2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, useRef } from 'react'; import { Building2, Plus, @@ -75,7 +75,7 @@ interface UserPersona { const USER_PERSONAS: UserPersona[] = [ { id: 'ceo_parent', - name: 'Ir. Aditya Pratama, M.Sc.', + name: 'Demo Group Executive', title: 'Group Chief Executive Officer (CEO)', entityRole: 'PARENT', organization: 'PT Nusantara Kreasi Digital Tbk (Induk Grup)', @@ -89,7 +89,7 @@ const USER_PERSONAS: UserPersona[] = [ }, { id: 'cfo_parent', - name: 'Sri Mulyani Dewi, S.E., Ak., CA', + name: 'Demo Group Finance Lead', title: 'Group Chief Financial Officer (CFO)', entityRole: 'PARENT', organization: 'PT Nusantara Kreasi Digital Tbk (Induk Grup)', @@ -103,7 +103,7 @@ const USER_PERSONAS: UserPersona[] = [ }, { id: 'md_sg', - name: 'Marcus Tan, CFA', + name: 'Demo Singapore Director', title: 'Managing Director — Singapore Subsidiary', entityRole: 'SUBSIDIARY', organization: 'Nusantara FinTech Pte. Ltd. (Singapura)', @@ -117,7 +117,7 @@ const USER_PERSONAS: UserPersona[] = [ }, { id: 'dir_my', - name: 'Ahmad Razak, CA (M)', + name: 'Demo Malaysia Director', title: 'Country Director — Malaysia Subsidiary', entityRole: 'SUBSIDIARY', organization: 'Nusantara Digital Solutions Sdn. Bhd. (Malaysia)', @@ -131,7 +131,7 @@ const USER_PERSONAS: UserPersona[] = [ }, { id: 'ctrl_us', - name: 'Sarah Jenkins, CPA', + name: 'Demo US Controller', title: 'VP Finance & Controller — US Subsidiary', entityRole: 'SUBSIDIARY', organization: 'Nusantara AI Technologies, Inc. (Amerika Serikat)', @@ -158,8 +158,8 @@ interface IndividualShareholder { effectiveSharePercent: number; votingRightPercent: number; dividendTaxRegime: string; - pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)' | 'Enhanced Due Diligence (PEP)'; - kycDocStatus: 'Verified by Document AI' | 'Pending Review'; + pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)' | 'Enhanced Due Diligence (PEP)' | 'Not assessed (demo)'; + kycDocStatus: 'Verified by Document AI' | 'Pending Review' | 'Not verified (demo)'; status: 'Active' | 'Under Review'; address: string; email: string; @@ -253,7 +253,7 @@ interface CorporateEntity { individualShareholders: IndividualShareholder[]; } -const INITIAL_COMPANIES: CorporateEntity[] = [ +const INITIAL_COMPANIES: CorporateEntity[] = ([ { id: 'ENT-ID-001', legalName: 'PT Nusantara Kreasi Digital Tbk', @@ -273,12 +273,12 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ functionalCurrency: 'IDR (Indonesian Rupiah)', paidUpCapital: 'Rp 25.000.000.000', taxOfficeName: 'KPP Madya Jakarta Selatan I (Coretax DJP)', - address: 'Jl. Jend. Sudirman Kav. 52-53, District 8 SCBD Treasury Tower Lt. 28', + address: '[REDACTED DEMO]', cityAndPostal: 'Jakarta Selatan, DKI Jakarta 12190, Indonesia', authorizedTaxOfficer: 'Budi Santoso, S.E., Ak., CA., BKP', officerPosition: 'VP of Global Tax & Fiscal Compliance (Kuasa Wajib Pajak)', - contactEmail: 'tax.compliance@nusantaradigital.co.id', - contactPhone: '+62 21 5088 9910', + contactEmail: 'demo@example.invalid', + contactPhone: '[REDACTED DEMO]', businessClassification: 'KLU 62019 — Aktivitas Pemrograman Komputer, AI & Holding FinTech', taxAuthority: 'Direktorat Jenderal Pajak (Coretax DJP)', vatOrGstStatus: 'PKP Aktif (PPN 11% • e-Faktur 4.0 Coretax)', @@ -454,8 +454,8 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2020-06-15', status: 'Active', bepsRisk: 'Low (Substantial Activity)', - address: '8 Marina View, #42-01 Asia Square Tower 1, Singapore 018960', - contactEmail: 'corporate@archipelagoventures.sg', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, { id: 'INT-00002', @@ -469,8 +469,8 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2018-03-14', status: 'Active', bepsRisk: 'Low (Substantial Activity)', - address: 'Pacific Century Place Lt. 35, SCBD Lot 10, Jakarta Selatan 12190', - contactEmail: 'legal@merdekakapital.co.id', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, { id: 'INT-00003', @@ -484,20 +484,20 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2022-11-01', status: 'Under Review', bepsRisk: 'High (Conduit / Treaty Shopping Risk)', - address: '200 Park Avenue, Suite 3900, New York, NY 10166, USA', - contactEmail: 'tax@bluehorizonpartners.com', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, ], individualShareholders: [ { id: 'IND-00001', type: 'Individual Shareholder (UBO)', - fullName: 'Dr. Hendra Kusuma, S.Kom., M.B.A.', + fullName: 'Demo Shareholder', roleTitle: 'Founder & President Commissioner (Ultimate Beneficial Owner)', nationality: '🇮🇩 Indonesia', taxResidency: '🇮🇩 Indonesia (SPDN — KPP Pratama Jakarta Kebayoran Baru Satu)', - nationalIdOrPassport: 'NIK: 3174011403800001', - personalTaxId: '3174011403800001 (NPWP 16 Digit NIK)', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 25, effectiveSharePercent: 48.5, votingRightPercent: 48.5, @@ -505,18 +505,18 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: 'Jl. Senopati Dalam No. 18, Kebayoran Baru, Jakarta Selatan 12190', - email: 'hendra.kusuma@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, { id: 'IND-00002', type: 'Individual Founder / Director', - fullName: 'Kartika Wijaya, S.E., Ak., CA.', + fullName: 'Demo Shareholder', roleTitle: 'Co-Founder & Chief Executive Officer (Direktur Utama)', nationality: '🇮🇩 Indonesia', taxResidency: '🇮🇩 Indonesia (SPDN — KPP Pratama Jakarta Setiabudi)', - nationalIdOrPassport: 'NIK: 3171045208850003', - personalTaxId: '3171045208850003 (NPWP 16 Digit NIK)', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 15, effectiveSharePercent: 28.0, votingRightPercent: 28.0, @@ -524,18 +524,18 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: 'Apartemen Pacific Place Residence Lt. 22, SCBD, Jakarta Selatan 12190', - email: 'kartika.wijaya@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, { id: 'IND-00003', type: 'Individual Minority Investor', - fullName: 'Marcus Tan Wei Ming', + fullName: 'Demo Shareholder', roleTitle: 'Non-Resident Individual Angel Investor (WPLN)', nationality: '🇸🇬 Singapore', taxResidency: '🇸🇬 Singapore (SPLN — IRAS Certificate of Residence Verified)', - nationalIdOrPassport: 'Passport: K8192031Z / NRIC: S8419283D', - personalTaxId: 'SG-NRIC-S8419283D', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 5, effectiveSharePercent: 19.0, votingRightPercent: 19.0, @@ -543,8 +543,8 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: '15 Nassim Road, #04-02 Nassim Park Residences, Singapore 258382', - email: 'marcus.tan@archipelagoventures.sg', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, ], }, @@ -567,12 +567,12 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ functionalCurrency: 'SGD (Singapore Dollar)', paidUpCapital: 'S$ 1,500,000', taxOfficeName: 'Inland Revenue Authority of Singapore (IRAS Revenue House)', - address: '10 Marina Boulevard, #39-01 Marina Bay Financial Centre Tower 2', + address: '[REDACTED DEMO]', cityAndPostal: 'Singapore 018983', authorizedTaxOfficer: 'Lim Wei Ling, SCTP (Accredited Tax Advisor)', officerPosition: 'Regional Tax Director — APAC', - contactEmail: 'sg.tax@nusantarafintech.sg', - contactPhone: '+65 6812 4910', + contactEmail: 'demo@example.invalid', + contactPhone: '[REDACTED DEMO]', businessClassification: 'SSIC 62019 — Software Publishing, FinTech IP & Treasury Center', taxAuthority: 'Inland Revenue Authority of Singapore (IRAS)', vatOrGstStatus: 'GST Registered (9% Prevailing Rate • IMDA InvoiceNow Active)', @@ -725,8 +725,8 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2021-05-20', status: 'Active', bepsRisk: 'Low (Substantial Activity)', - address: 'SCBD Lot 11, Jakarta Selatan 12190, Indonesia', - contactEmail: 'tax.compliance@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, { id: 'INT-00005', @@ -740,20 +740,20 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2022-01-10', status: 'Active', bepsRisk: 'Low (Substantial Activity)', - address: '60B Orchard Road, #06-18 The Atrium@Orchard, Singapore 238891', - contactEmail: 'investments@temasekseed.sg', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, ], individualShareholders: [ { id: 'IND-00004', type: 'Individual Shareholder (UBO)', - fullName: 'Dr. Hendra Kusuma, S.Kom., M.B.A.', + fullName: 'Demo Shareholder', roleTitle: 'Registrable Controller (ACRA RORC — Ultimate Beneficial Owner)', nationality: '🇮🇩 Indonesia', taxResidency: '🇮🇩 Indonesia (SPDN)', - nationalIdOrPassport: 'Passport: C9182731', - personalTaxId: '3174011403800001', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 0, effectiveSharePercent: 41.2, votingRightPercent: 41.2, @@ -761,18 +761,18 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: 'Jl. Senopati Dalam No. 18, Jakarta Selatan 12190, Indonesia', - email: 'hendra.kusuma@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, { id: 'IND-00005', type: 'Individual Founder / Director', - fullName: 'Lim Wei Ling, SCTP', + fullName: 'Demo Shareholder', roleTitle: 'Singapore Resident Director & ESOP Individual Shareholder', nationality: '🇸🇬 Singapore', taxResidency: '🇸🇬 Singapore (IRAS Tax Resident)', - nationalIdOrPassport: 'NRIC: S8829104G', - personalTaxId: 'SG-NRIC-S8829104G', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 5, effectiveSharePercent: 5.0, votingRightPercent: 5.0, @@ -780,8 +780,8 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: '18 Marina Gardens Drive, #28-04 Marina One Residences, Singapore 018953', - email: 'weiling.lim@nusantarafintech.sg', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, ], }, @@ -804,12 +804,12 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ functionalCurrency: 'MYR (Malaysian Ringgit)', paidUpCapital: 'RM 1,500,000', taxOfficeName: 'LHDN Cawangan Kuala Lumpur Bandar (MyTax / MyInvois)', - address: 'Level 25, Menara Maxis, Kuala Lumpur City Centre (KLCC)', + address: '[REDACTED DEMO]', cityAndPostal: '50088 Kuala Lumpur, Wilayah Persekutuan, Malaysia', authorizedTaxOfficer: 'Ahmad Faiz bin Razak, CTIM', officerPosition: 'Head of Malaysia Tax & MyInvois Compliance', - contactEmail: 'my.tax@nusantaradigital.my', - contactPhone: '+60 3 2380 1190', + contactEmail: 'demo@example.invalid', + contactPhone: '[REDACTED DEMO]', businessClassification: 'MSIC 62010 — Computer Programming & Enterprise SaaS Distribution', taxAuthority: 'Lembaga Hasil Dalam Negeri Malaysia (LHDN MyInvois)', vatOrGstStatus: 'RMCD SST Registered (8% Service Tax • Mandatory MyInvois e-Invoice)', @@ -937,20 +937,20 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2022-08-12', status: 'Active', bepsRisk: 'Low (Substantial Activity)', - address: '10 Marina Boulevard, #39-01 MBFC Tower 2, Singapore 018983', - contactEmail: 'sg.tax@nusantarafintech.sg', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, ], individualShareholders: [ { id: 'IND-00006', type: 'Individual Founder / Director', - fullName: 'Ahmad Faiz bin Razak, CTIM', + fullName: 'Demo Shareholder', roleTitle: 'Malaysia Resident Director & Minority Individual Shareholder', nationality: '🇲🇾 Malaysia', taxResidency: '🇲🇾 Malaysia (LHDN Resident Individual)', - nationalIdOrPassport: 'MyKad: 890412-14-5521', - personalTaxId: 'IG-890412145521 (LHDN TIN)', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 5, effectiveSharePercent: 5.0, votingRightPercent: 5.0, @@ -958,18 +958,18 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: 'Jalan Ampang Hilir No. 24, 55000 Kuala Lumpur, Malaysia', - email: 'ahmad.faiz@nusantaradigital.my', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, { id: 'IND-00007', type: 'Individual Shareholder (UBO)', - fullName: 'Dr. Hendra Kusuma, S.Kom., M.B.A.', + fullName: 'Demo Shareholder', roleTitle: 'Ultimate Beneficial Owner (SSM Section 56 BO Register)', nationality: '🇮🇩 Indonesia', taxResidency: '🇮🇩 Indonesia (SPDN)', - nationalIdOrPassport: 'Passport: C9182731', - personalTaxId: '3174011403800001', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 0, effectiveSharePercent: 41.2, votingRightPercent: 41.2, @@ -977,8 +977,8 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: 'Jl. Senopati Dalam No. 18, Jakarta Selatan 12190, Indonesia', - email: 'hendra.kusuma@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, ], }, @@ -1001,12 +1001,12 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ functionalCurrency: 'USD (United States Dollar)', paidUpCapital: 'US$ 2,000,000', taxOfficeName: 'Internal Revenue Service (Ogden UT International & Corporate Center)', - address: '548 Market St, Suite 49102, Financial District', + address: '[REDACTED DEMO]', cityAndPostal: 'San Francisco, CA 94104, United States', authorizedTaxOfficer: 'Michael Chen, CPA, MST', officerPosition: 'Head of US Tax & IRS Compliance', - contactEmail: 'us.tax@nusantara-ai.com', - contactPhone: '+1 (415) 890-4412', + contactEmail: 'demo@example.invalid', + contactPhone: '[REDACTED DEMO]', businessClassification: 'NAICS 541511 — Custom Computer Programming & AI Cloud Services', taxAuthority: 'Internal Revenue Service (IRS MeF & State Nexus)', vatOrGstStatus: 'Multi-State Economic Sales Tax Nexus (CA, NY, TX • Wayfair Compliant)', @@ -1134,20 +1134,20 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ effectiveDate: '2023-02-01', status: 'Active', bepsRisk: 'Low (Substantial Activity)', - address: 'SCBD Lot 11, Jakarta Selatan 12190, Indonesia', - contactEmail: 'tax.compliance@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + contactEmail: 'demo@example.invalid', }, ], individualShareholders: [ { id: 'IND-00008', type: 'Individual Founder / Director', - fullName: 'Michael Chen, CPA, MST', + fullName: 'Demo Shareholder', roleTitle: 'US Resident Officer & Section 83(b) Restricted Stock Shareholder', nationality: '🇺🇸 United States', taxResidency: '🇺🇸 United States (IRS Form 1040 Resident)', - nationalIdOrPassport: 'US Passport: 549182731', - personalTaxId: 'SSN: ***-**-4918 (Form W-9 Verified)', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 5, effectiveSharePercent: 5.0, votingRightPercent: 5.0, @@ -1155,18 +1155,18 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: '425 California St, Apt 18B, San Francisco, CA 94104, USA', - email: 'm.chen@nusantara-ai.com', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, { id: 'IND-00009', type: 'Individual Shareholder (UBO)', - fullName: 'Dr. Hendra Kusuma, S.Kom., M.B.A.', + fullName: 'Demo Shareholder', roleTitle: 'FinCEN BOI Registered Ultimate Beneficial Owner', nationality: '🇮🇩 Indonesia', taxResidency: '🇮🇩 Indonesia (Non-Resident Alien — Form W-8BEN)', - nationalIdOrPassport: 'Passport: C9182731', - personalTaxId: 'Foreign TIN: 3174011403800001 (Form W-8BEN)', + nationalIdOrPassport: '[REDACTED DEMO]', + personalTaxId: '[REDACTED DEMO]', directSharePercent: 0, effectiveSharePercent: 48.5, votingRightPercent: 48.5, @@ -1174,12 +1174,28 @@ const INITIAL_COMPANIES: CorporateEntity[] = [ pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', kycDocStatus: 'Verified by Document AI', status: 'Active', - address: 'Jl. Senopati Dalam No. 18, Jakarta Selatan 12190, Indonesia', - email: 'hendra.kusuma@nusantaradigital.co.id', + address: '[REDACTED DEMO]', + email: 'demo@example.invalid', }, ], }, -]; +] satisfies CorporateEntity[]).map((company) => ({ + ...company, + address: '[REDACTED DEMO]', + authorizedTaxOfficer: 'Demo Contact (unverified)', + contactEmail: 'demo@example.invalid', + contactPhone: '[REDACTED DEMO]', + status: 'PENDING', + aiComplianceScore: 0, + documents: company.documents.map((document) => ({ + ...document, + status: 'Pending Review', + confidenceScore: 0, + extractedNote: 'Data simulasi; dokumen tidak diunggah atau diverifikasi.', + })), + structureNodes: company.structureNodes.map((node) => ({ ...node, treatyFormStatus: 'Pending Renewal', complianceScore: 0 })), + individualShareholders: [], +})); // Pure AST markdown renderer for chat bubbles (**bold**, *italic*, `code`, ### headers, bullets, formulas, NPWP badges) @@ -1277,6 +1293,12 @@ function renderChatMessage(text: string, isUserMessage = false): React.ReactNode } export default function NexoraApp() { + const [sessionStatus, setSessionStatus] = useState<'checking' | 'authenticated' | 'preview' | 'login_required' | 'configuration_missing'>('checking'); + const [accessToken, setAccessToken] = useState(''); + const [sessionError, setSessionError] = useState(''); + const [workspaceStatus, setWorkspaceStatus] = useState<'loading' | 'connected' | 'unavailable' | 'saving' | 'save_failed'>('loading'); + const workspaceReadyRef = useRef(false); + const workspaceCoreRef = useRef>({ version: 1, entities: [], ownerships: [], documents: [], reconciliation: [] }); // Primary & Sub Navigation matching Figma Prototype (`Companies | Calculation | Tax Return | Payment` + `Corporate | Individual | Studies | Tax Calendar | Reports`) const [primaryNav, setPrimaryNav] = useState('companies'); const [subNav, setSubNav] = useState('corporate'); @@ -1288,6 +1310,92 @@ export default function NexoraApp() { const [companySearch, setCompanySearch] = useState(''); const [entityRoleFilter, setEntityRoleFilter] = useState<'ALL' | 'PARENT' | 'SUBSIDIARY'>('ALL'); + useEffect(() => { + let active = true; + fetch('/api/session').then(async (response) => { + if (response.ok) { + const result = await response.json(); + if (active) setSessionStatus(result.authenticated ? 'authenticated' : 'login_required'); + } else if (response.status === 503 && process.env.NODE_ENV === 'development') { + if (active) setSessionStatus('preview'); + } else if (active) { + setSessionStatus(response.status === 503 ? 'configuration_missing' : 'login_required'); + } + }).catch(() => { if (active) setSessionStatus(process.env.NODE_ENV === 'development' ? 'preview' : 'configuration_missing'); }); + return () => { active = false; }; + }, []); + + useEffect(() => { + let active = true; + if (sessionStatus === 'preview') { + setWorkspaceStatus('unavailable'); + return () => { active = false; }; + } + if (sessionStatus !== 'authenticated') return () => { active = false; }; + fetch('/api/workspace').then(async (response) => { + if (!response.ok) throw new Error('Workspace MySQL belum dapat dimuat.'); + const { workspace } = await response.json(); + workspaceCoreRef.current = { + version: 1, + entities: Array.isArray(workspace?.entities) ? workspace.entities : [], + ownerships: Array.isArray(workspace?.ownerships) ? workspace.ownerships : [], + documents: Array.isArray(workspace?.documents) ? workspace.documents : [], + reconciliation: Array.isArray(workspace?.reconciliation) ? workspace.reconciliation : [], + }; + const storedCompanies = workspace?.corporateCompanies; + if (Array.isArray(storedCompanies) && storedCompanies.length > 0 && storedCompanies.every((company) => + company && typeof company === 'object' && typeof company.id === 'string' && typeof company.legalName === 'string' && typeof company.countryCode === 'string' + )) { + setCompanies(storedCompanies as CorporateEntity[]); + const firstEntityId = (storedCompanies[0] as CorporateEntity).id; + setSelectedEntityId(firstEntityId); + } + if (active) { + workspaceReadyRef.current = true; + setWorkspaceStatus('connected'); + } + }).catch(() => { if (active) setWorkspaceStatus('unavailable'); }); + return () => { active = false; }; + }, [sessionStatus]); + + useEffect(() => { + if (sessionStatus !== 'authenticated' || !workspaceReadyRef.current) return; + const timer = window.setTimeout(() => { + setWorkspaceStatus('saving'); + fetch('/api/workspace', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...workspaceCoreRef.current, corporateCompanies: companies }), + }).then((response) => { + setWorkspaceStatus(response.ok ? 'connected' : 'save_failed'); + }).catch(() => setWorkspaceStatus('save_failed')); + }, 700); + return () => window.clearTimeout(timer); + }, [companies, sessionStatus]); + + async function handleSessionLogin(event: React.FormEvent) { + event.preventDefault(); + setSessionError(''); + try { + const response = await fetch('/api/session', { + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: accessToken }), + }); + if (!response.ok) { + const result = await response.json(); + setSessionError(result.error || 'Autentikasi gagal.'); + return; + } + setAccessToken(''); + setSessionStatus('authenticated'); + } catch { setSessionError('Server tidak dapat dijangkau.'); } + } + + async function handleLogout() { + await fetch('/api/session', { method: 'DELETE' }).catch(() => undefined); + workspaceReadyRef.current = false; + setSessionStatus('login_required'); + } + // Figma 1 5-Screen Corporate Management State const [inputMode, setInputMode] = useState<'search_existing' | 'manual'>('manual'); const [registryLookupQuery, setRegistryLookupQuery] = useState(''); @@ -1318,24 +1426,24 @@ export default function NexoraApp() { const [newShJurisdiction, setNewShJurisdiction] = useState('🇸🇬 Singapore'); const [newShTaxId, setNewShTaxId] = useState('UEN-202499182K'); const [newShPercent, setNewShPercent] = useState('15'); - const [newShAddress, setNewShAddress] = useState('1 Raffles Place, #20-61 One Raffles Place, Singapore 048616'); + const [newShAddress, setNewShAddress] = useState('[DATA DEMO DIHAPUS]'); const [newShEmail, setNewShEmail] = useState('legal@globalhorizon.sg'); // Add Individual Shareholder Form fields (Pemegang Saham Perorangan / UBO) - const [newIndName, setNewIndName] = useState('Ir. Aditya Pratama, M.Sc.'); + const [newIndName, setNewIndName] = useState('Contoh Pemegang Saham'); const [newIndType, setNewIndType] = useState('Individual Shareholder (UBO)'); const [newIndRole, setNewIndRole] = useState('Co-Founder & Chief Technology Officer (UBO)'); const [newIndNationality, setNewIndNationality] = useState('🇮🇩 Indonesia'); const [newIndResidency, setNewIndResidency] = useState('🇮🇩 Indonesia (SPDN — KPP Pratama Jakarta Selatan)'); - const [newIndNationalId, setNewIndNationalId] = useState('NIK: 3174051908840002'); - const [newIndTaxId, setNewIndTaxId] = useState('3174051908840002 (NPWP 16 Digit NIK)'); + const [newIndNationalId, setNewIndNationalId] = useState('[DATA DEMO DIHAPUS]'); + const [newIndTaxId, setNewIndTaxId] = useState('[DATA DEMO DIHAPUS]'); const [newIndDirectShare, setNewIndDirectShare] = useState('10'); const [newIndEffectiveShare, setNewIndEffectiveShare] = useState('18.5'); const [newIndDividendRegime, setNewIndDividendRegime] = useState( '0% Bebas PPh Dividen (Diinvestasikan di NKRI min. 3 Tahun — UU HPP Pasal 4 ayat (3) huruf f & PMK 18/2021)' ); - const [newIndAddress, setNewIndAddress] = useState('Jl. Wijaya II No. 44, Kebayoran Baru, Jakarta Selatan 12160'); - const [newIndEmail, setNewIndEmail] = useState('aditya.pratama@nusantaradigital.co.id'); + const [newIndAddress, setNewIndAddress] = useState('[DATA DEMO DIHAPUS]'); + const [newIndEmail, setNewIndEmail] = useState('demo@example.invalid'); // Multi-Entity & RBAC Access Control States (International Standard) const [activePersonaId, setActivePersonaId] = useState('ceo_parent'); @@ -1374,13 +1482,8 @@ export default function NexoraApp() { { role: 'assistant', entityLabel: '🇮🇩 PT Nusantara Kreasi Digital Tbk (Induk) & 3 Anak Perusahaan', - text: 'Halo! Saya **Nexora AI Tax & Subsidiary Copilot** yang siap membantu Anda menganalisis pajak induk & seluruh anak perusahaan (**🇮🇩 Indonesia, 🇸🇬 Singapura, 🇲🇾 Malaysia, 🇺🇸 Amerika Serikat**). Klik tombol cepat di bawah atau pilih perusahaan di halaman untuk mengaudit dokumen, mendeteksi faktur fiktif, dan mengoptimalkan *Tax Treaty (P3B/DTA)*.', - citations: [ - 'UU HPP & Coretax DJP (Induk ID)', - 'IRAS InvoiceNow & ITA (Anak Usaha SG)', - 'LHDN MyInvois & ITA 1967 (Anak Usaha MY)', - 'IRS Form 1120 & Form 5472 (Anak Usaha US)', - ], + text: 'Copilot memberikan keluaran AI yang belum diverifikasi dan tidak mengambil sumber resmi. Jangan masukkan data pribadi atau rahasia. Untuk klaim pajak: **PERLU VERIFIKASI AHLI PAJAK.**', + citations: [], }, ]); @@ -1458,24 +1561,7 @@ export default function NexoraApp() { setIsDocAiRunning(true); setTimeout(() => { setIsDocAiRunning(false); - setCompanies((prev) => - prev.map((c) => - c.id === selectedEntity.id - ? { - ...c, - status: 'ACTIVE', - aiComplianceScore: Math.min(99, c.aiComplianceScore + 5), - documents: c.documents.map((d) => ({ - ...d, - status: 'Verified by Document AI', - })), - } - : c - ) - ); - triggerToast( - `Google Cloud Document AI berhasil mengekstrak & memvalidasi ke-4 dokumen korporasi untuk ${selectedEntity.legalName}!` - ); + triggerToast('Simulasi saja: belum ada file yang diunggah dan Document AI/registry belum dijalankan.'); }, 750); } @@ -1575,7 +1661,7 @@ export default function NexoraApp() { ); setShowShareholderModal(false); triggerToast( - `Intermediate Shareholder "${newSh.name}" (${newSh.id}) berhasil disimpan & lolos skrining OECD BEPS Action 6!` + `Data contoh "${newSh.name}" (${newSh.id}) tersimpan; skrining hukum dan kepatuhan belum dilakukan.` ); } @@ -1611,9 +1697,9 @@ export default function NexoraApp() { effectiveSharePercent: Number(newIndEffectiveShare) || Number(newIndDirectShare) || 10, votingRightPercent: Number(newIndEffectiveShare) || Number(newIndDirectShare) || 10, dividendTaxRegime: newIndDividendRegime, - pepAndSanctionStatus: 'Verified Clear (Non-PEP / FATF Safe)', - kycDocStatus: 'Verified by Document AI', - status: 'Active', + pepAndSanctionStatus: 'Not assessed (demo)', + kycDocStatus: 'Not verified (demo)', + status: 'Under Review', address: newIndAddress, email: newIndEmail, }; @@ -1629,7 +1715,7 @@ export default function NexoraApp() { ); setShowIndividualModal(false); triggerToast( - `Individual Shareholder "${newInd.fullName}" (${newInd.id}) berhasil ditambahkan & diverifikasi Document AI KYC!` + `Data demo pemegang saham "${newInd.fullName}" (${newInd.id}) ditambahkan; KYC dan PEP belum diverifikasi.` ); } @@ -1673,7 +1759,7 @@ export default function NexoraApp() { ) ); triggerToast( - `Anomali transaksi ${trxId} (${vendorName}) pada ${selectedEntity.legalName} berhasil dikoreksi secara fiskal!` + `Status demo untuk anomali ${trxId} (${vendorName}) diperbarui. Tidak ada catatan pajak atau transaksi yang dikoreksi.` ); } } @@ -1722,8 +1808,37 @@ export default function NexoraApp() { } } + if (sessionStatus === 'checking') { + return
Memeriksa sesi…
; + } + + if (sessionStatus === 'login_required' || sessionStatus === 'configuration_missing') { + return ( +
+
+
Nexora • akses terlindungi
+

Masuk ke workspace

+ {sessionStatus === 'configuration_missing' ? ( +

Akses belum dikonfigurasi. Administrator perlu mengatur NEXORA_ACCESS_TOKEN dan NEXORA_SESSION_SECRET.

+ ) : ( + <> + + setAccessToken(event.target.value)} className="w-full rounded-lg border border-slate-300 px-3 py-2 text-sm" required /> + {sessionError &&

{sessionError}

} + + + )} +
+
+ ); + } + return ( -
+
+
+ DATA DEMO / SIMULASI. Persona, hasil AI, skor kepatuhan, dan data perusahaan bawaan bukan verifikasi produksi. + MySQL: {workspaceStatus === 'connected' ? 'terhubung' : workspaceStatus === 'saving' ? 'menyimpan…' : workspaceStatus === 'save_failed' ? 'gagal menyimpan' : workspaceStatus === 'loading' ? 'memuat…' : 'tidak tersedia (perubahan tidak tersimpan)'} +
{/* ===================================================================== 1. TOP WHITE HEADER BAR (EXACT FIGMA 2 LAYOUT — CLEAN, NO EXTRA BUTTONS) ===================================================================== */} @@ -1747,7 +1862,7 @@ export default function NexoraApp() { Nexora - Fintech Tax + Demo
@@ -1808,6 +1923,11 @@ export default function NexoraApp() {
+ {sessionStatus === 'authenticated' && ( + + )}
@@ -1816,7 +1936,7 @@ export default function NexoraApp() { =================================================================== */}
-
+
{[ { id: 'corporate', label: 'Corporate', icon: Building2 }, { id: 'individual', label: 'Individual Shareholder', icon: Users }, @@ -1857,6 +1977,19 @@ export default function NexoraApp() {
+ + {/* Toast Notification */} {toastMessage && (
@@ -1891,11 +2024,11 @@ export default function NexoraApp() {
- OECD Pillar Two & ISO/IEC 27001 Access Governance + DEMO DATA • Persona switcher bukan kontrol akses - Multi-Entity Consolidated Control + Tampilan grup simulasi 1 Induk + 3 Anak Usaha @@ -1906,7 +2039,7 @@ export default function NexoraApp() { GROUP ENTITIES (INDUK & ANAK PERUSAHAAN)

- Struktur Korporasi Grup: PT Nusantara Kreasi Digital Tbk (Induk) mengonsolidasi 3 Anak Perusahaan di 🇸🇬 Singapura, 🇲🇾 Malaysia, dan 🇺🇸 Amerika Serikat. Selaras standar tata kelola internasional, pemegang peran eksekutif induk memiliki visibilitas konsolidasian penuh. + Contoh struktur perusahaan untuk demonstrasi. Data dan hubungan kepemilikan ini belum diverifikasi dan tidak mewakili organisasi nyata.

@@ -1963,11 +2096,11 @@ export default function NexoraApp() { {isGroupCeoOrParentExecutive ? ( <> - Hak Akses Eksekutif Terverifikasi: Sebagai CEO/setara dari Perusahaan Induk (PT Nusantara Kreasi Digital Tbk), Anda memiliki wewenang penuh melihat seluruh entitas induk & anak perusahaan (100% Group Visibility sesuai standar enterprise internasional). + Persona demo aktif: Pemilihan ini hanya mengubah tampilan contoh. Akses database dan API dikontrol terpisah oleh sesi server. ) : ( <> - Segregasi Hak Akses Aktif: Anda sedang masuk sebagai karyawan anak usaha ({activePersona.title}). Sesuai ISO 27001, entitas lain dikunci. Hanya CEO/C-Level dari Perusahaan Induk yang berhak melihat semua entitas. + Persona demo aktif: Filter ini hanya simulasi tampilan dan bukan pembatasan keamanan. Sesi saat ini memakai satu token operator bersama. )} @@ -1978,7 +2111,7 @@ export default function NexoraApp() { type="button" onClick={() => { setActivePersonaId('ceo_parent'); - triggerToast('Beralih ke Hak Akses: Ir. Aditya Pratama (Group CEO - Induk)'); + triggerToast('Persona demo diganti. Ini bukan perubahan hak akses server.'); }} className="text-[11px] font-bold text-amber-300 hover:text-white underline cursor-pointer shrink-0" > @@ -2449,7 +2582,7 @@ export default function NexoraApp() { type="button" onClick={() => triggerToast( - `Data registrasi pajak untuk "${registryLookupQuery || selectedEntity.legalName}" berhasil disinkronkan!` + `Simulasi saja: registry pajak eksternal belum diakses untuk "${registryLookupQuery || selectedEntity.legalName}".` ) } className="absolute right-1.5 top-1.5 bottom-1.5 px-4 rounded-lg bg-[#2563EB] hover:bg-blue-700 text-white text-xs font-bold transition shadow-xs cursor-pointer flex items-center justify-center" @@ -2714,7 +2847,7 @@ export default function NexoraApp() { {isDocAiRunning ? 'Scanning Document AI...' - : 'Verify All Documents (Document AI)'} + : 'Simulasikan pemeriksaan (demo)'}
@@ -2725,13 +2858,13 @@ export default function NexoraApp() {
- Memindai Dokumen Statutory dengan Google Cloud Document AI OCR... + Simulasi antarmuka — tidak ada dokumen yang sedang dipindai - 99.2% + DEMO
@@ -2757,10 +2890,10 @@ export default function NexoraApp() { className={`text-[10px] font-bold px-2.5 py-0.5 rounded-full border font-mono tabular-nums ${ doc.status === 'Verified by Document AI' ? 'bg-emerald-50 text-emerald-700 border-emerald-200' - : 'bg-rose-50 text-rose-700 border-rose-200' + : 'bg-amber-50 text-amber-800 border-amber-200' }`} > - ✓ {doc.confidenceScore}% AI Verified + Simulasi • belum diverifikasi
@@ -4233,7 +4366,9 @@ export default function NexoraApp() { className={`px-2.5 py-0.5 rounded-full text-[10px] font-bold ${ doc.status === 'PATUH' ? 'bg-emerald-100 text-emerald-800' - : 'bg-rose-600 text-white' + : doc.status === 'SIMULASI' + ? 'bg-amber-100 text-amber-900' + : 'bg-rose-600 text-white' }`} > {doc.status} @@ -4288,17 +4423,17 @@ export default function NexoraApp() {
- Generate Official Tax Billing Code ({cfg.authorityShort}) + Simulasi kode billing — integrasi belum tersedia ({cfg.authorityShort})
@@ -4425,6 +4560,10 @@ export default function NexoraApp() { ))}
+
+ Keluaran AI tidak diverifikasi dan prompt dapat dikirim ke Google Gemini jika API key dikonfigurasi. Jangan kirim data pribadi/rahasia. PERLU VERIFIKASI AHLI PAJAK. +
+ {/* Chat Messages */}
{chatHistory.map((m, idx) => ( @@ -4466,7 +4605,7 @@ export default function NexoraApp() {
- Menganalisis pajak {selectedEntity.legalName} ({cfg.authorityShort})... + Menghasilkan jawaban AI yang belum diverifikasi...
)} @@ -5147,35 +5286,35 @@ export default function NexoraApp() { {indInputMode === 'search_existing' && (
- Pilih dari daftar Pengurus / Founder / UBO terdaftar di Grup Nusantara: + Pilih contoh data demonstrasi; identitas dan hubungan UBO tidak diverifikasi:
@@ -5407,10 +5546,10 @@ export default function NexoraApp() {

- Hak Akses & Peran Karyawan Korporasi + Persona Demo (Bukan Kontrol Akses)

- Sesuai standar tata kelola perusahaan internasional, karyawan dengan posisi CEO/setara dari Perusahaan Induk (Holding) memiliki visibilitas penuh ke seluruh entitas. + Pilihan di bawah hanya mengubah tampilan simulasi. Peran pengguna individual dan pembatasan entitas belum dikonfigurasi.