feat: Implement organization registration fee and bank PIN change functionality
- Updated HomeView and RegistrationView to reflect the new $50,000 registration fee for organizations. - Enhanced actor onboarding process to include sending welcome emails and messages, along with initializing bank accounts with $2,000 starting credit. - Added functionality to change bank PINs, including validation and persistence of new PINs. - Updated bank and organization modules to handle registration fee charges and refunds appropriately. - Enhanced documentation to reflect changes in organization registration and bank operations.
This commit is contained in:
parent
80d2b1fc00
commit
264559306d
@ -3,7 +3,7 @@
|
||||
## Overview
|
||||
The bank addon provides the client banking UI and browser bridge for account
|
||||
hydrate, deposits, withdrawals, transfers, PIN entry, earnings deposits, and
|
||||
credit-line repayment.
|
||||
credit-line repayment. It also exposes PIN changes from the full bank UI.
|
||||
|
||||
## Dependencies
|
||||
- `forge_client_common`
|
||||
@ -27,6 +27,7 @@ credit-line repayment.
|
||||
- `bank::depositEarnings::request`
|
||||
- `bank::repayCreditLine::request`
|
||||
- `bank::pin::request`
|
||||
- `bank::pin::change::request`
|
||||
- `bank::close`
|
||||
|
||||
## Runtime Notes
|
||||
|
||||
@ -78,6 +78,11 @@ switch (_event) do {
|
||||
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
|
||||
};
|
||||
};
|
||||
case "bank::pin::change::request": {
|
||||
if !(isNil QGVAR(BankUIBridge)) then {
|
||||
GVAR(BankUIBridge) call ["handleChangePinRequest", [_data]];
|
||||
};
|
||||
};
|
||||
default {
|
||||
hint format ["Unhandled bank UI event: %1", _event];
|
||||
};
|
||||
|
||||
@ -63,6 +63,17 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleChangePinRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _currentPin = _data getOrDefault ["currentPin", ""];
|
||||
private _newPin = _data getOrDefault ["newPin", ""];
|
||||
if !(_currentPin isEqualType "") then { _currentPin = str _currentPin; };
|
||||
if !(_newPin isEqualType "") then { _newPin = str _newPin; };
|
||||
|
||||
[SRPC(bank,requestChangePin), [getPlayerUID player, _currentPin, _newPin]] call CFUNC(serverEvent);
|
||||
true
|
||||
}],
|
||||
["handleRepayCreditLineRequest", compileFinal {
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -49,6 +49,9 @@
|
||||
requestRefresh() {
|
||||
return bridge.send("bank::refresh", {});
|
||||
},
|
||||
requestChangePin(payload) {
|
||||
return bridge.send("bank::pin::change::request", payload);
|
||||
},
|
||||
requestSubmitPin(payload) {
|
||||
return bridge.send("bank::pin::request", payload);
|
||||
},
|
||||
|
||||
@ -352,6 +352,73 @@
|
||||
: "Deposit Earnings",
|
||||
),
|
||||
),
|
||||
h(
|
||||
"section",
|
||||
{ className: "bank-page-section" },
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-section-header" },
|
||||
h(
|
||||
"div",
|
||||
null,
|
||||
h("span", { className: "bank-eyebrow" }, "Security"),
|
||||
h(
|
||||
"h2",
|
||||
{ className: "bank-section-title" },
|
||||
"Change ATM PIN",
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ className: "bank-form-stack" },
|
||||
h("input", {
|
||||
id: "bank-current-pin",
|
||||
className: "bank-input",
|
||||
type: "password",
|
||||
inputMode: "numeric",
|
||||
maxLength: "4",
|
||||
placeholder: "Current PIN",
|
||||
}),
|
||||
h("input", {
|
||||
id: "bank-new-pin",
|
||||
className: "bank-input",
|
||||
type: "password",
|
||||
inputMode: "numeric",
|
||||
maxLength: "4",
|
||||
placeholder: "New PIN",
|
||||
}),
|
||||
h("input", {
|
||||
id: "bank-confirm-pin",
|
||||
className: "bank-input",
|
||||
type: "password",
|
||||
inputMode: "numeric",
|
||||
maxLength: "4",
|
||||
placeholder: "Confirm new PIN",
|
||||
}),
|
||||
h(
|
||||
"button",
|
||||
{
|
||||
type: "button",
|
||||
className: "bank-btn bank-btn-primary",
|
||||
disabled: pending("changepin"),
|
||||
onClick: () => {
|
||||
const sent = actions.requestChangePin(
|
||||
readInputValue("bank-current-pin"),
|
||||
readInputValue("bank-new-pin"),
|
||||
readInputValue("bank-confirm-pin"),
|
||||
);
|
||||
if (sent) {
|
||||
clearInputValue("bank-current-pin");
|
||||
clearInputValue("bank-new-pin");
|
||||
clearInputValue("bank-confirm-pin");
|
||||
}
|
||||
},
|
||||
},
|
||||
pending("changepin") ? "Updating PIN..." : "Update PIN",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -149,6 +149,53 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizePin(value) {
|
||||
return String(value || "")
|
||||
.replace(/\D/g, "")
|
||||
.slice(0, 4);
|
||||
}
|
||||
|
||||
function requestChangePin(currentPinValue, newPinValue, confirmPinValue) {
|
||||
const currentPin = normalizePin(currentPinValue);
|
||||
const newPin = normalizePin(newPinValue);
|
||||
const confirmPin = normalizePin(confirmPinValue);
|
||||
const bridge = BankApp.bridge;
|
||||
|
||||
if (!bridge || typeof bridge.requestChangePin !== "function") {
|
||||
showNotice("error", "PIN change bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
if (currentPin.length !== 4) {
|
||||
showNotice("error", "Enter your current four-digit PIN.");
|
||||
return false;
|
||||
}
|
||||
if (newPin.length !== 4) {
|
||||
showNotice("error", "Choose a new four-digit PIN.");
|
||||
return false;
|
||||
}
|
||||
if (newPin !== confirmPin) {
|
||||
showNotice("error", "New PIN confirmation does not match.");
|
||||
return false;
|
||||
}
|
||||
if (currentPin === newPin) {
|
||||
showNotice(
|
||||
"error",
|
||||
"Choose a different PIN from your current PIN.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
store.startAction("changepin");
|
||||
const sent = bridge.requestChangePin({ currentPin, newPin });
|
||||
if (!sent) {
|
||||
store.finishAction();
|
||||
showNotice("error", "PIN change bridge is unavailable.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function appendPinDigit(digit) {
|
||||
const nextDigit = String(digit || "").trim();
|
||||
if (!nextDigit) {
|
||||
@ -276,6 +323,7 @@
|
||||
closeBank,
|
||||
refreshBank,
|
||||
requestAtmAmount,
|
||||
requestChangePin,
|
||||
requestDeposit,
|
||||
requestDepositEarnings,
|
||||
requestRepayCreditLine,
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
## Overview
|
||||
The organization addon provides the client organization portal UI and bridge for
|
||||
organization hydrate, registration, membership, invitations, credit lines,
|
||||
leave/disband actions, assets, fleet, and treasury display.
|
||||
leave/disband actions, assets, fleet, and treasury display. Registration shows
|
||||
the $50,000 personal funds requirement enforced by the server org addon.
|
||||
|
||||
## Dependencies
|
||||
- `forge_client_common`
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -46,7 +46,7 @@ ${scopeSelector} .home-feedback {
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.",
|
||||
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Registration requires $50,000 in personal funds.",
|
||||
),
|
||||
h(
|
||||
"button",
|
||||
|
||||
@ -177,7 +177,7 @@ ${scopeSelector} .form-feedback.is-error {
|
||||
h(
|
||||
"p",
|
||||
null,
|
||||
"Complete the form to add your organization to the Global Organization Registry.",
|
||||
"Complete the form to add your organization to the Global Organization Registry. Registration requires at least $50,000 in personal funds.",
|
||||
),
|
||||
h(
|
||||
"ul",
|
||||
@ -258,7 +258,11 @@ ${scopeSelector} .form-feedback.is-error {
|
||||
h(
|
||||
"div",
|
||||
{ className: "price-tag" },
|
||||
h("span", { className: "price-label" }, "Registration Fee"),
|
||||
h(
|
||||
"span",
|
||||
{ className: "price-label" },
|
||||
"Required Registration Fee",
|
||||
),
|
||||
h("span", { className: "price-value" }, "$50,000"),
|
||||
),
|
||||
),
|
||||
|
||||
@ -12,6 +12,8 @@ life state, phone number, email, organization, and holster state.
|
||||
- `forge_server_main`
|
||||
- `forge_server_common`
|
||||
- `forge_server_extension` at runtime for actor extension calls
|
||||
- `forge_server_phone` for new actor welcome email and messages
|
||||
- `forge_server_bank` for new actor starting bank credit
|
||||
- `forge_client_actor` for response RPCs
|
||||
|
||||
## Main Components
|
||||
@ -23,6 +25,9 @@ life state, phone number, email, organization, and holster state.
|
||||
|
||||
## Runtime Behavior
|
||||
- Missing persistent actors can be created from live player snapshots.
|
||||
- Newly created actors receive a Field Commander job orientation email, two
|
||||
Field Commander text messages, and a `$2,000` starting credit in their bank
|
||||
account.
|
||||
- Hot actor reads are migrated and hydrated before use.
|
||||
- `saveHotState` in the main addon snapshots and saves actor state on player
|
||||
disconnect and mission end.
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
* File: fnc_initActorStore.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2025-12-17
|
||||
* Last Update: 2026-04-05
|
||||
* Last Update: 2026-05-16
|
||||
* Public: Yes
|
||||
*
|
||||
* Description:
|
||||
@ -149,6 +149,108 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
_uids select { _x isEqualType "" && { _x isNotEqualTo "" } }
|
||||
}],
|
||||
["sendNewActorWelcomeComms", compileFinal {
|
||||
params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { false };
|
||||
if (isNil QEGVAR(phone,PhoneStore)) exitWith {
|
||||
["WARNING", format ["Unable to send new actor welcome comms for %1: phone store is unavailable.", _uid]] call EFUNC(common,log);
|
||||
false
|
||||
};
|
||||
|
||||
EGVAR(phone,PhoneStore) call ["init", [_uid]];
|
||||
|
||||
private _phoneNumber = _actor getOrDefault ["phone_number", ""];
|
||||
private _emailAddress = _actor getOrDefault ["email", ""];
|
||||
private _welcomeEmail = format [
|
||||
"Welcome to your first day on the job. Forge Dynamics has issued you a work phone with phone number %1 and email address %2. Keep these details handy for field communications and future assignments.",
|
||||
_phoneNumber,
|
||||
_emailAddress
|
||||
];
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
private _emailObj = EGVAR(phone,PhoneStore) call [
|
||||
"sendEmail",
|
||||
["field_commander", _uid, "Job Orientation", _welcomeEmail]
|
||||
];
|
||||
|
||||
if (
|
||||
_emailObj isEqualType createHashMap
|
||||
&& { _emailObj isNotEqualTo createHashMap }
|
||||
&& { !(isNull _player) }
|
||||
) then {
|
||||
["forge_client_phone_responseEmailReceived", [_emailObj], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
|
||||
private _messages = [
|
||||
"Welcome to your first day on the job. Forge Dynamics has issued your starting equipment and a small account credit. These are the only free supplies you will receive for this identity, so use them wisely. You are responsible for all purchases going forward.",
|
||||
"Deposit your Earnings before leaving the session. Access the Bank from any laptop, then select Deposit Earnings."
|
||||
];
|
||||
|
||||
{
|
||||
private _messageObj = EGVAR(phone,PhoneStore) call [
|
||||
"sendMessage",
|
||||
["field_commander", _uid, _x]
|
||||
];
|
||||
if (
|
||||
_messageObj isEqualType createHashMap
|
||||
&& { _messageObj isNotEqualTo createHashMap }
|
||||
&& { !(isNull _player) }
|
||||
) then {
|
||||
["forge_client_phone_responseMessageReceived", [_messageObj], _player] call CFUNC(targetEvent);
|
||||
};
|
||||
} forEach _messages;
|
||||
|
||||
true
|
||||
}],
|
||||
["grantNewActorStartingBankCredit", compileFinal {
|
||||
params [["_uid", "", [""]], ["_amount", 2000, [0]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _amount <= 0 }) exitWith { false };
|
||||
if (isNil QEGVAR(bank,BankStore) || { isNil QEGVAR(bank,BankMessenger) }) exitWith {
|
||||
["WARNING", format ["Unable to grant new actor starting bank credit for %1: bank store or messenger is unavailable.", _uid]] call EFUNC(common,log);
|
||||
false
|
||||
};
|
||||
|
||||
private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
|
||||
if (_account isEqualTo createHashMap) then {
|
||||
_account = EGVAR(bank,BankStore) call ["init", [_uid]];
|
||||
};
|
||||
if (_account isEqualTo createHashMap) exitWith {
|
||||
["WARNING", format ["Unable to grant new actor starting bank credit for %1: bank account could not be initialized.", _uid]] call EFUNC(common,log);
|
||||
false
|
||||
};
|
||||
|
||||
private _currentBank = _account getOrDefault ["bank", 0];
|
||||
if !(_currentBank isEqualType 0) then { _currentBank = 0; };
|
||||
|
||||
private _patch = EGVAR(bank,BankStore) call [
|
||||
"mset",
|
||||
[
|
||||
_uid,
|
||||
createHashMapFromArray [["bank", _currentBank + _amount]],
|
||||
true
|
||||
]
|
||||
];
|
||||
if (_patch isEqualTo createHashMap) exitWith {
|
||||
["WARNING", format ["Unable to grant new actor starting bank credit for %1: bank account update failed.", _uid]] call EFUNC(common,log);
|
||||
false
|
||||
};
|
||||
|
||||
EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]];
|
||||
|
||||
true
|
||||
}],
|
||||
["bootstrapNewActor", compileFinal {
|
||||
params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { false };
|
||||
|
||||
_self call ["sendNewActorWelcomeComms", [_uid, _actor]];
|
||||
_self call ["grantNewActorStartingBankCredit", [_uid, 2000]];
|
||||
|
||||
true
|
||||
}],
|
||||
["loadHotActor", compileFinal {
|
||||
params [["_uid", "", [""]], ["_initialize", false, [false]]];
|
||||
|
||||
@ -200,6 +302,13 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
|
||||
false
|
||||
};
|
||||
|
||||
private _createdActor = fromJSON _createResult;
|
||||
if !(_createdActor isEqualType createHashMap) then {
|
||||
_createdActor = +_actor;
|
||||
};
|
||||
_createdActor = GVAR(ActorModel) call ["migrate", [_createdActor]];
|
||||
_self call ["bootstrapNewActor", [_uid, _createdActor]];
|
||||
|
||||
true
|
||||
}],
|
||||
["hydrateActorIfNeeded", compileFinal {
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
## Overview
|
||||
The bank addon owns the SQF bridge for player accounts, cash and bank balances,
|
||||
PIN/session handling, transfers, checkout charging, earnings deposits, and
|
||||
credit-line repayment.
|
||||
credit-line repayment. It also verifies and persists player-requested ATM PIN
|
||||
changes.
|
||||
|
||||
Account truth lives in the extension hot cache. SQF handles Arma-facing
|
||||
validation, client messaging, session state, and payment integration with other
|
||||
@ -28,7 +29,7 @@ server addons.
|
||||
## Supported Operations
|
||||
- initialize and hydrate player bank state
|
||||
- deposit, withdraw, transfer, and deposit earnings
|
||||
- validate PIN-backed sessions
|
||||
- validate PIN-backed sessions and change ATM PINs
|
||||
- charge checkout previews and committed purchases
|
||||
- repay organization credit lines with rollback on failure
|
||||
- save hot bank state to durable storage
|
||||
|
||||
@ -32,6 +32,12 @@ PREP_RECOMPILE_END;
|
||||
GVAR(BankSessionManager) call ["submitPin", [_uid, _pin]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestChangePin), {
|
||||
params [["_uid", "", [""]], ["_currentPin", "", [""]], ["_newPin", "", [""]]];
|
||||
|
||||
GVAR(BankStore) call ["changePin", [_uid, _currentPin, _newPin]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestTransfer), {
|
||||
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
|
||||
@ -496,6 +496,34 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
|
||||
true
|
||||
}
|
||||
}],
|
||||
["changePin", compileFinal {
|
||||
params [["_uid", "", [""]], ["_currentPin", "", [""]], ["_newPin", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "") exitWith { false };
|
||||
|
||||
private _current = _currentPin;
|
||||
private _next = _newPin;
|
||||
if !(_current isEqualType "") then { _current = str _current; };
|
||||
if !(_next isEqualType "") then { _next = str _next; };
|
||||
|
||||
private _changed = _self call [
|
||||
"runMutation",
|
||||
[
|
||||
_uid,
|
||||
"bank:hot:change_pin",
|
||||
[_uid, _current, _next, toJSON (GVAR(BankPayloadBuilder) call ["buildOperationContext", [_uid]])],
|
||||
true,
|
||||
""
|
||||
]
|
||||
];
|
||||
|
||||
if (_changed) then {
|
||||
GVAR(BankMessenger) call ["sendAlert", [_uid, "success", "Bank PIN updated."]];
|
||||
_self call ["hydrateSession", [_uid, "", false]];
|
||||
};
|
||||
|
||||
_changed
|
||||
}],
|
||||
["withdraw", compileFinal {
|
||||
params [["_uid", "", [""]], ["_amount", 0, [0]]];
|
||||
|
||||
|
||||
@ -9,6 +9,9 @@ Organization hot state is owned by the extension. SQF coordinates Arma-facing
|
||||
events, UI payloads, membership syncs, and integration with actor, bank, store,
|
||||
and task flows.
|
||||
|
||||
Organization registration charges a $50,000 personal funds fee before the
|
||||
player is assigned to the new organization.
|
||||
|
||||
## Dependencies
|
||||
- `forge_server_main`
|
||||
- `forge_server_common`
|
||||
|
||||
@ -780,6 +780,91 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
true
|
||||
}],
|
||||
["syncBankPatch", compileFinal {
|
||||
params [["_uid", "", [""]], ["_patch", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _patch isEqualTo createHashMap }) exitWith { false };
|
||||
|
||||
if (isNil QEGVAR(common,EventBus)) then {
|
||||
EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]];
|
||||
} else {
|
||||
EGVAR(common,EventBus) call ["emit", [
|
||||
"bank.account.sync.requested",
|
||||
createHashMapFromArray [
|
||||
["uid", _uid],
|
||||
["account", +_patch]
|
||||
],
|
||||
createHashMapFromArray [["source", "org"]]
|
||||
]];
|
||||
};
|
||||
|
||||
true
|
||||
}],
|
||||
["chargeRegistrationFee", compileFinal {
|
||||
params [["_uid", "", [""]], ["_amount", 50000, [0]]];
|
||||
|
||||
private _result = createHashMapFromArray [
|
||||
["success", false],
|
||||
["message", "Unable to charge the organization registration fee."],
|
||||
["patch", createHashMap],
|
||||
["refundPatch", createHashMap]
|
||||
];
|
||||
|
||||
if (_uid isEqualTo "" || { _amount <= 0 }) exitWith { _result };
|
||||
if (isNil QEGVAR(bank,BankStore)) exitWith {
|
||||
_result set ["message", "Bank service is unavailable for organization registration."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _account = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
|
||||
if (_account isEqualTo createHashMap) then {
|
||||
_account = EGVAR(bank,BankStore) call ["init", [_uid]];
|
||||
};
|
||||
if (_account isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Bank account could not be loaded for organization registration."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _currentBank = _account getOrDefault ["bank", 0];
|
||||
private _currentCash = _account getOrDefault ["cash", 0];
|
||||
if ((_currentBank + _currentCash) < _amount) exitWith {
|
||||
_result set ["message", format ["You need at least $%1 in personal funds to create an organization.", [_amount] call EFUNC(common,formatNumber)]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _bankCharge = _amount min _currentBank;
|
||||
private _cashCharge = _amount - _bankCharge;
|
||||
private _patch = createHashMapFromArray [
|
||||
["bank", _currentBank - _bankCharge],
|
||||
["cash", _currentCash - _cashCharge]
|
||||
];
|
||||
private _refundPatch = createHashMapFromArray [
|
||||
["bank", _currentBank],
|
||||
["cash", _currentCash]
|
||||
];
|
||||
|
||||
private _appliedPatch = EGVAR(bank,BankStore) call ["mset", [_uid, _patch, true]];
|
||||
if (_appliedPatch isEqualTo createHashMap) exitWith {
|
||||
_result set ["message", "Organization registration fee could not be charged."];
|
||||
_result
|
||||
};
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["message", ""];
|
||||
_result set ["patch", _appliedPatch];
|
||||
_result set ["refundPatch", _refundPatch];
|
||||
_result
|
||||
}],
|
||||
["refundRegistrationFee", compileFinal {
|
||||
params [["_uid", "", [""]], ["_refundPatch", createHashMap, [createHashMap]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _refundPatch isEqualTo createHashMap } || { isNil QEGVAR(bank,BankStore) }) exitWith { false };
|
||||
|
||||
private _patch = EGVAR(bank,BankStore) call ["mset", [_uid, _refundPatch, true]];
|
||||
if (_patch isEqualTo createHashMap) exitWith { false };
|
||||
|
||||
_self call ["syncBankPatch", [_uid, _patch]]
|
||||
}],
|
||||
["updateOrgTreasuryFunds", compileFinal {
|
||||
params [["_orgID", "", [""]], ["_funds", 0, [0]]];
|
||||
|
||||
@ -1201,24 +1286,36 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
["existingOrgId", _existingOrgID]
|
||||
];
|
||||
|
||||
private _registrationFee = 50000;
|
||||
private _feeCharge = _self call ["chargeRegistrationFee", [_uid, _registrationFee]];
|
||||
if !(_feeCharge getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _feeCharge getOrDefault ["message", "Organization registration fee could not be charged."]];
|
||||
_result
|
||||
};
|
||||
private _refundPatch = _feeCharge getOrDefault ["refundPatch", createHashMap];
|
||||
|
||||
["org:hot:register", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"];
|
||||
if !_isSuccess exitWith {
|
||||
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
|
||||
_result set ["message", "Organization service was unavailable during registration."];
|
||||
_result
|
||||
};
|
||||
|
||||
if !(_rawResult isEqualType "") exitWith {
|
||||
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
|
||||
_result set ["message", "Organization service returned an invalid registration response."];
|
||||
_result
|
||||
};
|
||||
|
||||
if ((_rawResult find "Error:") == 0) exitWith {
|
||||
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
|
||||
_result set ["message", _rawResult select [7]];
|
||||
_result
|
||||
};
|
||||
|
||||
private _envelope = fromJSON _rawResult;
|
||||
if !(_envelope isEqualType createHashMap) exitWith {
|
||||
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
|
||||
_result set ["message", "Organization service returned malformed registration data."];
|
||||
_result
|
||||
};
|
||||
@ -1232,10 +1329,12 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]];
|
||||
if (_actorPatch isEqualTo createHashMap) exitWith {
|
||||
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
|
||||
_result set ["message", "Failed to assign the player to the new organization."];
|
||||
_result
|
||||
};
|
||||
|
||||
_self call ["syncBankPatch", [_uid, _feeCharge getOrDefault ["patch", createHashMap]]];
|
||||
_result set ["success", true];
|
||||
_result set ["message", _envelope getOrDefault ["message", ""]];
|
||||
_result set ["org", _envelope getOrDefault ["org", createHashMap]];
|
||||
|
||||
@ -58,6 +58,7 @@ pub fn group() -> Group {
|
||||
.command("deposit_earnings", deposit_earnings_hot_bank)
|
||||
.command("transfer", transfer_hot_bank)
|
||||
.command("validate_pin", validate_pin_hot_bank)
|
||||
.command("change_pin", change_pin_hot_bank)
|
||||
.command("save", save_hot_bank)
|
||||
.command("remove", remove_hot_bank),
|
||||
)
|
||||
@ -317,6 +318,28 @@ pub(crate) fn validate_pin_hot_bank(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn change_pin_hot_bank(
|
||||
call_context: CallContext,
|
||||
key: String,
|
||||
current_pin: String,
|
||||
new_pin: String,
|
||||
json_context: String,
|
||||
) -> String {
|
||||
let resolved_uid = match resolve_uid(&key, &call_context) {
|
||||
Some(uid) => uid,
|
||||
None => return format!("Error: Failed to resolve UID for key: {}", key),
|
||||
};
|
||||
let context = match parse_pin_context(json_context) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return format!("Error: {}", error),
|
||||
};
|
||||
|
||||
match HOT_BANK_SERVICE.change_pin(resolved_uid, current_pin, new_pin, context) {
|
||||
Ok(result) => serialize_hot_bank_mutation(result),
|
||||
Err(error) => format!("Error: {}", error),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn save_hot_bank(call_context: CallContext, key: String) -> String {
|
||||
let resolved_uid = match resolve_uid(&key, &call_context) {
|
||||
Some(uid) => uid,
|
||||
|
||||
@ -118,6 +118,16 @@ pub(super) fn route(
|
||||
arguments[2].clone(),
|
||||
))
|
||||
}
|
||||
"bank:hot:change_pin" => {
|
||||
expect_arg_count(function_name, &arguments, 4)?;
|
||||
Ok(bank::change_pin_hot_bank(
|
||||
call_context,
|
||||
arguments[0].clone(),
|
||||
arguments[1].clone(),
|
||||
arguments[2].clone(),
|
||||
arguments[3].clone(),
|
||||
))
|
||||
}
|
||||
"bank:hot:save" => {
|
||||
expect_arg_count(function_name, &arguments, 1)?;
|
||||
Ok(bank::save_hot_bank(call_context, arguments[0].clone()))
|
||||
|
||||
@ -74,6 +74,22 @@ private _result = "forge_server" callExtension ["actor:create", [
|
||||
]];
|
||||
```
|
||||
|
||||
## New Player Bootstrap
|
||||
|
||||
The server actor store treats a player with no persisted actor as a new player.
|
||||
After `actor:create` succeeds, the actor store runs onboarding once for that UID:
|
||||
|
||||
- Initializes the player's phone state.
|
||||
- Sends a Field Commander email from `field_commander` with the `Job Orientation`
|
||||
subject and the generated phone number and email address.
|
||||
- Sends two Field Commander text messages with the first-day instructions.
|
||||
- Initializes the player's bank account if needed and adds `$2,000` to the bank
|
||||
balance.
|
||||
|
||||
This bootstrap is tied to persistent actor creation, not hot-state hydration, so
|
||||
returning players and repaired partial actor records do not receive the welcome
|
||||
messages or starting money again.
|
||||
|
||||
## Update an Actor
|
||||
|
||||
`actor:update` accepts a JSON object containing only fields to change.
|
||||
|
||||
@ -2,7 +2,8 @@
|
||||
|
||||
The bank module stores player account balances, earnings, PINs, and transaction
|
||||
strings. The hot-state API also owns the active banking workflows used by the
|
||||
UI: deposit, withdraw, transfer, checkout charge, and PIN validation.
|
||||
UI: deposit, withdraw, transfer, checkout charge, PIN validation, and PIN
|
||||
changes.
|
||||
|
||||
## Storage Model
|
||||
|
||||
@ -73,6 +74,7 @@ private _result = "forge_server" callExtension ["bank:create", [
|
||||
| `bank:hot:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. |
|
||||
| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. |
|
||||
| `bank:hot:change_pin` | `uid`, `current_pin`, `new_pin`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:save` | `uid` | Current hot bank JSON and async durable save. |
|
||||
| `bank:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
@ -155,6 +157,25 @@ private _result = "forge_server" callExtension ["bank:hot:validate_pin", [
|
||||
]];
|
||||
```
|
||||
|
||||
## PIN Changes
|
||||
|
||||
PIN changes require the current PIN and a different four-digit new PIN. The
|
||||
command is only valid from the full bank interface.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["mode", "bank"],
|
||||
["atmAuthorized", false]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["bank:hot:change_pin", [
|
||||
getPlayerUID player,
|
||||
"1234",
|
||||
"5678",
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
|
||||
@ -33,8 +33,8 @@ state.
|
||||
- bank/ATM mode
|
||||
- browser ready handling
|
||||
- account hydrate and sync responses
|
||||
- deposit, withdrawal, transfer, earnings deposit, credit repayment, and PIN
|
||||
requests
|
||||
- deposit, withdrawal, transfer, earnings deposit, credit repayment, PIN
|
||||
validation, and PIN change requests
|
||||
- browser notice delivery
|
||||
|
||||
## Browser Events
|
||||
@ -49,6 +49,7 @@ state.
|
||||
| `bank::depositEarnings::request` | Request earnings deposit. |
|
||||
| `bank::repayCreditLine::request` | Request credit-line repayment. |
|
||||
| `bank::pin::request` | Forward PIN validation request. |
|
||||
| `bank::pin::change::request` | Forward current and new PIN values for a PIN change. |
|
||||
| `bank::close` | Dispose bridge screen state and close the display. |
|
||||
|
||||
## Browser Response Events
|
||||
@ -77,6 +78,10 @@ Balances, PIN authorization, transfers, checkout charges, credit lines, and
|
||||
persistence are server-owned. The client should only display account data and
|
||||
request mutations through server events.
|
||||
|
||||
PIN changes are available from the full bank UI only. The browser validates the
|
||||
current, new, and confirmation fields, but the server extension remains
|
||||
authoritative and persists the updated PIN.
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md)
|
||||
|
||||
@ -4,6 +4,10 @@ The client organization addon provides the organization portal UI and browser
|
||||
bridge for login, registration, membership, invites, credit lines, leave and
|
||||
disband flows, assets, fleet, and treasury display.
|
||||
|
||||
Organization registration requires $50,000 in personal funds. The server org
|
||||
addon enforces and charges the fee; the browser only displays the requirement
|
||||
and submits the registration request.
|
||||
|
||||
## Open Organization UI
|
||||
|
||||
```sqf
|
||||
|
||||
@ -22,7 +22,7 @@ docs/ Framework-level documentation
|
||||
| Domain | Purpose | Client addon | Server addon | Service/model layer | Extension group |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Actor | Player identity, loadout, position, status, contact identifiers, and persistent character data. | `arma/client/addons/actor` | `arma/server/addons/actor` | `lib/models/src/actor.rs`, `lib/services/src/actor.rs` | `actor:*` |
|
||||
| Bank | Player accounts, cash/bank balances, PIN validation, transfers, checkout charging, and transaction context. | `arma/client/addons/bank` | `arma/server/addons/bank` | `lib/models/src/bank.rs`, `lib/services/src/bank.rs` | `bank:*`, `bank:hot:*` |
|
||||
| Bank | Player accounts, cash/bank balances, PIN validation and changes, transfers, checkout charging, and transaction context. | `arma/client/addons/bank` | `arma/server/addons/bank` | `lib/models/src/bank.rs`, `lib/services/src/bank.rs` | `bank:*`, `bank:hot:*` |
|
||||
| CAD | Dispatch requests, assignments, orders, activity stream, profiles, groups, and hydrated dispatcher views. | `arma/client/addons/cad` | `arma/server/addons/cad` | `lib/models/src/cad.rs`, `lib/services/src/cad.rs` | `cad:*` |
|
||||
| Garage | Player vehicle storage with plate IDs, fuel, damage, and hit point state. | `arma/client/addons/garage` | `arma/server/addons/garage` | `lib/models/src/garage.rs`, `lib/services/src/garage.rs` | `garage:*`, `garage:hot:*` |
|
||||
| Locker | Player item storage keyed by classname with category and amount. | `arma/client/addons/locker` | `arma/server/addons/locker` | `lib/models/src/locker.rs`, `lib/services/src/locker.rs` | `locker:*`, `locker:hot:*` |
|
||||
@ -114,7 +114,7 @@ See [Actor Usage Guide](./ACTOR_USAGE_GUIDE.md) for examples.
|
||||
| `bank:hot:init`, `bank:hot:get`, `bank:hot:override`, `bank:hot:patch`, `bank:hot:save`, `bank:hot:remove` | Manage bank hot state. |
|
||||
| `bank:hot:deposit`, `bank:hot:withdraw`, `bank:hot:deposit_earnings`, `bank:hot:transfer` | Mutate hot bank balances with operation context. |
|
||||
| `bank:hot:charge_checkout` | Charge a checkout against hot bank state. |
|
||||
| `bank:hot:validate_pin` | Validate a PIN for bank operations. |
|
||||
| `bank:hot:validate_pin`, `bank:hot:change_pin` | Validate and update PINs for bank operations. |
|
||||
|
||||
See [Bank Usage Guide](./BANK_USAGE_GUIDE.md) for examples.
|
||||
|
||||
|
||||
@ -45,6 +45,9 @@ Rules validated by the Rust service:
|
||||
- `funds`, reputation, and credit line amounts cannot be negative.
|
||||
- Player registration is rejected when the player already belongs to a
|
||||
non-default organization.
|
||||
- Player registration through the server org addon requires a $50,000 personal
|
||||
funds registration fee. The fee is charged from the player's bank balance
|
||||
first, then on-hand cash if needed.
|
||||
|
||||
## Durable Commands
|
||||
|
||||
@ -159,6 +162,10 @@ private _fleet = createHashMapFromArray [
|
||||
|
||||
## Register from UI Context
|
||||
|
||||
The server-side `forge_server_org` registration flow charges the $50,000
|
||||
registration fee before completing organization creation. If the organization
|
||||
service rejects the registration, the bank charge is refunded.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
|
||||
@ -23,7 +23,7 @@ docs/ Framework-level documentation
|
||||
| Domain | Purpose | Client addon | Server addon | Service/model layer | Extension group |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Actor | Player identity, loadout, position, status, contact identifiers, and persistent character data. | `arma/client/addons/actor` | `arma/server/addons/actor` | `lib/models/src/actor.rs`, `lib/services/src/actor.rs` | `actor:*` |
|
||||
| Bank | Player accounts, cash/bank balances, PIN validation, transfers, checkout charging, and transaction context. | `arma/client/addons/bank` | `arma/server/addons/bank` | `lib/models/src/bank.rs`, `lib/services/src/bank.rs` | `bank:*`, `bank:hot:*` |
|
||||
| Bank | Player accounts, cash/bank balances, PIN validation and changes, transfers, checkout charging, and transaction context. | `arma/client/addons/bank` | `arma/server/addons/bank` | `lib/models/src/bank.rs`, `lib/services/src/bank.rs` | `bank:*`, `bank:hot:*` |
|
||||
| CAD | Dispatch requests, assignments, orders, activity stream, profiles, groups, and hydrated dispatcher views. | `arma/client/addons/cad` | `arma/server/addons/cad` | `lib/models/src/cad.rs`, `lib/services/src/cad.rs` | `cad:*` |
|
||||
| Garage | Player vehicle storage with plate IDs, fuel, damage, and hit point state. | `arma/client/addons/garage` | `arma/server/addons/garage` | `lib/models/src/garage.rs`, `lib/services/src/garage.rs` | `garage:*`, `garage:hot:*` |
|
||||
| Locker | Player item storage keyed by classname with category and amount. | `arma/client/addons/locker` | `arma/server/addons/locker` | `lib/models/src/locker.rs`, `lib/services/src/locker.rs` | `locker:*`, `locker:hot:*` |
|
||||
@ -115,7 +115,7 @@ See [Actor Usage Guide](/server-modules/actor) for examples.
|
||||
| `bank:hot:init`, `bank:hot:get`, `bank:hot:override`, `bank:hot:patch`, `bank:hot:save`, `bank:hot:remove` | Manage bank hot state. |
|
||||
| `bank:hot:deposit`, `bank:hot:withdraw`, `bank:hot:deposit_earnings`, `bank:hot:transfer` | Mutate hot bank balances with operation context. |
|
||||
| `bank:hot:charge_checkout` | Charge a checkout against hot bank state. |
|
||||
| `bank:hot:validate_pin` | Validate a PIN for bank operations. |
|
||||
| `bank:hot:validate_pin`, `bank:hot:change_pin` | Validate and update PINs for bank operations. |
|
||||
|
||||
See [Bank Usage Guide](/server-modules/bank) for examples.
|
||||
|
||||
|
||||
@ -73,6 +73,22 @@ private _result = "forge_server" callExtension ["actor:create", [
|
||||
]];
|
||||
```
|
||||
|
||||
## New Player Bootstrap
|
||||
|
||||
The server actor store treats a player with no persisted actor as a new player.
|
||||
After `actor:create` succeeds, the actor store runs onboarding once for that UID:
|
||||
|
||||
- Initializes the player's phone state.
|
||||
- Sends a Field Commander email from `field_commander` with the `Job Orientation`
|
||||
subject and the generated phone number and email address.
|
||||
- Sends two Field Commander text messages with the first-day instructions.
|
||||
- Initializes the player's bank account if needed and adds `$2,000` to the bank
|
||||
balance.
|
||||
|
||||
This bootstrap is tied to persistent actor creation, not hot-state hydration, so
|
||||
returning players and repaired partial actor records do not receive the welcome
|
||||
messages or starting money again.
|
||||
|
||||
## Update an Actor
|
||||
|
||||
`actor:update` accepts a JSON object containing only fields to change.
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
title: "Bank Usage Guide"
|
||||
description: "The bank module stores player account balances, earnings, PINs, and transaction strings. The hot-state API also owns the active banking workflows used by the UI: deposit, withdraw, transfer, checkout charge, and PIN validation."
|
||||
description: "The bank module stores player account balances, earnings, PINs, and transaction strings. The hot-state API also owns the active banking workflows used by the UI: deposit, withdraw, transfer, checkout charge, PIN validation, and PIN changes."
|
||||
---
|
||||
|
||||
## Storage Model
|
||||
@ -72,6 +72,7 @@ private _result = "forge_server" callExtension ["bank:create", [
|
||||
| `bank:hot:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. |
|
||||
| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. |
|
||||
| `bank:hot:change_pin` | `uid`, `current_pin`, `new_pin`, `context_json` | `{ account, patch }`. |
|
||||
| `bank:hot:save` | `uid` | Current hot bank JSON and async durable save. |
|
||||
| `bank:hot:remove` | `uid` | `OK`. |
|
||||
|
||||
@ -154,6 +155,25 @@ private _result = "forge_server" callExtension ["bank:hot:validate_pin", [
|
||||
]];
|
||||
```
|
||||
|
||||
## PIN Changes
|
||||
|
||||
PIN changes require the current PIN and a different four-digit new PIN. The
|
||||
command is only valid from the full bank interface.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["mode", "bank"],
|
||||
["atmAuthorized", false]
|
||||
];
|
||||
|
||||
private _result = "forge_server" callExtension ["bank:hot:change_pin", [
|
||||
getPlayerUID player,
|
||||
"1234",
|
||||
"5678",
|
||||
toJSON _context
|
||||
]];
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```sqf
|
||||
|
||||
@ -44,6 +44,9 @@ Rules validated by the Rust service:
|
||||
- `funds`, reputation, and credit line amounts cannot be negative.
|
||||
- Player registration is rejected when the player already belongs to a
|
||||
non-default organization.
|
||||
- Player registration through the server org addon requires a $50,000 personal
|
||||
funds registration fee. The fee is charged from the player's bank balance
|
||||
first, then on-hand cash if needed.
|
||||
|
||||
## Durable Commands
|
||||
|
||||
@ -158,6 +161,10 @@ private _fleet = createHashMapFromArray [
|
||||
|
||||
## Register from UI Context
|
||||
|
||||
The server-side `forge_server_org` registration flow charges the $50,000
|
||||
registration fee before completing organization creation. If the organization
|
||||
service rejects the registration, the bank charge is refunded.
|
||||
|
||||
```sqf
|
||||
private _context = createHashMapFromArray [
|
||||
["requesterUid", getPlayerUID player],
|
||||
|
||||
@ -32,8 +32,8 @@ state.
|
||||
- bank/ATM mode
|
||||
- browser ready handling
|
||||
- account hydrate and sync responses
|
||||
- deposit, withdrawal, transfer, earnings deposit, credit repayment, and PIN
|
||||
requests
|
||||
- deposit, withdrawal, transfer, earnings deposit, credit repayment, PIN
|
||||
validation, and PIN change requests
|
||||
- browser notice delivery
|
||||
|
||||
## Browser Events
|
||||
@ -48,6 +48,7 @@ state.
|
||||
| `bank::depositEarnings::request` | Request earnings deposit. |
|
||||
| `bank::repayCreditLine::request` | Request credit-line repayment. |
|
||||
| `bank::pin::request` | Forward PIN validation request. |
|
||||
| `bank::pin::change::request` | Forward current and new PIN values for a PIN change. |
|
||||
| `bank::close` | Dispose bridge screen state and close the display. |
|
||||
|
||||
## Browser Response Events
|
||||
@ -76,6 +77,10 @@ Balances, PIN authorization, transfers, checkout charges, credit lines, and
|
||||
persistence are server-owned. The client should only display account data and
|
||||
request mutations through server events.
|
||||
|
||||
PIN changes are available from the full bank UI only. The browser validates the
|
||||
current, new, and confirmation fields, but the server extension remains
|
||||
authoritative and persists the updated PIN.
|
||||
|
||||
## Related Guides
|
||||
|
||||
- [Bank Usage Guide](/server-modules/bank)
|
||||
|
||||
@ -3,6 +3,10 @@ title: "Client Organization Usage Guide"
|
||||
description: "The client organization addon provides the organization portal UI and browser bridge for login, registration, membership, invites, credit lines, leave and disband flows, assets, fleet, and treasury display."
|
||||
---
|
||||
|
||||
Organization registration requires $50,000 in personal funds. The server org
|
||||
addon enforces and charges the fee; the browser only displays the requirement
|
||||
and submits the registration request.
|
||||
|
||||
## Open Organization UI
|
||||
|
||||
```sqf
|
||||
|
||||
@ -56,6 +56,8 @@ pub struct BankCheckoutContext {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BankPinContext {
|
||||
pub mode: String,
|
||||
#[serde(default)]
|
||||
pub atm_authorized: bool,
|
||||
}
|
||||
|
||||
impl Bank {
|
||||
|
||||
@ -310,6 +310,46 @@ impl<R: BankRepository, H: BankHotRepository> BankHotStateService<R, H> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn change_pin(
|
||||
&self,
|
||||
key: String,
|
||||
current_pin: String,
|
||||
new_pin: String,
|
||||
context: BankPinContext,
|
||||
) -> Result<BankMutationResult, String> {
|
||||
let mode = context.mode.trim();
|
||||
if !mode.eq_ignore_ascii_case("bank") {
|
||||
return Err("PIN changes are only available from the full bank interface.".to_string());
|
||||
}
|
||||
|
||||
if !is_four_digit_pin(¤t_pin) {
|
||||
return Err("Enter your current four-digit PIN.".to_string());
|
||||
}
|
||||
if !is_four_digit_pin(&new_pin) {
|
||||
return Err("Choose a new four-digit PIN.".to_string());
|
||||
}
|
||||
if current_pin == new_pin {
|
||||
return Err("Choose a different PIN from your current PIN.".to_string());
|
||||
}
|
||||
|
||||
let mut bank = self.get_bank(key)?;
|
||||
if current_pin != bank.pin.to_string() {
|
||||
return Err("Current PIN is incorrect.".to_string());
|
||||
}
|
||||
|
||||
bank.pin = new_pin
|
||||
.parse::<u64>()
|
||||
.map_err(|error| format!("Invalid new PIN: {}", error))?;
|
||||
bank.validate()
|
||||
.map_err(|e| format!("Validation failed: {}", e))?;
|
||||
self.repository.save(&bank)?;
|
||||
|
||||
Ok(BankMutationResult {
|
||||
account: bank.clone(),
|
||||
patch: build_patch(&bank, &["pin"])?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_bank(&self, key: String) -> Result<Bank, String> {
|
||||
let bank = self
|
||||
.repository
|
||||
@ -402,6 +442,10 @@ fn build_patch(bank: &Bank, fields: &[&str]) -> Result<HashMap<String, Value>, S
|
||||
Ok(patch)
|
||||
}
|
||||
|
||||
fn is_four_digit_pin(pin: &str) -> bool {
|
||||
pin.len() == 4 && pin.chars().all(|character| character.is_ascii_digit())
|
||||
}
|
||||
|
||||
fn validate_atm_access(context: &BankOperationContext, action: &str) -> Result<(), String> {
|
||||
if context.mode.eq_ignore_ascii_case("atm") && !context.atm_authorized {
|
||||
return Err(format!("ATM authorization is required before {}.", action));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user