Jacob Schmidt ff7ff0c4e5 Implement org credit line debt and bank repayment flow (#2)
## Summary

This finishes the org credit line workflow so it behaves like reserved treasury-backed credit instead of a simple member allowance.

## What changed

- reserve org funds immediately when a credit line is assigned
- track credit lines with:
  - approved amount
  - available amount
  - outstanding principal
  - interest rate
  - amount due
- consume reserved credit during store checkout without charging org funds a second time
- add credit line repayment through the bank app
- sync richer credit line state into org and bank payloads/UI
- keep legacy `amount` compatibility mapped to available credit for older consumers

## User-facing behavior

- assigning a credit line now reduces available org funds immediately
- spending on `credit_line` reduces available credit and creates debt with interest
- the bank app now shows outstanding credit debt and allows repayment from personal bank funds
- the org treasury view now shows reserved credit and outstanding due totals

## Validation

- `cargo fmt`
- `npm run build:webui`
- `cargo test -p forge-services --quiet`
- `cargo test -p forge-server --quiet`

## Follow-up checks

- validate in-game that assigning a credit line reduces org funds immediately
- validate store checkout with `credit_line` updates available credit and debt correctly
- validate bank repayment decreases player bank balance, increases org funds, and reduces amount due

Co-authored-by: Jacob Schmidt <innovativestudios@outlook.com>
Reviewed-on: #2
2026-04-02 16:50:38 -05:00

75 lines
2.2 KiB
JavaScript

/**
* Shared JavaScript for Map UI
* Provides common utilities and state management across all UI components
*/
window.mapUIState = {
layersPanelVisible: true,
sidePanelElement: null,
};
window.mapUI = {
formatGridCoordinate(value) {
return Math.round(Number(value) || 0)
.toString()
.padStart(4, "0");
},
formatPosition(position) {
const safePosition = Array.isArray(position) ? position : [0, 0, 0];
return `X: ${this.formatGridCoordinate(safePosition[0])} Y: ${this.formatGridCoordinate(safePosition[1])}`;
},
sendEvent(event, data) {
A3API.SendAlert(JSON.stringify({ event: event, data: data }));
},
updateCoordinates(x, y) {
const coordDisplay = document.getElementById("coordsDisplay");
if (coordDisplay) {
coordDisplay.textContent = this.formatPosition([x, y, 0]);
}
},
updateScale(scale) {
const scaleDisplay = document.getElementById("scaleDisplay");
if (scaleDisplay) {
scaleDisplay.textContent = `Scale: 1:${Math.round(scale)}`;
}
},
updateStatus(text) {
const statusText = document.getElementById("statusText");
if (statusText) {
statusText.textContent = text;
}
},
};
window.updateCoordinates = window.mapUI.updateCoordinates;
window.updateScale = window.mapUI.updateScale;
window.updateStatus = window.mapUI.updateStatus;
window.ForgeBridge = window.ForgeBridge || {
_handlers: {},
on(event, handler) {
this._handlers[event] = this._handlers[event] || [];
this._handlers[event].push(handler);
},
ready(payload) {
window.mapUI.sendEvent("cad::ready", payload || {});
return true;
},
receive(payload) {
if (!payload || typeof payload !== "object") {
return;
}
const handlers = this._handlers[payload.event] || [];
handlers.forEach((handler) => handler(payload.data || {}));
},
send(event, data) {
window.mapUI.sendEvent(event, data || {});
return true;
},
close(data) {
window.mapUI.sendEvent("map::close", data || {});
return true;
},
};