Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions android/app/src/main/java/com/sanctum/advocate/NativeAdvocateModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package com.sanctum.advocate

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.WritableMap
import com.sanctum.data.LegalCaseEntity
import com.sanctum.data.SanctumDatabase
import com.sanctum.data.WitnessEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONObject

class NativeAdvocateModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

override fun getName(): String {
return "NativeAdvocateModule"
}

private fun caseToMap(entity: LegalCaseEntity): WritableMap {
val map = Arguments.createMap()
map.putString("id", entity.id)
map.putString("type", entity.type)
map.putDouble("startedAt", entity.startedAt.toDouble()) // JS numbers are doubles
map.putString("coParentName", entity.coParentName)
if (entity.jurisdiction != null) {
map.putString("jurisdiction", entity.jurisdiction)
}
return map
}

@ReactMethod
fun listCases(promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
val db = SanctumDatabase.getInstance(reactApplicationContext)
val cases = db.legalCaseDao().listCases()
val array = Arguments.createArray()
cases.forEach { array.pushMap(caseToMap(it)) }
promise.resolve(array)
} catch (e: Exception) {
promise.reject("ADVOCATE_LIST_FAILED", e)
}
}
}

@ReactMethod
fun createCase(type: String, coParentName: String, startedAtMillis: Double, jurisdiction: String?, promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
val db = SanctumDatabase.getInstance(reactApplicationContext)
val entity = LegalCaseEntity(
type = type,
coParentName = coParentName,
startedAt = startedAtMillis.toLong(),
jurisdiction = jurisdiction
)
db.legalCaseDao().upsertCase(entity)
promise.resolve(caseToMap(entity))
} catch (e: Exception) {
promise.reject("ADVOCATE_CREATE_FAILED", e)
}
}
}

@ReactMethod
fun deleteCaseWithCascade(caseId: String, promise: Promise) {
CoroutineScope(Dispatchers.IO).launch {
try {
val db = SanctumDatabase.getInstance(reactApplicationContext)
val witnessDao = db.witnessEventDao()

db.runInTransaction {
// These DAOs are non-suspend for transaction safety in this block
db.paymentEventDao().clearCaseAssignments(caseId)
db.legalCaseDao().deleteCaseById(caseId)
}

// Log to Witness: user intentionally deleted a case
val details = JSONObject()
details.put("caseId", caseId)

val event = WitnessEvent(
kind = "ADVOCATE_CASE_DELETED",
subject = "legal_case:$caseId",
summary = "User deleted legal case and cleared Advocate tags.",
detailsJson = details.toString()
)
witnessDao.insert(event)

promise.resolve(null)
} catch (e: Exception) {
// Also log the failure
try {
val db = SanctumDatabase.getInstance(reactApplicationContext)
val details = JSONObject()
details.put("caseId", caseId)
details.put("error", e.message ?: "unknown")

val event = WitnessEvent(
kind = "ADVOCATE_CASE_DELETE_ERROR",
subject = "legal_case:$caseId",
summary = "Failed to delete legal case $caseId",
detailsJson = details.toString()
)
db.witnessEventDao().insert(event)
} catch (_: Exception) {
// swallow secondary errors
}

promise.reject("ADVOCATE_DELETE_FAILED", e)
}
}
}
}
20 changes: 20 additions & 0 deletions android/app/src/main/java/com/sanctum/data/LegalCaseDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sanctum.data

import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert

@Dao
interface LegalCaseDao {
@Query("SELECT * FROM legal_cases")
suspend fun listCases(): List<LegalCaseEntity>

@Query("SELECT * FROM legal_cases WHERE id = :caseId")
suspend fun getCase(caseId: String): LegalCaseEntity?

@Upsert
suspend fun upsertCase(legalCase: LegalCaseEntity)

@Query("DELETE FROM legal_cases WHERE id = :caseId")
fun deleteCaseById(caseId: String): Int
}
14 changes: 14 additions & 0 deletions android/app/src/main/java/com/sanctum/data/LegalCaseEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.sanctum.data

import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.UUID

@Entity(tableName = "legal_cases")
data class LegalCaseEntity(
@PrimaryKey val id: String = UUID.randomUUID().toString(),
val type: String, // "divorce" | "custody" | "both"
val startedAt: Long, // millis
val coParentName: String, // display name only
val jurisdiction: String? = null
)
16 changes: 16 additions & 0 deletions android/app/src/main/java/com/sanctum/data/PaymentEventDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.sanctum.data

