Add credit line repayment to bank UI
- Wire bank client and server for credit line repayment requests - Show credit line balance and repay action in the banking view - Extend org/bank payloads and models with credit line fields
This commit is contained in:
parent
b8dd3ef651
commit
5ded3a60e5
@ -68,6 +68,11 @@ switch (_event) do {
|
||||
GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::repayCreditLine::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleRepayCreditLineRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::pin::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
|
||||
|
||||
@ -63,6 +63,13 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleRepayCreditLineRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _amount = floor (_data getOrDefault ["amount", 0]);
|
||||
[SRPC(bank,requestRepayCreditLine), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleHydrateResponse", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]], ["_event", "bank::hydrate", [""]]];
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -43,6 +43,9 @@
|
||||
requestDepositEarnings(payload) {
|
||||
return bridge.send("bank::depositEarnings::request", payload);
|
||||
},
|
||||
requestRepayCreditLine(payload) {
|
||||
return bridge.send("bank::repayCreditLine::request", payload);
|
||||
},
|
||||
requestRefresh() {
|
||||
return bridge.send("bank::refresh", {});
|
||||
},
|
||||
|
||||
@ -3,6 +3,13 @@
|
||||
|
||||
const defaultSession = {
|
||||
atmAuthorized: false,
|
||||
creditLine: {
|
||||
amountDue: 0,
|
||||
approvedAmount: 0,
|
||||
availableAmount: 0,
|
||||
interestRate: 0.1,
|
||||
outstandingPrincipal: 0,
|
||||
},
|
||||
mode: "bank",
|
||||
orgFunds: 0,
|
||||
orgName: "",
|
||||
|
||||
@ -89,6 +89,16 @@
|
||||
"Reference value pulled from the organization treasury.",
|
||||
session.orgFunds > 0 ? "success" : "",
|
||||
),
|
||||
metricCard(
|
||||
"Credit Due",
|
||||
formatCurrency(session.creditLine?.amountDue || 0),
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? `Outstanding principal ${formatCurrency(session.creditLine?.outstandingPrincipal || 0)} at ${Math.round(Number(session.creditLine?.interestRate || 0) * 100)}% interest.`
|
||||
: "No active credit repayment is currently due.",
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? "warning"
|
||||
: "",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -238,6 +248,63 @@
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Credit"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Repay Org Credit",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h(
|
||||
"p",
|
||||
{ className: "bank-card-copy" },
|
||||
Number(session.creditLine?.amountDue || 0) > 0
|
||||
? `Outstanding due ${formatCurrency(session.creditLine.amountDue || 0)}. Available reserved credit ${formatCurrency(session.creditLine.availableAmount || 0)}.`
|
||||
: "No repayment is currently due on the assigned organization credit line.",
|
||||
),
|
||||
h("input", {
|
||||
id: "bank-credit-line-amount",
|
||||
className: "bank-input",
|
||||
type: "number",
|
||||
min: "1",
|
||||
placeholder: "Enter repayment amount",
|
||||
}),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled:
|
||||
pending("repaycreditline") ||
|
||||
Number(session.creditLine?.amountDue || 0) <= 0,
|
||||
onClick: () => {
|
||||
const sent = actions.requestRepayCreditLine(
|
||||
readInputValue("bank-credit-line-amount"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-credit-line-amount");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("repaycreditline")
|
||||
? "Posting Repayment..."
|
||||
: "Repay Credit Line",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -130,6 +130,25 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function requestRepayCreditLine(amountValue) {
|
||||
const amount = normalizeAmount(amountValue);
|
||||
const bridge = BankApp.bridge;
|
||||
if (!bridge || typeof bridge.requestRepayCreditLine !== "function") {
|
||||
showNotice("error", "Credit repayment bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("repaycreditline");
|
||||
const sent = bridge.requestRepayCreditLine({ amount });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "Credit repayment bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function appendPinDigit(digit) {
|
||||
const nextDigit = String(digit || "").trim();
|
||||
if (!nextDigit) {
|
||||
@ -259,6 +278,7 @@
|
||||
requestAtmAmount,
|
||||
requestDeposit,
|
||||
requestDepositEarnings,
|
||||
requestRepayCreditLine,
|
||||
requestTransfer,
|
||||
requestWithdraw,
|
||||
selectAtmView,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -136,8 +136,18 @@
|
||||
|
||||
OrgPortal.store.setCreditLines((currentLines) => {
|
||||
const nextLine = {
|
||||
amount: payloadData.amount || 0,
|
||||
amount: payloadData.availableAmount || payloadData.amount || 0,
|
||||
amountDue: payloadData.amountDue || 0,
|
||||
approvedAmount:
|
||||
payloadData.approvedAmount ||
|
||||
payloadData.availableAmount ||
|
||||
payloadData.amount ||
|
||||
0,
|
||||
availableAmount:
|
||||
payloadData.availableAmount || payloadData.amount || 0,
|
||||
interestRate: payloadData.interestRate || 0.1,
|
||||
member: payloadData.memberName || "",
|
||||
outstandingPrincipal: payloadData.outstandingPrincipal || 0,
|
||||
uid: payloadData.memberUid || "",
|
||||
};
|
||||
const matchIndex = currentLines.findIndex(
|
||||
|
||||
@ -215,6 +215,15 @@ ${scopeSelector} .org-credit-line-empty {
|
||||
const allowTreasuryActions = getters.canManageTreasury();
|
||||
const activeTab = getTreasuryTab();
|
||||
const isMenuOpen = getTreasuryMenuOpen();
|
||||
const totalReserved = creditLines.reduce(
|
||||
(sum, line) =>
|
||||
sum + Number(line.availableAmount || line.amount || 0),
|
||||
0,
|
||||
);
|
||||
const totalDue = creditLines.reduce(
|
||||
(sum, line) => sum + Number(line.amountDue || 0),
|
||||
0,
|
||||
);
|
||||
const activeCreditLabel =
|
||||
creditLines.length === 1
|
||||
? "1 active credit line"
|
||||
@ -331,16 +340,59 @@ ${scopeSelector} .org-credit-line-empty {
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Amount",
|
||||
"Available",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
getters.formatCurrency(
|
||||
line.amount,
|
||||
line.availableAmount ||
|
||||
line.amount,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-member",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Amount Due",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
getters.formatCurrency(
|
||||
line.amountDue,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-member",
|
||||
},
|
||||
h(
|
||||
"span",
|
||||
{
|
||||
className:
|
||||
"org-credit-line-label",
|
||||
},
|
||||
"Interest",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
`${Math.round(Number(line.interestRate || 0) * 100)}%`,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -379,6 +431,34 @@ ${scopeSelector} .org-credit-line-empty {
|
||||
),
|
||||
h("strong", null, `${reputation}`),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Reserved Credit",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
getters.formatCurrency(totalReserved),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h(
|
||||
"span",
|
||||
{ className: "org-meta-label" },
|
||||
"Outstanding Due",
|
||||
),
|
||||
h(
|
||||
"strong",
|
||||
null,
|
||||
getters.formatCurrency(totalDue),
|
||||
),
|
||||
),
|
||||
),
|
||||
allowTreasuryActions
|
||||
? h(
|
||||
@ -432,7 +512,7 @@ ${scopeSelector} .org-credit-line-empty {
|
||||
"span",
|
||||
null,
|
||||
creditLines.length > 0
|
||||
? "Open the Credit Lines tab to review assigned members and amounts."
|
||||
? "Open the Credit Lines tab to review reserved balances, due amounts, and member exposure."
|
||||
: "Assign a credit line to create the first approved member limit.",
|
||||
),
|
||||
),
|
||||
|
||||
@ -49,3 +49,9 @@ PREP_RECOMPILE_END;
|
||||
|
||||
GVAR(BankStore) call ["depositEarnings", [_uid, _amount]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestRepayCreditLine), {
|
||||
params [["_uid", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
GVAR(BankStore) call ["repayCreditLine", [_uid, _amount]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
@ -48,7 +48,18 @@ GVAR(BankPayloadBuilder) = createHashMapObject [[
|
||||
["resolveOrgState", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
private _defaultState = createHashMapFromArray [["funds", 0], ["name", ""]];
|
||||
private _defaultCreditLine = createHashMapFromArray [
|
||||
["approvedAmount", 0],
|
||||
["availableAmount", 0],
|
||||
["outstandingPrincipal", 0],
|
||||
["interestRate", 0.1],
|
||||
["amountDue", 0]
|
||||
];
|
||||
private _defaultState = createHashMapFromArray [
|
||||
["funds", 0],
|
||||
["name", ""],
|
||||
["creditLine", _defaultCreditLine]
|
||||
];
|
||||
if (_uid isEqualTo "") exitWith { _defaultState };
|
||||
|
||||
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
|
||||
@ -61,7 +72,33 @@ GVAR(BankPayloadBuilder) = createHashMapObject [[
|
||||
};
|
||||
if (_org isEqualTo createHashMap) exitWith { _defaultState };
|
||||
|
||||
createHashMapFromArray [["funds", _org getOrDefault ["funds", 0]], ["name", _org getOrDefault ["name", ""]]]
|
||||
private _creditLines = _org getOrDefault ["credit_lines", createHashMap];
|
||||
if !(_creditLines isEqualType createHashMap) then {
|
||||
_creditLines = createHashMap;
|
||||
};
|
||||
|
||||
private _creditLine = _creditLines getOrDefault [_uid, createHashMap];
|
||||
if !(_creditLine isEqualType createHashMap) then {
|
||||
_creditLine = createHashMap;
|
||||
};
|
||||
|
||||
createHashMapFromArray [
|
||||
["funds", _org getOrDefault ["funds", 0]],
|
||||
["name", _org getOrDefault ["name", ""]],
|
||||
["creditLine", createHashMapFromArray [
|
||||
["approvedAmount", _creditLine getOrDefault [
|
||||
"approved_amount",
|
||||
_creditLine getOrDefault ["amount", 0]
|
||||
]],
|
||||
["availableAmount", _creditLine getOrDefault [
|
||||
"available_amount",
|
||||
_creditLine getOrDefault ["amount", 0]
|
||||
]],
|
||||
["outstandingPrincipal", _creditLine getOrDefault ["outstanding_principal", 0]],
|
||||
["interestRate", _creditLine getOrDefault ["interest_rate", 0.1]],
|
||||
["amountDue", _creditLine getOrDefault ["amount_due", 0]]
|
||||
]]
|
||||
]
|
||||
}],
|
||||
["buildTransferTargets", compileFinal {
|
||||
params [["_sourceUid", "", [""]]];
|
||||
@ -101,6 +138,7 @@ GVAR(BankPayloadBuilder) = createHashMapObject [[
|
||||
["mode", _session getOrDefault ["mode", "bank"]],
|
||||
["orgFunds", _orgState getOrDefault ["funds", 0]],
|
||||
["orgName", _orgState getOrDefault ["name", ""]],
|
||||
["creditLine", _orgState getOrDefault ["creditLine", createHashMap]],
|
||||
["playerName", _playerName],
|
||||
["transferTargets", _self call ["buildTransferTargets", [_uid]]],
|
||||
["uid", _uid]
|
||||
|
||||
@ -167,6 +167,83 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result set ["patch", _patch];
|
||||
_result
|
||||
}],
|
||||
["repayCreditLine", compileFinal {
|
||||
params [["_uid", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _amount <= 0 }) exitWith {
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Enter a valid repayment amount."]];
|
||||
false
|
||||
};
|
||||
|
||||
private _originalAccount = _self call ["loadHotBank", [_uid, false, ""]];
|
||||
if (_originalAccount isEqualTo createHashMap) then {
|
||||
_originalAccount = _self call ["loadHotBank", [_uid, true, ""]];
|
||||
};
|
||||
if (_originalAccount isEqualTo createHashMap) exitWith {
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank account could not be loaded."]];
|
||||
false
|
||||
};
|
||||
|
||||
private _checkoutContext = GVAR(BankPayloadBuilder) call ["buildCheckoutContext", ["bank", false]];
|
||||
private _previewEnvelope = _self call [
|
||||
"callHotBankEnvelope",
|
||||
[
|
||||
"bank:hot:charge_checkout",
|
||||
[_uid, str _amount, toJSON _checkoutContext]
|
||||
]
|
||||
];
|
||||
private _previewResult = _previewEnvelope getOrDefault ["data", createHashMap];
|
||||
private _bankPatch = _self call ["finalizeMutation", [_uid, _previewResult, false]];
|
||||
if (_bankPatch isEqualTo createHashMap) exitWith {
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _previewEnvelope getOrDefault ["error", "Credit repayment could not be funded from the bank account."]]];
|
||||
false
|
||||
};
|
||||
|
||||
private _nextAccount = _previewResult getOrDefault ["account", createHashMap];
|
||||
if (_nextAccount isEqualTo createHashMap) exitWith {
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", "Bank repayment preview returned an invalid account state."]];
|
||||
false
|
||||
};
|
||||
|
||||
private _overrideEnvelope = _self call [
|
||||
"callHotBankEnvelope",
|
||||
["bank:hot:override", [_uid, _self call ["toJSON", [_nextAccount]]]]
|
||||
];
|
||||
if ((_overrideEnvelope getOrDefault ["data", createHashMap]) isEqualTo createHashMap) exitWith {
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _overrideEnvelope getOrDefault ["error", "Credit repayment could not reserve bank funds."]]];
|
||||
false
|
||||
};
|
||||
|
||||
private _orgResult = EGVAR(org,OrgStore) call ["repayCreditLine", [_uid, _amount]];
|
||||
if !(_orgResult getOrDefault ["success", false]) exitWith {
|
||||
private _rollbackEnvelope = _self call [
|
||||
"callHotBankEnvelope",
|
||||
["bank:hot:override", [_uid, _self call ["toJSON", [_originalAccount]]]]
|
||||
];
|
||||
if ((_rollbackEnvelope getOrDefault ["data", createHashMap]) isEqualTo createHashMap) then {
|
||||
["ERROR", format ["Failed to roll back bank state for %1 after org credit repayment failure.", _uid]] call EFUNC(common,log);
|
||||
};
|
||||
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "error", _orgResult getOrDefault ["message", "Credit repayment failed."]]];
|
||||
false
|
||||
};
|
||||
|
||||
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _bankPatch]];
|
||||
GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", _orgResult getOrDefault ["message", format ["Repaid $%1 toward the organization credit line.", [_amount] call EFUNC(common,formatNumber)]]]];
|
||||
|
||||
private _orgPatch = _orgResult getOrDefault ["patch", createHashMap];
|
||||
if (_orgPatch isNotEqualTo createHashMap) then {
|
||||
{
|
||||
private _memberPlayer = [_x] call EFUNC(common,getPlayer);
|
||||
if (_memberPlayer isNotEqualTo objNull) then {
|
||||
[CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent);
|
||||
};
|
||||
} forEach (_orgResult getOrDefault ["memberUids", []]);
|
||||
};
|
||||
|
||||
_self call ["hydrateSession", [_uid, "", false]];
|
||||
true
|
||||
}],
|
||||
["deposit", compileFinal {
|
||||
params [["_uid", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
|
||||
@ -83,6 +83,34 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
|
||||
|
||||
_org set ["assets", _migratedAssets];
|
||||
|
||||
private _creditLines = _org getOrDefault ["credit_lines", createHashMap];
|
||||
if !(_creditLines isEqualType createHashMap) then {
|
||||
_creditLines = createHashMap;
|
||||
};
|
||||
|
||||
{
|
||||
if !(_y isEqualType createHashMap) then { continue; };
|
||||
|
||||
private _line = +_y;
|
||||
private _legacyAmount = _line getOrDefault ["amount", 0];
|
||||
private _approvedAmount = _line getOrDefault ["approved_amount", _legacyAmount];
|
||||
private _availableAmount = _line getOrDefault ["available_amount", _approvedAmount];
|
||||
private _outstandingPrincipal = _line getOrDefault ["outstanding_principal", 0];
|
||||
private _interestRate = _line getOrDefault ["interest_rate", 0.1];
|
||||
private _amountDue = _line getOrDefault ["amount_due", 0];
|
||||
|
||||
_line set ["uid", _line getOrDefault ["uid", _x]];
|
||||
_line set ["approved_amount", _approvedAmount];
|
||||
_line set ["available_amount", _availableAmount];
|
||||
_line set ["outstanding_principal", _outstandingPrincipal];
|
||||
_line set ["interest_rate", _interestRate];
|
||||
_line set ["amount_due", _amountDue];
|
||||
_line set ["amount", _availableAmount];
|
||||
_creditLines set [_x, _line];
|
||||
} forEach _creditLines;
|
||||
|
||||
_org set ["credit_lines", _creditLines];
|
||||
|
||||
_org
|
||||
}],
|
||||
["validate", compileFinal {
|
||||
@ -483,6 +511,43 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
|
||||
_result
|
||||
}],
|
||||
["repayCreditLine", compileFinal {
|
||||
params [["_requesterUid", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", ""],
|
||||
["patch", createHashMap],
|
||||
["memberUids", []]
|
||||
];
|
||||
|
||||
if (_requesterUid isEqualTo "" || { _amount <= 0 }) exitWith {
|
||||
_result set ["message", "A valid repayment amount is required."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
|
||||
private _orgID = _requesterActor getOrDefault ["organization", "default"];
|
||||
if (_orgID isEqualTo "") then { _orgID = "default"; };
|
||||
|
||||
private _context = createHashMapFromArray [
|
||||
["requesterUid", _requesterUid],
|
||||
["orgId", _orgID],
|
||||
["amount", _amount]
|
||||
];
|
||||
|
||||
private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:repay_credit_line", [toJSON _context]]];
|
||||
if (_envelope isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Unable to apply credit repayment."];
|
||||
_result
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", _envelope getOrDefault ["message", "Credit repayment posted."]];
|
||||
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
|
||||
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
|
||||
_result
|
||||
}],
|
||||
["buildPortalPayload", compileFinal {
|
||||
params [["_uid", "", [""]]];
|
||||
|
||||
|
||||
@ -134,10 +134,19 @@ GVAR(OrgPayloadBuilder) = createHashMapObject [[
|
||||
private _creditLinesList = [];
|
||||
{
|
||||
private _creditLineData = _y;
|
||||
private _availableAmount = _creditLineData getOrDefault [
|
||||
"available_amount",
|
||||
_creditLineData getOrDefault ["amount", 0]
|
||||
];
|
||||
_creditLinesList pushBack [
|
||||
["uid", _creditLineData getOrDefault ["uid", _x]],
|
||||
["member", _creditLineData getOrDefault ["name", "Unknown Member"]],
|
||||
["amount", _creditLineData getOrDefault ["amount", 0]]
|
||||
["approvedAmount", _creditLineData getOrDefault ["approved_amount", _availableAmount]],
|
||||
["availableAmount", _availableAmount],
|
||||
["outstandingPrincipal", _creditLineData getOrDefault ["outstanding_principal", 0]],
|
||||
["interestRate", _creditLineData getOrDefault ["interest_rate", 0.1]],
|
||||
["amountDue", _creditLineData getOrDefault ["amount_due", 0]],
|
||||
["amount", _availableAmount]
|
||||
];
|
||||
} forEach _creditLinesRaw;
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _budget = 50000;
|
||||
private _creditLine = 0;
|
||||
private _creditLineDue = 0;
|
||||
private _cashBalance = 0;
|
||||
private _bankBalance = 0;
|
||||
private _orgFunds = 0;
|
||||
@ -72,7 +73,11 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
if (_orgCreditLines isEqualType createHashMap) then {
|
||||
private _playerCreditLine = _orgCreditLines getOrDefault [_uid, createHashMap];
|
||||
if (_playerCreditLine isEqualType createHashMap) then {
|
||||
_creditLine = _playerCreditLine getOrDefault ["amount", 0];
|
||||
_creditLine = _playerCreditLine getOrDefault [
|
||||
"available_amount",
|
||||
_playerCreditLine getOrDefault ["amount", 0]
|
||||
];
|
||||
_creditLineDue = _playerCreditLine getOrDefault ["amount_due", 0];
|
||||
};
|
||||
};
|
||||
|
||||
@ -113,7 +118,10 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
["enabled", _creditLine > 0],
|
||||
["detail", [
|
||||
"No approved credit line is assigned to this member.",
|
||||
"Use the approved procurement credit line."
|
||||
format [
|
||||
"Use the approved procurement credit line. Outstanding due: $%1.",
|
||||
[_creditLineDue] call EFUNC(common,formatNumber)
|
||||
]
|
||||
] select (_creditLine > 0)]
|
||||
]
|
||||
];
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
|
||||
use arma_rs::Group;
|
||||
use forge_models::{
|
||||
HotOrgRecord, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext, OrgDisbandResult,
|
||||
HotOrgRecord, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext,
|
||||
OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandResult,
|
||||
OrgEnsureMemberContext, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, OrgLeaveResult,
|
||||
OrgRegisterContext,
|
||||
};
|
||||
@ -59,6 +60,7 @@ pub fn group() -> Group {
|
||||
.command("ensure_member", ensure_hot_org_member)
|
||||
.command("register", register_hot_org)
|
||||
.command("assign_credit_line", assign_credit_line_hot_org)
|
||||
.command("repay_credit_line", repay_credit_line_hot_org)
|
||||
.command("charge_checkout", charge_checkout_hot_org)
|
||||
.command("add_assets", add_assets_hot_org)
|
||||
.command("add_fleet", add_fleet_hot_org)
|
||||
@ -176,6 +178,20 @@ pub(crate) fn charge_checkout_hot_org(json_data: String) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn repay_credit_line_hot_org(json_data: String) -> String {
|
||||
let context: OrgCreditLineRepaymentContext = match serde_json::from_str(&json_data) {
|
||||
Ok(data) => data,
|
||||
Err(error) => return format!("Error: Invalid org credit repayment JSON: {}", error),
|
||||
};
|
||||
|
||||
match HOT_ORG_SERVICE.repay_credit_line(context) {
|
||||
Ok(result) => {
|
||||
serialize_result::<OrgCreditLineRepaymentResult>(&result, "org credit repayment result")
|
||||
}
|
||||
Err(error) => format!("Error: {}", error),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_assets_hot_org(context_json: String, assets_json: String) -> String {
|
||||
let context: OrgGrantContext = match serde_json::from_str(&context_json) {
|
||||
Ok(data) => data,
|
||||
|
||||
@ -363,6 +363,10 @@ fn route_command(
|
||||
expect_arg_count(function_name, &arguments, 1)?;
|
||||
Ok(org::assign_credit_line_hot_org(arguments[0].clone()))
|
||||
}
|
||||
"org:hot:repay_credit_line" => {
|
||||
expect_arg_count(function_name, &arguments, 1)?;
|
||||
Ok(org::repay_credit_line_hot_org(arguments[0].clone()))
|
||||
}
|
||||
"org:hot:charge_checkout" => {
|
||||
expect_arg_count(function_name, &arguments, 1)?;
|
||||
Ok(org::charge_checkout_hot_org(arguments[0].clone()))
|
||||
|
||||
@ -23,10 +23,11 @@ pub use cad::{
|
||||
pub use garage::{Garage, HitPoints, Vehicle};
|
||||
pub use locker::{Item, Locker};
|
||||
pub use org::{
|
||||
CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgAssetGrantSeed,
|
||||
OrgCheckoutContext, OrgCreditLineContext, OrgDisbandMemberResult, OrgDisbandResult,
|
||||
OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext,
|
||||
OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
|
||||
CreditLineSummary, DEFAULT_CREDIT_LINE_INTEREST_RATE, HotOrgRecord, MemberSummary, Org,
|
||||
OrgAssetEntry, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext,
|
||||
OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandMemberResult,
|
||||
OrgDisbandResult, OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext,
|
||||
OrgLeaveContext, OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
|
||||
};
|
||||
pub use store::{
|
||||
StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutVehicleSeed,
|
||||
|
||||
@ -3,13 +3,34 @@ use forge_shared::OrgValidationError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const DEFAULT_CREDIT_LINE_INTEREST_RATE: f64 = 0.10;
|
||||
|
||||
fn round_currency(value: f64) -> f64 {
|
||||
(value.max(0.0) * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreditLineSummary {
|
||||
pub uid: String,
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub approved_amount: f64,
|
||||
#[serde(default)]
|
||||
pub available_amount: f64,
|
||||
#[serde(default)]
|
||||
pub outstanding_principal: f64,
|
||||
#[serde(default = "default_credit_line_interest_rate")]
|
||||
pub interest_rate: f64,
|
||||
#[serde(default)]
|
||||
pub amount_due: f64,
|
||||
#[serde(default)]
|
||||
pub amount: f64,
|
||||
}
|
||||
|
||||
fn default_credit_line_interest_rate() -> f64 {
|
||||
DEFAULT_CREDIT_LINE_INTEREST_RATE
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct OrgAssetEntry {
|
||||
pub classname: String,
|
||||
@ -113,6 +134,14 @@ pub struct OrgCheckoutContext {
|
||||
pub commit: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OrgCreditLineRepaymentContext {
|
||||
pub requester_uid: String,
|
||||
pub org_id: String,
|
||||
pub amount: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OrgAssetGrantSeed {
|
||||
@ -145,6 +174,19 @@ pub struct OrgMutationResult {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OrgCreditLineRepaymentResult {
|
||||
pub org: HotOrgRecord,
|
||||
pub patch: HashMap<String, serde_json::Value>,
|
||||
pub member_uids: Vec<String>,
|
||||
pub paid_amount: f64,
|
||||
pub principal_paid: f64,
|
||||
pub interest_paid: f64,
|
||||
pub remaining_amount_due: f64,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OrgLeaveContext {
|
||||
@ -241,7 +283,12 @@ impl Org {
|
||||
));
|
||||
}
|
||||
|
||||
if credit_line.amount < 0.0 {
|
||||
if credit_line.approved_amount < 0.0
|
||||
|| credit_line.available_amount < 0.0
|
||||
|| credit_line.outstanding_principal < 0.0
|
||||
|| credit_line.amount_due < 0.0
|
||||
|| credit_line.amount < 0.0
|
||||
{
|
||||
return Err(OrgValidationError::NegativeCreditLine(
|
||||
resolved_uid.to_string(),
|
||||
));
|
||||
@ -254,6 +301,12 @@ impl Org {
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub fn normalize_credit_lines(&mut self) {
|
||||
for credit_line in self.credit_lines.values_mut() {
|
||||
credit_line.normalize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HotOrgRecord {
|
||||
@ -280,14 +333,47 @@ impl HotOrgRecord {
|
||||
}
|
||||
|
||||
pub fn into_org(self) -> Org {
|
||||
Org {
|
||||
let mut org = Org {
|
||||
id: self.id,
|
||||
owner: self.owner,
|
||||
name: self.name,
|
||||
funds: self.funds,
|
||||
reputation: self.reputation,
|
||||
credit_lines: self.credit_lines,
|
||||
};
|
||||
org.normalize_credit_lines();
|
||||
org
|
||||
}
|
||||
}
|
||||
|
||||
impl CreditLineSummary {
|
||||
pub fn normalize(&mut self) {
|
||||
let legacy_amount = round_currency(self.amount);
|
||||
|
||||
self.approved_amount = round_currency(self.approved_amount);
|
||||
self.available_amount = round_currency(self.available_amount);
|
||||
self.outstanding_principal = round_currency(self.outstanding_principal);
|
||||
self.amount_due = round_currency(self.amount_due);
|
||||
|
||||
if self.approved_amount <= 0.0 && self.available_amount <= 0.0 && legacy_amount > 0.0 {
|
||||
self.approved_amount = legacy_amount;
|
||||
self.available_amount = legacy_amount;
|
||||
} else if self.approved_amount <= 0.0 && self.available_amount > 0.0 {
|
||||
self.approved_amount = self.available_amount;
|
||||
} else if self.available_amount <= 0.0 && self.approved_amount > 0.0 {
|
||||
self.available_amount = self.approved_amount;
|
||||
}
|
||||
|
||||
if self.interest_rate <= 0.0 {
|
||||
self.interest_rate = DEFAULT_CREDIT_LINE_INTEREST_RATE;
|
||||
}
|
||||
|
||||
if self.amount_due <= 0.0 && self.outstanding_principal > 0.0 {
|
||||
self.amount_due =
|
||||
round_currency(self.outstanding_principal * (1.0 + self.interest_rate));
|
||||
}
|
||||
|
||||
self.amount = self.available_amount;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,10 +6,11 @@
|
||||
//! For full documentation, architecture, and examples, see the [crate README](../README.md).
|
||||
|
||||
use forge_models::{
|
||||
CreditLineSummary, HotOrgRecord, MemberSummary, Org, OrgAssetEntry, OrgAssetGrantSeed,
|
||||
OrgCheckoutContext, OrgCreditLineContext, OrgDisbandMemberResult, OrgDisbandResult,
|
||||
OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext,
|
||||
OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
|
||||
CreditLineSummary, DEFAULT_CREDIT_LINE_INTEREST_RATE, HotOrgRecord, MemberSummary, Org,
|
||||
OrgAssetEntry, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext,
|
||||
OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandMemberResult,
|
||||
OrgDisbandResult, OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext,
|
||||
OrgLeaveContext, OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
|
||||
};
|
||||
use forge_repositories::{OrgHotRepository, OrgRepository};
|
||||
use serde_json::{Value, json};
|
||||
@ -58,7 +59,10 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
);
|
||||
}
|
||||
|
||||
serde_json::from_value::<Org>(org_value).map_err(|e| format!("Invalid Org JSON: {}", e))
|
||||
let mut org = serde_json::from_value::<Org>(org_value)
|
||||
.map_err(|e| format!("Invalid Org JSON: {}", e))?;
|
||||
org.normalize_credit_lines();
|
||||
Ok(org)
|
||||
}
|
||||
|
||||
/// Creates a new organization service with the provided repository.
|
||||
@ -94,9 +98,12 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
}
|
||||
|
||||
pub fn get_org(&self, key: String) -> Result<Org, String> {
|
||||
self.repository
|
||||
let mut org = self
|
||||
.repository
|
||||
.get_by_id(&key)?
|
||||
.ok_or_else(|| format!("Organization with ID '{}' not found", key))
|
||||
.ok_or_else(|| format!("Organization with ID '{}' not found", key))?;
|
||||
org.normalize_credit_lines();
|
||||
Ok(org)
|
||||
}
|
||||
|
||||
/// Updates an existing organization with new data from JSON.
|
||||
@ -191,6 +198,7 @@ impl<R: OrgRepository> OrgService<R> {
|
||||
}
|
||||
|
||||
// Validate the updated organization before committing changes
|
||||
updated_org.normalize_credit_lines();
|
||||
updated_org
|
||||
.validate()
|
||||
.map_err(|e| format!("Validation failed: {}", e))?;
|
||||
@ -532,23 +540,50 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
|
||||
context.member_name
|
||||
};
|
||||
|
||||
org.credit_lines.insert(
|
||||
context.member_uid.clone(),
|
||||
CreditLineSummary {
|
||||
let mut credit_line = org
|
||||
.credit_lines
|
||||
.get(&context.member_uid)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| CreditLineSummary {
|
||||
uid: context.member_uid.clone(),
|
||||
name: member_name.clone(),
|
||||
amount: context.amount,
|
||||
},
|
||||
);
|
||||
approved_amount: 0.0,
|
||||
available_amount: 0.0,
|
||||
outstanding_principal: 0.0,
|
||||
interest_rate: DEFAULT_CREDIT_LINE_INTEREST_RATE,
|
||||
amount_due: 0.0,
|
||||
amount: 0.0,
|
||||
});
|
||||
credit_line.normalize();
|
||||
|
||||
let next_reserved_amount = round_currency(context.amount);
|
||||
let previous_reserved_amount = round_currency(credit_line.available_amount);
|
||||
let treasury_delta = round_currency(next_reserved_amount - previous_reserved_amount);
|
||||
if treasury_delta > 0.0 && org.funds < treasury_delta {
|
||||
return Err("Organization funds cannot cover that credit assignment.".to_string());
|
||||
}
|
||||
|
||||
org.funds = round_currency(org.funds - treasury_delta);
|
||||
credit_line.uid = context.member_uid.clone();
|
||||
credit_line.name = member_name.clone();
|
||||
credit_line.approved_amount = next_reserved_amount;
|
||||
credit_line.available_amount = next_reserved_amount;
|
||||
credit_line.amount = next_reserved_amount;
|
||||
if credit_line.interest_rate <= 0.0 {
|
||||
credit_line.interest_rate = DEFAULT_CREDIT_LINE_INTEREST_RATE;
|
||||
}
|
||||
|
||||
org.credit_lines
|
||||
.insert(context.member_uid.clone(), credit_line);
|
||||
self.repository.save(&org)?;
|
||||
|
||||
Ok(OrgMutationResult {
|
||||
patch: build_org_patch(&org, &["credit_lines"])?,
|
||||
patch: build_org_patch(&org, &["funds", "credit_lines"])?,
|
||||
member_uids: resolve_member_uids(&org, Some(&context.requester_uid)),
|
||||
message: format!(
|
||||
"Credit line of ${} assigned to {}.",
|
||||
format_currency(context.amount),
|
||||
member_name
|
||||
"Credit line for {} set to ${}.",
|
||||
member_name,
|
||||
format_currency(next_reserved_amount)
|
||||
),
|
||||
org,
|
||||
})
|
||||
@ -602,11 +637,22 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
|
||||
"Assigned credit line cannot cover this checkout.".to_string()
|
||||
})?;
|
||||
|
||||
if credit_line.amount < context.amount {
|
||||
credit_line.normalize();
|
||||
|
||||
if credit_line.available_amount < context.amount {
|
||||
return Err("Assigned credit line cannot cover this checkout.".to_string());
|
||||
}
|
||||
|
||||
credit_line.amount -= context.amount;
|
||||
let charged_amount = round_currency(context.amount);
|
||||
credit_line.available_amount =
|
||||
round_currency(credit_line.available_amount - charged_amount);
|
||||
credit_line.approved_amount = credit_line.available_amount;
|
||||
credit_line.outstanding_principal =
|
||||
round_currency(credit_line.outstanding_principal + charged_amount);
|
||||
credit_line.amount_due = round_currency(
|
||||
credit_line.amount_due + (charged_amount * (1.0 + credit_line.interest_rate)),
|
||||
);
|
||||
credit_line.amount = credit_line.available_amount;
|
||||
org.credit_lines
|
||||
.insert(context.requester_uid.clone(), credit_line);
|
||||
self.repository.save(&org)?;
|
||||
@ -622,6 +668,81 @@ impl<R: OrgRepository, H: OrgHotRepository> OrgHotStateService<R, H> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn repay_credit_line(
|
||||
&self,
|
||||
context: OrgCreditLineRepaymentContext,
|
||||
) -> Result<OrgCreditLineRepaymentResult, String> {
|
||||
if context.requester_uid.trim().is_empty() || context.org_id.trim().is_empty() {
|
||||
return Err("A valid requester and organization are required.".to_string());
|
||||
}
|
||||
if context.amount <= 0.0 {
|
||||
return Err("Repayment amount must be greater than zero.".to_string());
|
||||
}
|
||||
|
||||
let mut org = self.get_org(context.org_id)?;
|
||||
let member_uids = resolve_member_uids(&org, Some(&context.requester_uid));
|
||||
let mut credit_line = org
|
||||
.credit_lines
|
||||
.get(&context.requester_uid)
|
||||
.cloned()
|
||||
.ok_or_else(|| "No active credit line is assigned to this member.".to_string())?;
|
||||
credit_line.normalize();
|
||||
|
||||
if credit_line.amount_due <= 0.0 {
|
||||
return Err("This credit line has no outstanding balance.".to_string());
|
||||
}
|
||||
|
||||
let paid_amount = round_currency(context.amount.min(credit_line.amount_due));
|
||||
let principal_paid = if paid_amount >= credit_line.amount_due {
|
||||
credit_line.outstanding_principal
|
||||
} else {
|
||||
round_currency(
|
||||
paid_amount * (credit_line.outstanding_principal / credit_line.amount_due),
|
||||
)
|
||||
.min(credit_line.outstanding_principal)
|
||||
.min(paid_amount)
|
||||
};
|
||||
let interest_paid = round_currency(paid_amount - principal_paid);
|
||||
|
||||
credit_line.outstanding_principal =
|
||||
round_currency(credit_line.outstanding_principal - principal_paid);
|
||||
credit_line.amount_due = round_currency(credit_line.amount_due - paid_amount);
|
||||
if credit_line.outstanding_principal <= 0.0 {
|
||||
credit_line.outstanding_principal = 0.0;
|
||||
}
|
||||
if credit_line.amount_due <= 0.0 {
|
||||
credit_line.amount_due = 0.0;
|
||||
}
|
||||
credit_line.amount = credit_line.available_amount;
|
||||
|
||||
org.funds = round_currency(org.funds + paid_amount);
|
||||
org.credit_lines
|
||||
.insert(context.requester_uid.clone(), credit_line.clone());
|
||||
self.repository.save(&org)?;
|
||||
|
||||
Ok(OrgCreditLineRepaymentResult {
|
||||
patch: build_org_patch(&org, &["funds", "credit_lines"])?,
|
||||
member_uids,
|
||||
paid_amount,
|
||||
principal_paid,
|
||||
interest_paid,
|
||||
remaining_amount_due: credit_line.amount_due,
|
||||
message: if credit_line.amount_due > 0.0 {
|
||||
format!(
|
||||
"Credit repayment posted. ${} paid with ${} still due.",
|
||||
format_currency(paid_amount),
|
||||
format_currency(credit_line.amount_due)
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Credit repayment posted. ${} cleared the outstanding balance.",
|
||||
format_currency(paid_amount)
|
||||
)
|
||||
},
|
||||
org,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_assets(
|
||||
&self,
|
||||
context: OrgGrantContext,
|
||||
@ -891,7 +1012,7 @@ fn current_org_field_value(org: &HotOrgRecord, field: &str) -> Result<Value, Str
|
||||
}
|
||||
|
||||
fn format_currency(amount: f64) -> String {
|
||||
let rounded = amount.max(0.0).round() as i64;
|
||||
let rounded = round_currency(amount).round() as i64;
|
||||
let digits = rounded.to_string();
|
||||
let mut formatted = String::new();
|
||||
|
||||
@ -904,3 +1025,7 @@ fn format_currency(amount: f64) -> String {
|
||||
|
||||
formatted.chars().rev().collect()
|
||||
}
|
||||
|
||||
fn round_currency(amount: f64) -> f64 {
|
||||
(amount.max(0.0) * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
@ -451,13 +451,23 @@ where
|
||||
org.credit_lines.get_mut(requester_uid).ok_or_else(|| {
|
||||
"Assigned credit line cannot cover this checkout.".to_string()
|
||||
})?;
|
||||
if credit_line.amount < charged_total {
|
||||
credit_line.normalize();
|
||||
if credit_line.available_amount < charged_total {
|
||||
return Err(
|
||||
"Assigned credit line cannot cover this checkout.".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
credit_line.amount -= charged_total;
|
||||
credit_line.available_amount =
|
||||
round_currency(credit_line.available_amount - charged_total);
|
||||
credit_line.approved_amount = credit_line.available_amount;
|
||||
credit_line.outstanding_principal =
|
||||
round_currency(credit_line.outstanding_principal + charged_total);
|
||||
credit_line.amount_due = round_currency(
|
||||
credit_line.amount_due
|
||||
+ (charged_total * (1.0 + credit_line.interest_rate)),
|
||||
);
|
||||
credit_line.amount = credit_line.available_amount;
|
||||
org_patch.insert("credit_lines".to_string(), json!(org.credit_lines));
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@ -683,3 +693,7 @@ fn format_currency(amount: f64) -> String {
|
||||
|
||||
format!("${}", formatted.chars().rev().collect::<String>())
|
||||
}
|
||||
|
||||
fn round_currency(amount: f64) -> f64 {
|
||||
(amount.max(0.0) * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user