diff --git a/backend/src/settlement/entities/user.entity.ts b/backend/src/settlement/entities/user.entity.ts new file mode 100644 index 0000000..3493229 --- /dev/null +++ b/backend/src/settlement/entities/user.entity.ts @@ -0,0 +1,13 @@ +import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; + +@Entity("users") +export class User { + @PrimaryGeneratedColumn("uuid") + id: string; + + @Column("jsonb", { default: {} }) + balances: Record; // asset → amount + + @Column({ type: "timestamp", nullable: true }) + snoozedUntil: Date | null; +} diff --git a/backend/src/settlement/settlement.controller.ts b/backend/src/settlement/settlement.controller.ts index f57e429..c8d11fd 100644 --- a/backend/src/settlement/settlement.controller.ts +++ b/backend/src/settlement/settlement.controller.ts @@ -1,3 +1,32 @@ +import { Router } from "express"; +import { generateSuggestions, snoozeSettlement, unsnoozeSettlement, verifySettlement } from "./settlement.service"; + +const router = Router(); + +router.get("/suggestions", async (req, res) => { + const suggestions = await generateSuggestions(); + res.json(suggestions); +}); + +router.post("/snooze", async (req, res) => { + const { userId, until } = req.body; + await snoozeSettlement(userId, new Date(until)); + res.json({ success: true }); +}); + +router.post("/unsnooze", async (req, res) => { + const { userId } = req.body; + await unsnoozeSettlement(userId); + res.json({ success: true }); +}); + +router.post("/verify", async (req, res) => { + const { user, asset, amount } = req.body; + const verified = await verifySettlement(user, asset, amount); + res.json({ verified }); +}); + +export default router; import { Controller, Get, Post, Put, Body, Param, Req } from "@nestjs/common"; import { SuggestionEngineService } from "./suggestion-engine.service"; import { SettlementService } from "./settlement.service"; diff --git a/backend/src/settlement/settlement.repository.ts b/backend/src/settlement/settlement.repository.ts new file mode 100644 index 0000000..10a7868 --- /dev/null +++ b/backend/src/settlement/settlement.repository.ts @@ -0,0 +1,22 @@ +import { AppDataSource } from "../data-source"; +import { User } from "../entities/user.entity"; + +export const userRepo = AppDataSource.getRepository(User); + +export async function findActiveParticipants(): Promise { + const now = new Date(); + return userRepo.find({ + where: [ + { snoozedUntil: null }, + { snoozedUntil: { $lt: now } } + ] + }); +} + +export async function updateSnooze(userId: string, untilDate: Date) { + await userRepo.update(userId, { snoozedUntil: untilDate }); +} + +export async function clearSnooze(userId: string) { + await userRepo.update(userId, { snoozedUntil: null }); +} diff --git a/backend/src/settlement/settlement.service.ts b/backend/src/settlement/settlement.service.ts index ab05e48..7fe7a7f 100644 --- a/backend/src/settlement/settlement.service.ts +++ b/backend/src/settlement/settlement.service.ts @@ -1,3 +1,41 @@ +import { findActiveParticipants, updateSnooze, clearSnooze } from "./settlement.repository"; +import { verifyBalance } from "../utils/stellarVerification"; +import { User } from "../entities/user.entity"; + +export interface SettlementSuggestion { + participants: string[]; + status: "partial" | "completed" | "invalid"; + details: string; +} + +export async function generateSuggestions(): Promise { + const participants = await findActiveParticipants(); + const suggestions: SettlementSuggestion[] = []; + + // Example deterministic logic + const total = participants.reduce((sum, u) => sum + Object.values(u.balances).reduce((a, b) => a + b, 0), 0); + + if (total === 0) { + suggestions.push({ participants: participants.map(p => p.id), status: "completed", details: "All balances settled" }); + } else if (total > 0) { + suggestions.push({ participants: participants.map(p => p.id), status: "partial", details: "Some balances remain unsettled" }); + } else { + suggestions.push({ participants: participants.map(p => p.id), status: "invalid", details: "Balances mismatch" }); + } + + return suggestions; +} + +export async function snoozeSettlement(userId: string, until: Date) { + await updateSnooze(userId, until); +} + +export async function unsnoozeSettlement(userId: string) { + await clearSnooze(userId); +} + +export async function verifySettlement(user: User, asset: string, amount: number): Promise { + return verifyBalance(user.id, asset, amount); import { Injectable, NotFoundException, diff --git a/backend/src/settlement/tests/settlement.service.spec.ts b/backend/src/settlement/tests/settlement.service.spec.ts new file mode 100644 index 0000000..f2da978 --- /dev/null +++ b/backend/src/settlement/tests/settlement.service.spec.ts @@ -0,0 +1,25 @@ +import { generateSuggestions, snoozeSettlement, unsnoozeSettlement } from "../settlement.service"; +import { userRepo } from "../settlement.repository"; + +describe("Settlement Service", () => { + it("should generate completed suggestion when balances net to zero", async () => { + // mock participants with zero balances + const suggestions = await generateSuggestions(); + expect(suggestions[0].status).toBe("completed"); + }); + + it("should persist snooze state", async () => { + const userId = "test-user"; + const until = new Date(Date.now() + 3600 * 1000); + await snoozeSettlement(userId, until); + const user = await userRepo.findOneBy({ id: userId }); + expect(user?.snoozedUntil).toEqual(until); + }); + + it("should clear snooze state", async () => { + const userId = "test-user"; + await unsnoozeSettlement(userId); + const user = await userRepo.findOneBy({ id: userId }); + expect(user?.snoozedUntil).toBeNull(); + }); +}); diff --git a/backend/src/utils/stellarVerification.ts b/backend/src/utils/stellarVerification.ts new file mode 100644 index 0000000..299dee5 --- /dev/null +++ b/backend/src/utils/stellarVerification.ts @@ -0,0 +1,9 @@ +import { Server } from "stellar-sdk"; + +const server = new Server("https://horizon-testnet.stellar.org"); + +export async function verifyBalance(accountId: string, asset: string, expected: number): Promise { + const account = await server.loadAccount(accountId); + const balance = account.balances.find(b => b.asset_code === asset); + return balance ? Number(balance.balance) >= expected : false; +}