import androidx.room.Dao
import androidx.room.Query

@Dao
interface PaymentEventDao {
@Query("""
UPDATE payment_events
SET case_id = NULL,
legal_category = NULL,
case_relevance_score = 0
WHERE case_id = :caseId
""")
fun clearCaseAssignments(caseId: String)
}
27 changes: 27 additions & 0 deletions android/app/src/main/java/com/sanctum/data/PaymentEventEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.sanctum.data

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "payment_events")
data class PaymentEventEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,

val vendor_raw: String,
val vendor_normalized: String,
val amount_cents: Long?,
val currency: String?,
val seen_ts: Long,
val source_app: String,
val channel: String, // "notification"

// Extra for Advocate / Bursar
val method: String? = null, // "zelle", "venmo", "card"
val direction: String = "outgoing", // Advocate cares most about outgoing support
val counterparty_name: String? = null,

// Advocate legal overlay
val case_id: String? = null,
val legal_category: String? = null, // "support_payment", "potential_support", etc.
val case_relevance_score: Int = 0
)
41 changes: 41 additions & 0 deletions android/app/src/main/java/com/sanctum/data/SanctumDatabase.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.sanctum.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(
entities = [
LegalCaseEntity::class,
PaymentEventEntity::class,
WitnessEvent::class,
SubscriptionEntity::class
],
version = 1,
exportSchema = false
)
abstract class SanctumDatabase : RoomDatabase() {

abstract fun legalCaseDao(): LegalCaseDao
abstract fun paymentEventDao(): PaymentEventDao
abstract fun witnessEventDao(): WitnessEventDao
// SubscriptionDao would go here, but I don't need to implement it for this task, just the entity registration

companion object {
@Volatile
private var INSTANCE: SanctumDatabase? = null

fun getInstance(context: Context): SanctumDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
SanctumDatabase::class.java,
"sanctum_database"
).build()
INSTANCE = instance
instance
}
}
}
}
35 changes: 35 additions & 0 deletions android/app/src/main/java/com/sanctum/data/SubscriptionEntity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.sanctum.data

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "subscriptions")
data class SubscriptionEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0L,

// Core
val vendor: String,
val vendor_normalized: String = vendor.lowercase(),
val last_amount_cents: Long? = null,
val currency: String? = "USD",
val first_seen_ts: Long,
val last_seen_ts: Long,
val occurrences: Int,

val status: String = "unknown", // "active" | "trial" | "unknown"
val risk: String = "unknown", // "high" | "low" | "unknown"
val risk_score: Int = 0, // 0–100

// Actions / UX
val ignored: Boolean = false,
val package_name: String? = null,
val website_url: String? = null,
val next_expected_charge_ts: Long? = null,
val remind_before_days: Int? = null,

// Foreman metadata
val vertical: String? = null, // "contractor" | null
val category: String? = null, // ContractorCategory.code
val is_job_costable: Boolean = false,
val gl_hint: String? = null
)
14 changes: 14 additions & 0 deletions android/app/src/main/java/com/sanctum/data/WitnessEvent.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.sanctum.data

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "witness_events")
data class WitnessEvent(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val at: Long = System.currentTimeMillis(),
val kind: String, // e.g., "BURSAR_RISK_CHANGED", "ADVOCATE_SUPPORT_TAGGED"
val subject: String?, // "subscription:42", "case:CASE-123", etc.
val summary: String,
val detailsJson: String? = null // JSON for context payload
)
10 changes: 10 additions & 0 deletions android/app/src/main/java/com/sanctum/data/WitnessEventDao.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.sanctum.data

import androidx.room.Dao
import androidx.room.Insert

@Dao
interface WitnessEventDao {
@Insert
suspend fun insert(event: WitnessEvent)
}
28 changes: 28 additions & 0 deletions app/NativeAdvocateModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NativeModules } from 'react-native';

export type LegalCaseType = 'divorce' | 'custody' | 'both';

export type LegalCase = {
id: string;
type: LegalCaseType;
startedAt: number;
coParentName: string;
jurisdiction?: string | null;
};

type NativeAdvocateModuleType = {
listCases(): Promise<LegalCase[]>;
createCase(
type: LegalCaseType,
coParentName: string,
startedAtMillis: number,
jurisdiction?: string
): Promise<LegalCase>;
deleteCaseWithCascade(caseId: string): Promise<void>;
};

const { NativeAdvocateModule } = NativeModules as {
NativeAdvocateModule: NativeAdvocateModuleType;
};

export default NativeAdvocateModule;
Loading