Xpensio, kurumsal masraf yönetimi için geliştirilmiş çok kiracılı (multi-tenant) bir SaaS platformudur. Monorepo yapısında organize edilmiş olup pnpm workspace ile yönetilmektedir. Platform; NestJS tabanlı backend API, Next.js tabanlı web uygulaması, Flutter tabanlı mobil uygulama ve pazarlama odaklı bir landing sayfasından oluşmaktadır.
Framework: NestJS 10 (Node.js)
Port: 3001
ORM: Prisma 5.8
DB: PostgreSQL 15
Cache: Redis (opsiyonel)
URL: api.xpensioapp.com
Framework: Next.js 14 App Router
Port: 3000
State: Zustand
UI: Tailwind CSS
Test: Playwright E2E
URL: app.xpensioapp.com
Framework: Flutter 3
Platform: Android + iOS
State: Provider
Push: Firebase Messaging
Kamera: OCR ile fatura tarama
Framework: Next.js 14
i18n: TR/EN
Fiyatlandırma: €10/€6 per-user
AI Chat: Yerleşik widget
URL: xpensioapp.com
| Bileşen | Platform | URL / Adres | Notlar |
|---|---|---|---|
| Backend API | Hetzner VPS (Docker Compose) | https://api.xpensioapp.com | Port 3001, Nginx reverse proxy |
| PostgreSQL | Hetzner VPS (Docker) | Internal: postgres:5432 | Persistent volume mount |
| Web App | Vercel | https://app.xpensioapp.com | Auto-deploy from main branch |
| Landing | Vercel | https://xpensioapp.com | TR/EN, SEO optimized |
| CDN / SSL | Cloudflare | — | DDoS protection, SSL termination |
| SMTP (Resend/SendGrid uyumlu) | SMTP_HOST env var | Nodemailer ile gönderim | |
| Push Notifications | Firebase Cloud Messaging | Google Cloud | Firebase Admin SDK |
| OCR (Primary) | Google Cloud | Gemini 2.5 Flash API | Fatura görüntü analizi |
| OCR (Fallback) | OpenAI | GPT-4o Vision API | Gemini başarısız olursa |
| OCR (Local Fallback) | Sunucu içi | Tesseract.js | Her iki API de başarısızsa |
xpensioapp/ # Monorepo kök dizini
├── apps/
│ ├── backend/ # NestJS 10 REST API
│ │ ├── src/
│ │ │ ├── auth/ # JWT auth, login, şifre sıfırlama, change-password
│ │ │ │ ├── auth.controller.ts
│ │ │ │ ├── auth.service.ts
│ │ │ │ ├── jwt.strategy.ts
│ │ │ │ └── guards/ # JwtAuthGuard, RolesGuard
│ │ │ ├── users/ # Kullanıcı CRUD, Excel bulk import
│ │ │ │ ├── users.controller.ts
│ │ │ │ ├── users.service.ts
│ │ │ │ └── dto/
│ │ │ ├── expenses/ # Masraf state machine, export
│ │ │ │ ├── expenses.controller.ts
│ │ │ │ ├── expenses.service.ts
│ │ │ │ └── dto/
│ │ │ ├── receipts/ # OCR pipeline (Gemini → GPT-4o → Tesseract)
│ │ │ │ ├── receipts.controller.ts
│ │ │ │ └── receipts.service.ts
│ │ │ ├── approvals/ # Onay/red işlemleri
│ │ │ ├── notifications/ # Push + email notification
│ │ │ ├── fraud-detection/ # SHA-256 hash + Gemini AI fraud scoring
│ │ │ ├── mail/ # Nodemailer SMTP servis
│ │ │ └── identity/ # ERP adaptörleri
│ │ │ └── adapters/ # SapAdapter, NoneAdapter
│ │ ├── prisma/
│ │ │ ├── schema.prisma # Veritabanı şeması
│ │ │ ├── migrations/ # Prisma migration dosyaları
│ │ │ └── seed-demo.js # Demo veri seed scripti
│ │ ├── Dockerfile
│ │ └── .env # Backend env vars (gitignore'da)
│ │
│ ├── web/ # Next.js 14 App Router
│ │ ├── src/
│ │ │ ├── app/ # Next.js pages (App Router)
│ │ │ │ ├── (auth)/ # Login, register, forgot-password
│ │ │ │ ├── dashboard/ # Ana uygulama sayfaları
│ │ │ │ │ ├── expenses/ # Masraf liste ve detay
│ │ │ │ │ ├── approvals/ # Manager/CFO onay ekranı
│ │ │ │ │ ├── reports/ # Finance raporları + Excel export
│ │ │ │ │ ├── users/ # Admin kullanıcı yönetimi
│ │ │ │ │ └── settings/ # Profil, şifre değiştirme
│ │ │ │ └── api/ # Next.js API routes
│ │ │ ├── components/ # React bileşenleri
│ │ │ └── lib/
│ │ │ ├── api.ts # Axios API istemcisi
│ │ │ └── store.ts # Zustand global state
│ │ ├── e2e/ # Playwright E2E testleri
│ │ │ ├── helpers.ts # Ortak mock yardımcıları
│ │ │ └── production/ # Production E2E test suite
│ │ └── public/
│ │ └── sw.js # Service Worker (PWA)
│ │
│ └── mobile/ # Flutter 3
│ └── expense_mobile/
│ ├── lib/
│ │ ├── main.dart
│ │ ├── screens/ # UI ekranları
│ │ │ ├── login_screen.dart
│ │ │ ├── home_screen.dart
│ │ │ ├── expense_list_screen.dart
│ │ │ ├── expense_form_screen.dart
│ │ │ ├── scan_receipt_screen.dart
│ │ │ ├── approvals_screen.dart
│ │ │ ├── companies_screen.dart
│ │ │ └── profile_screen.dart
│ │ ├── services/
│ │ │ ├── api_service.dart
│ │ │ └── auth_service.dart
│ │ └── models/ # Dart veri modelleri
│ ├── android/
│ ├── ios/
│ └── pubspec.yaml
│
└── website/ # Landing page (Next.js 14)
├── src/
│ ├── app/ # Sayfalar
│ ├── components/
│ │ ├── Pricing.tsx # Fiyatlandırma (€10/€6)
│ │ └── AIChatWidget.tsx # AI chat widget
│ └── i18n.ts # TR/EN çeviriler
└── public/
@nestjs/core — NestJS framework çekirdeği@prisma/client — DB ORM istemcisi@nestjs/jwt — JWT token yönetimi@nestjs/throttler — Rate limitingclass-validator — DTO doğrulamabcryptjs — Şifre hashlemenodemailer — SMTP email gönderimifirebase-admin — Push notificationxlsx — Excel dosya üretimi/okumatesseract.js — Yerel OCR fallback@google/generative-ai — Gemini APInext 14 — App Router frameworkzustand — Lightweight state managementaxios — HTTP istemcisitailwindcss — Utility-first CSS@playwright/test — E2E test frameworkshare_plus — Dosya paylaşımıopen_filex — Dosya açmafirebase_messaging — Push notificationsimage_picker — Kamera/galerihttp — API istekleriXpensio, PostgreSQL 15 üzerinde çalışan ve Prisma 5.8 ile yönetilen ilişkisel bir veritabanı şemasına sahiptir. Tüm veriler orgId alanı üzerinden çok kiracılı (multi-tenant) olarak izole edilmektedir.
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| id | String (cuid) | Evet | Birincil anahtar, otomatik üretilir |
| String | Evet | Benzersiz, login için kullanılır | |
| name | String | Evet | Kullanıcının tam adı |
| passwordHash | String | Evet | bcrypt ile hashlenmiş şifre |
| role | Enum | Evet | EMPLOYEE | MANAGER | FINANCE | ADMIN | SUPERUSER |
| orgId | String | Evet | Organizasyon izolasyonu için FK |
| subCompanyId | String | Hayır | Alt şirket ataması (ERP posting için) |
| grade | String | Hayır | Kullanıcı kademesi (onay limitleri için) |
| managerId | String | Hayır | Yönetici kullanıcı ID (self FK) |
| mustChangePassword | Boolean | Evet | İlk giriş zorunlu şifre değiştirme bayrağı |
| isApproved | Boolean | Evet | Admin tarafından onay durumu |
| isEmailConfirmed | Boolean | Evet | Email doğrulama durumu |
| fcmToken | String | Hayır | Firebase Cloud Messaging token |
| passwordResetToken | String | Hayır | SHA-256 hash'lenmiş reset token |
| passwordResetExpiry | DateTime | Hayır | Reset token geçerlilik süresi (1 saat) |
| createdAt | DateTime | Evet | Kayıt tarihi |
| updatedAt | DateTime | Evet | Son güncelleme tarihi |
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| id | String (cuid) | Evet | Birincil anahtar |
| userId | String | Evet | Masrafı oluşturan kullanıcı FK |
| orgId | String | Evet | Multi-tenant izolasyon |
| amount | Decimal | Evet | Brüt tutar (OCR sonrası kilitlenir) |
| currency | String | Evet | ISO 4217: TRY, EUR, USD |
| taxAmount | Decimal | Hayır | KDV tutarı (OCR sonrası kilitlenir) |
| taxPercentageCode | String | Hayır | V0,V1,V2,V3,V4 (0,8,10,18,20%) |
| category | String | Evet | Masraf kategorisi (FOOD, TRAVEL vb.) |
| documentType | String | Hayır | INVOICE, RECEIPT, TICKET |
| expenseTypeCode | String | Hayır | ERP gönderimi için masraf tipi kodu |
| vehiclePlate | String | Hayır | Araç plakası (yakıt masrafları) |
| receiptNumber | String | Hayır | Fiş/fatura no (OCR sonrası kilitlenir) |
| status | Enum | Evet | DRAFT|SUBMITTED|PENDING_MANAGER|PENDING_CFO|PENDING_FINANCE|APPROVED|REJECTED|ERP_SENT |
| expenseDate | DateTime | Evet | Masraf tarihi (OCR sonrası kilitlenir) |
| projectCode | String | Hayır | Proje kodu (ERP entegrasyonu) |
| costCenter | String | Hayır | Maliyet merkezi |
| description | String | Hayır | Masraf açıklaması |
| fraudScore | Int | Hayır | 0-100 arası AI fraud risk skoru |
| receiptImageUrl | String | Hayır | Yüklenen fatura görsel URL |
| approvedBy | String | Hayır | Son onaylayan kullanıcı ID |
| rejectedBy | String | Hayır | Reddeden kullanıcı ID |
| rejectionReason | String | Hayır | Red gerekçesi |
| Model | Alanlar | Açıklama |
|---|---|---|
| Organization | id, name, erpType, sapBukrs, createdAt | Kiracı organizasyon. erpType: NONE | SAP_ECC | SAP_S4HANA. sapBukrs: ERP Şirket Kodu |
| SubCompany | id, orgId, name, sapBukrs, createdAt | Alt şirket. SAP posting için orgId+subCompanyId kombinasyonu zorunlu |
| RefreshToken | id, userId, token (SHA-256 hash), expiresAt, createdAt | Tüm session'ları yönetmek için DB'de saklanan refresh token'lar |
| Notification | id, userId, type, title, body, isRead, createdAt | Kullanıcı bildirim geçmişi. type: EXPENSE_SUBMITTED | EXPENSE_APPROVED | EXPENSE_REJECTED |
| ApprovalHistory | id, expenseId, userId, action, comment, createdAt | Onay/red geçmişi audit logu |
url = env("DATABASE_URL") sözdiziminde kırıcı değişiklik içermektedir. Üretim ortamında kesinlikle prisma@5.8.0 kullanılmalıdır. Docker entrypoint'te ./node_modules/.bin/prisma önce denenir, bulunamazsa npx prisma@5.8.0 fallback çalışır.
| Modül | Yenilik |
|---|---|
| SAP HR | Kullanıcı masraf yeri (cost center) günlük SAP userlist sync ile otomatik güncelleniyor — fallback zinciri: OPHCODE / KOSTL / COSTCENTER / RELID |
| SAP OAuth2 | Dinamik token akışı: POST /sap-auth/token, SapJwtGuard, ABAP ZCL_XPENSIO_AUTH + ZXPENSIO_TOKEN; client_id/secret ZEXP_CFG_01 üzerinden yönetilir |
| SAP TLS | Setup wizard'da özel CA sertifikası (PEM) yükleme; Axios https.Agent({ ca }) ile tek yönlü TLS desteği |
| Setup Wizard | HR / Finance endpoint path'leri ayrı sekmede override edilebilir (ZEXP_CFG_01 muadili) |
| Expense Snapshot | Masraf girildiği anda snapshotCostCenter*, snapshotDepartment*, snapshotPosition*, snapshotUpperManager*, snapshotCapturedAt alanları kayıt altına alınır — sonradan kullanıcı verisi değişse de rapor doğru görünür |
| TESLIM_ALINDI | Yeni ExpenseStatus ara durumu. PDF rapor üzerine QR (xpensio://receive/<id>?t=<token>) basılır, finans mobil app'ten okur. Endpoint: POST /reports/:id/receive |
| Mobile | v1.4.0+40: mobile_scanner ile QR teslim alma ekranı, FINANCE/ADMIN rolüne özel |
| UserChangeHistory | Field-level diff izleme (department, manager, cost center, position, role, grade vb.). Kaynak: SAP_SYNC | ADMIN. GET /users/:id/history + admin UI "Değişiklik Geçmişi" sekmesi |
| Menü Görünürlüğü | Org Chart, Pozisyonlar, Şirketler, Masraf Yerleri, Setup, Fraud, Audit Logs → yalnızca super admin. Backend SuperAdminGuard + Sidebar superAdminOnly filtresi |
| Grade Limitleri | Şirket bazlı grade harcama limitleri (grade_policies.company_id) |
| Report Numbering | Org-wide tek artan rapor numarası (report_number_sequences org bazlı) |