diff --git a/platforms/eCurrency-api/src/controllers/CurrencyController.ts b/platforms/eCurrency-api/src/controllers/CurrencyController.ts
index 5b33c102..61b9f1b8 100644
--- a/platforms/eCurrency-api/src/controllers/CurrencyController.ts
+++ b/platforms/eCurrency-api/src/controllers/CurrencyController.ts
@@ -15,7 +15,7 @@ export class CurrencyController {
return res.status(401).json({ error: "Authentication required" });
}
- const { name, description, groupId, allowNegative } = req.body;
+ const { name, description, groupId, allowNegative, maxNegativeBalance } = req.body;
if (!name || !groupId) {
return res.status(400).json({ error: "Name and groupId are required" });
@@ -26,6 +26,7 @@ export class CurrencyController {
groupId,
req.user.id,
allowNegative || false,
+ maxNegativeBalance ?? null,
description
);
@@ -36,6 +37,7 @@ export class CurrencyController {
ename: currency.ename,
groupId: currency.groupId,
allowNegative: currency.allowNegative,
+ maxNegativeBalance: currency.maxNegativeBalance,
createdBy: currency.createdBy,
createdAt: currency.createdAt,
updatedAt: currency.updatedAt,
@@ -58,6 +60,7 @@ export class CurrencyController {
ename: currency.ename,
groupId: currency.groupId,
allowNegative: currency.allowNegative,
+ maxNegativeBalance: currency.maxNegativeBalance,
createdBy: currency.createdBy,
createdAt: currency.createdAt,
updatedAt: currency.updatedAt,
@@ -84,6 +87,7 @@ export class CurrencyController {
ename: currency.ename,
groupId: currency.groupId,
allowNegative: currency.allowNegative,
+ maxNegativeBalance: currency.maxNegativeBalance,
createdBy: currency.createdBy,
createdAt: currency.createdAt,
updatedAt: currency.updatedAt,
@@ -105,6 +109,7 @@ export class CurrencyController {
ename: currency.ename,
groupId: currency.groupId,
allowNegative: currency.allowNegative,
+ maxNegativeBalance: currency.maxNegativeBalance,
createdBy: currency.createdBy,
createdAt: currency.createdAt,
updatedAt: currency.updatedAt,
@@ -144,5 +149,47 @@ export class CurrencyController {
res.status(500).json({ error: "Internal server error" });
}
};
+
+ updateMaxNegativeBalance = async (req: Request, res: Response) => {
+ try {
+ if (!req.user) {
+ return res.status(401).json({ error: "Authentication required" });
+ }
+
+ const { id } = req.params;
+ const { value } = req.body;
+
+ // Allow null to clear; otherwise must be a number
+ const parsedValue = value === null || value === undefined ? null : Number(value);
+ if (parsedValue !== null && Number.isNaN(parsedValue)) {
+ return res.status(400).json({ error: "Invalid value for maxNegativeBalance" });
+ }
+
+ const updated = await this.currencyService.updateMaxNegativeBalance(
+ id,
+ parsedValue,
+ req.user.id
+ );
+
+ res.status(200).json({
+ id: updated.id,
+ name: updated.name,
+ description: updated.description,
+ ename: updated.ename,
+ groupId: updated.groupId,
+ allowNegative: updated.allowNegative,
+ maxNegativeBalance: updated.maxNegativeBalance,
+ createdBy: updated.createdBy,
+ createdAt: updated.createdAt,
+ updatedAt: updated.updatedAt,
+ });
+ } catch (error: any) {
+ console.error("Error updating max negative balance:", error);
+ if (error.message.includes("Only group admins") || error.message.includes("not found") || error.message.includes("Cannot set max negative") || error.message.includes("Max negative")) {
+ return res.status(400).json({ error: error.message });
+ }
+ res.status(500).json({ error: "Internal server error" });
+ }
+ };
}
diff --git a/platforms/eCurrency-api/src/database/entities/Currency.ts b/platforms/eCurrency-api/src/database/entities/Currency.ts
index 385f786a..20e015d1 100644
--- a/platforms/eCurrency-api/src/database/entities/Currency.ts
+++ b/platforms/eCurrency-api/src/database/entities/Currency.ts
@@ -36,6 +36,9 @@ export class Currency {
@Column({ default: false })
allowNegative!: boolean;
+ @Column("decimal", { precision: 18, scale: 2, nullable: true })
+ maxNegativeBalance!: number | null;
+
@Column()
createdBy!: string;
diff --git a/platforms/eCurrency-api/src/database/migrations/1765784749012-migration.ts b/platforms/eCurrency-api/src/database/migrations/1765784749012-migration.ts
new file mode 100644
index 00000000..e577c144
--- /dev/null
+++ b/platforms/eCurrency-api/src/database/migrations/1765784749012-migration.ts
@@ -0,0 +1,14 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class Migration1765784749012 implements MigrationInterface {
+ name = 'Migration1765784749012'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "currencies" ADD "maxNegativeBalance" numeric(18,2)`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE "currencies" DROP COLUMN "maxNegativeBalance"`);
+ }
+
+}
diff --git a/platforms/eCurrency-api/src/index.ts b/platforms/eCurrency-api/src/index.ts
index 3ec4247c..5a052bdc 100644
--- a/platforms/eCurrency-api/src/index.ts
+++ b/platforms/eCurrency-api/src/index.ts
@@ -112,6 +112,7 @@ app.get("/api/currencies", currencyController.getAllCurrencies);
app.get("/api/currencies/:id", currencyController.getCurrencyById);
app.get("/api/currencies/group/:groupId", currencyController.getCurrenciesByGroup);
app.post("/api/currencies/:id/mint", authGuard, currencyController.mintCurrency);
+app.patch("/api/currencies/:id/max-negative", authGuard, currencyController.updateMaxNegativeBalance);
// Ledger routes
app.get("/api/ledger/balance", authGuard, ledgerController.getBalance);
diff --git a/platforms/eCurrency-api/src/services/CurrencyService.ts b/platforms/eCurrency-api/src/services/CurrencyService.ts
index afcb931e..650d9969 100644
--- a/platforms/eCurrency-api/src/services/CurrencyService.ts
+++ b/platforms/eCurrency-api/src/services/CurrencyService.ts
@@ -22,6 +22,7 @@ export class CurrencyService {
groupId: string,
createdBy: string,
allowNegative: boolean = false,
+ maxNegativeBalance: number | null = null,
description?: string
): Promise {
// Verify user is group admin
@@ -40,6 +41,7 @@ export class CurrencyService {
groupId,
createdBy,
allowNegative,
+ maxNegativeBalance,
});
const savedCurrency = await this.currencyRepository.save(currency);
@@ -82,6 +84,38 @@ export class CurrencyService {
});
}
+ async updateMaxNegativeBalance(
+ currencyId: string,
+ value: number | null,
+ requestedBy: string
+ ): Promise {
+ const currency = await this.getCurrencyById(currencyId);
+ if (!currency) {
+ throw new Error("Currency not found");
+ }
+
+ const isAdmin = await this.groupService.isGroupAdmin(currency.groupId, requestedBy);
+ if (!isAdmin) {
+ throw new Error("Only group admins can update max negative balance");
+ }
+
+ if (!currency.allowNegative) {
+ throw new Error("Cannot set max negative balance when negative balances are not allowed");
+ }
+
+ if (value !== null) {
+ if (Number.isNaN(value)) {
+ throw new Error("Invalid max negative balance value");
+ }
+ if (value > 0) {
+ throw new Error("Max negative balance must be zero or negative");
+ }
+ }
+
+ currency.maxNegativeBalance = value;
+ return await this.currencyRepository.save(currency);
+ }
+
async mintCurrency(
currencyId: string,
amount: number,
diff --git a/platforms/eCurrency-api/src/services/LedgerService.ts b/platforms/eCurrency-api/src/services/LedgerService.ts
index 6cbae4f3..0f0b6422 100644
--- a/platforms/eCurrency-api/src/services/LedgerService.ts
+++ b/platforms/eCurrency-api/src/services/LedgerService.ts
@@ -40,10 +40,13 @@ export class LedgerService {
senderAccountId?: string,
senderAccountType?: AccountType,
receiverAccountId?: string,
- receiverAccountType?: AccountType
+ receiverAccountType?: AccountType,
+ existingBalance?: number
): Promise {
// Get current balance
- const currentBalance = await this.getAccountBalance(currencyId, accountId, accountType);
+ const currentBalance = existingBalance !== undefined
+ ? existingBalance
+ : await this.getAccountBalance(currencyId, accountId, accountType);
// Calculate new balance
const newBalance = type === LedgerType.CREDIT
@@ -90,11 +93,17 @@ export class LedgerService {
throw new Error("Currency not found");
}
- // Only check balance if negative balances are not allowed
- if (!currency.allowNegative) {
- const currentBalance = await this.getAccountBalance(currencyId, fromAccountId, fromAccountType);
- if (currentBalance < amount) {
- throw new Error("Insufficient balance. This currency does not allow negative balances.");
+ const currentBalance = await this.getAccountBalance(currencyId, fromAccountId, fromAccountType);
+
+ // Validate debit bounds
+ if (!currency.allowNegative && currentBalance < amount) {
+ throw new Error("Insufficient balance. This currency does not allow negative balances.");
+ }
+
+ if (currency.allowNegative && currency.maxNegativeBalance !== null && currency.maxNegativeBalance !== undefined) {
+ const newBalance = currentBalance - amount;
+ if (newBalance < Number(currency.maxNegativeBalance)) {
+ throw new Error(`Insufficient balance. This currency allows negative balances down to ${currency.maxNegativeBalance}.`);
}
}
@@ -110,7 +119,8 @@ export class LedgerService {
fromAccountId, // sender
fromAccountType, // sender type
toAccountId, // receiver
- toAccountType // receiver type
+ toAccountType, // receiver type
+ currentBalance
);
// Create credit entry (to receiver's account)
diff --git a/platforms/eCurrency/client/src/pages/currency-detail.tsx b/platforms/eCurrency/client/src/pages/currency-detail.tsx
index 01f61523..1a5c149f 100644
--- a/platforms/eCurrency/client/src/pages/currency-detail.tsx
+++ b/platforms/eCurrency/client/src/pages/currency-detail.tsx
@@ -1,4 +1,4 @@
-import { useQuery } from "@tanstack/react-query";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useLocation, useRoute } from "wouter";
import { apiClient } from "../lib/apiClient";
import { useAuth } from "../hooks/useAuth";
@@ -15,12 +15,17 @@ export default function CurrencyDetail() {
const [, params] = useRoute("/currency/:currencyId");
const [, setLocation] = useLocation();
const { user } = useAuth();
+ const queryClient = useQueryClient();
const [transferOpen, setTransferOpen] = useState(false);
const [mintOpen, setMintOpen] = useState(false);
const [selectedTransactionId, setSelectedTransactionId] = useState(null);
const [transactionOffset, setTransactionOffset] = useState(0);
const [allTransactions, setAllTransactions] = useState([]);
const PAGE_SIZE = 10;
+ const MAX_NEGATIVE_SLIDER = 1_000_000;
+ const [maxNegativeInput, setMaxNegativeInput] = useState("");
+ const [maxNegativeSaving, setMaxNegativeSaving] = useState(false);
+ const [maxNegativeError, setMaxNegativeError] = useState(null);
// Load account context from localStorage
const [accountContext, setAccountContext] = useState<{ type: "user" | "group"; id: string } | null>(() => {
@@ -48,6 +53,16 @@ export default function CurrencyDetail() {
enabled: !!currencyId,
});
+ useEffect(() => {
+ if (currency) {
+ if (currency.maxNegativeBalance !== null && currency.maxNegativeBalance !== undefined) {
+ setMaxNegativeInput(Math.abs(Number(currency.maxNegativeBalance)).toString());
+ } else {
+ setMaxNegativeInput("");
+ }
+ }
+ }, [currency]);
+
const { data: accountDetails } = useQuery({
queryKey: ["accountDetails", currencyId, accountContext],
queryFn: async () => {
@@ -177,6 +192,40 @@ export default function CurrencyDetail() {
const isAdminOfCurrency = currency && groups?.some((g: any) => g.id === currency.groupId && g.isAdmin);
+ const saveMaxNegative = async () => {
+ if (!currencyId) return;
+ setMaxNegativeError(null);
+ setMaxNegativeSaving(true);
+ try {
+ const trimmed = maxNegativeInput.trim();
+ const isClearing = trimmed === "";
+ let payloadValue: number | null = null;
+
+ if (!isClearing) {
+ const magnitude = parseFloat(trimmed);
+ if (Number.isNaN(magnitude) || magnitude < 0) {
+ setMaxNegativeError("Enter a valid non-negative number.");
+ setMaxNegativeSaving(false);
+ return;
+ }
+ // Store as negative (or zero)
+ payloadValue = magnitude === 0 ? 0 : -Math.abs(magnitude);
+ }
+
+ await apiClient.patch(`/api/currencies/${currencyId}/max-negative`, {
+ value: payloadValue,
+ });
+
+ await queryClient.invalidateQueries({ queryKey: ["currency", currencyId] });
+ await queryClient.invalidateQueries({ queryKey: ["accountDetails", currencyId, accountContext] });
+ } catch (error: any) {
+ const message = error?.response?.data?.error || error?.message || "Failed to update max negative balance";
+ setMaxNegativeError(message);
+ } finally {
+ setMaxNegativeSaving(false);
+ }
+ };
+
if (!currencyId) {
return Currency not found
;
}
@@ -240,6 +289,16 @@ export default function CurrencyDetail() {
{currency.allowNegative ? "Yes" : "No"}
+
+
Max Negative Balance
+
+ {currency.allowNegative
+ ? (currency.maxNegativeBalance !== null && currency.maxNegativeBalance !== undefined
+ ? Number(currency.maxNegativeBalance).toLocaleString()
+ : "No cap")
+ : "Not applicable"}
+
+
Total Currency Supply
@@ -257,6 +316,66 @@ export default function CurrencyDetail() {
)}
+ {/* Max Negative Control - only for admins when negatives are allowed */}
+ {currency && currency.allowNegative && isAdminOfCurrency && accountContext?.type === "group" && accountContext.id === currency.groupId && (
+
+
Set max negative balance
+
+ Limit how far any account can go negative for this currency. Leave blank for no cap.
+
+
+
setMaxNegativeInput(e.target.value)}
+ className="w-full"
+ />
+
+
+
+
setMaxNegativeInput(e.target.value)}
+ placeholder="Leave blank for no cap"
+ className="w-full px-4 py-2 border rounded-lg"
+ />
+
+ Saved as negative value: {maxNegativeInput === "" ? "No cap" : `-${Math.abs(Number(maxNegativeInput) || 0).toLocaleString()}`}
+
+
+
+
+
+
+
+ {maxNegativeError && (
+
+ {maxNegativeError}
+
+ )}
+
+
+ )}
+
{/* Transactions */}