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:
Jacob Schmidt 2026-05-16 12:13:13 -05:00
parent 80d2b1fc00
commit 264559306d
34 changed files with 595 additions and 20 deletions

View File

@ -3,7 +3,7 @@
## Overview ## Overview
The bank addon provides the client banking UI and browser bridge for account The bank addon provides the client banking UI and browser bridge for account
hydrate, deposits, withdrawals, transfers, PIN entry, earnings deposits, and 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 ## Dependencies
- `forge_client_common` - `forge_client_common`
@ -27,6 +27,7 @@ credit-line repayment.
- `bank::depositEarnings::request` - `bank::depositEarnings::request`
- `bank::repayCreditLine::request` - `bank::repayCreditLine::request`
- `bank::pin::request` - `bank::pin::request`
- `bank::pin::change::request`
- `bank::close` - `bank::close`
## Runtime Notes ## Runtime Notes

View File

@ -78,6 +78,11 @@ switch (_event) do {
GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]]; GVAR(BankUIBridge) call ["handleSubmitPinRequest", [_data]];
}; };
}; };
case "bank::pin::change::request": {
if !(isNil QGVAR(BankUIBridge)) then {
GVAR(BankUIBridge) call ["handleChangePinRequest", [_data]];
};
};
default { default {
hint format ["Unhandled bank UI event: %1", _event]; hint format ["Unhandled bank UI event: %1", _event];
}; };

View File

@ -63,6 +63,17 @@ GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
[SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent); [SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
true 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 { ["handleRepayCreditLineRequest", compileFinal {
params [["_data", createHashMap, [createHashMap]]]; params [["_data", createHashMap, [createHashMap]]];

File diff suppressed because one or more lines are too long

View File

@ -49,6 +49,9 @@
requestRefresh() { requestRefresh() {
return bridge.send("bank::refresh", {}); return bridge.send("bank::refresh", {});
}, },
requestChangePin(payload) {
return bridge.send("bank::pin::change::request", payload);
},
requestSubmitPin(payload) { requestSubmitPin(payload) {
return bridge.send("bank::pin::request", payload); return bridge.send("bank::pin::request", payload);
}, },

View File

@ -352,6 +352,73 @@
: "Deposit Earnings", : "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",
),
),
),
); );
} }

View File

@ -149,6 +149,53 @@
return true; 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) { function appendPinDigit(digit) {
const nextDigit = String(digit || "").trim(); const nextDigit = String(digit || "").trim();
if (!nextDigit) { if (!nextDigit) {
@ -276,6 +323,7 @@
closeBank, closeBank,
refreshBank, refreshBank,
requestAtmAmount, requestAtmAmount,
requestChangePin,
requestDeposit, requestDeposit,
requestDepositEarnings, requestDepositEarnings,
requestRepayCreditLine, requestRepayCreditLine,

View File

@ -3,7 +3,8 @@
## Overview ## Overview
The organization addon provides the client organization portal UI and bridge for The organization addon provides the client organization portal UI and bridge for
organization hydrate, registration, membership, invitations, credit lines, 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 ## Dependencies
- `forge_client_common` - `forge_client_common`

File diff suppressed because one or more lines are too long

View File

@ -46,7 +46,7 @@ ${scopeSelector} .home-feedback {
h( h(
"p", "p",
null, 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( h(
"button", "button",

View File

@ -177,7 +177,7 @@ ${scopeSelector} .form-feedback.is-error {
h( h(
"p", "p",
null, 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( h(
"ul", "ul",
@ -258,7 +258,11 @@ ${scopeSelector} .form-feedback.is-error {
h( h(
"div", "div",
{ className: "price-tag" }, { 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"), h("span", { className: "price-value" }, "$50,000"),
), ),
), ),

View File

@ -12,6 +12,8 @@ life state, phone number, email, organization, and holster state.
- `forge_server_main` - `forge_server_main`
- `forge_server_common` - `forge_server_common`
- `forge_server_extension` at runtime for actor extension calls - `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 - `forge_client_actor` for response RPCs
## Main Components ## Main Components
@ -23,6 +25,9 @@ life state, phone number, email, organization, and holster state.
## Runtime Behavior ## Runtime Behavior
- Missing persistent actors can be created from live player snapshots. - 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. - Hot actor reads are migrated and hydrated before use.
- `saveHotState` in the main addon snapshots and saves actor state on player - `saveHotState` in the main addon snapshots and saves actor state on player
disconnect and mission end. disconnect and mission end.

View File

@ -4,7 +4,7 @@
* File: fnc_initActorStore.sqf * File: fnc_initActorStore.sqf
* Author: IDSolutions * Author: IDSolutions
* Date: 2025-12-17 * Date: 2025-12-17
* Last Update: 2026-04-05 * Last Update: 2026-05-16
* Public: Yes * Public: Yes
* *
* Description: * Description:
@ -149,6 +149,108 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_uids select { _x isEqualType "" && { _x isNotEqualTo "" } } _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 { ["loadHotActor", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]]; params [["_uid", "", [""]], ["_initialize", false, [false]]];
@ -200,6 +302,13 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
false false
}; };
private _createdActor = fromJSON _createResult;
if !(_createdActor isEqualType createHashMap) then {
_createdActor = +_actor;
};
_createdActor = GVAR(ActorModel) call ["migrate", [_createdActor]];
_self call ["bootstrapNewActor", [_uid, _createdActor]];
true true
}], }],
["hydrateActorIfNeeded", compileFinal { ["hydrateActorIfNeeded", compileFinal {

View File

@ -3,7 +3,8 @@
## Overview ## Overview
The bank addon owns the SQF bridge for player accounts, cash and bank balances, The bank addon owns the SQF bridge for player accounts, cash and bank balances,
PIN/session handling, transfers, checkout charging, earnings deposits, and 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 Account truth lives in the extension hot cache. SQF handles Arma-facing
validation, client messaging, session state, and payment integration with other validation, client messaging, session state, and payment integration with other
@ -28,7 +29,7 @@ server addons.
## Supported Operations ## Supported Operations
- initialize and hydrate player bank state - initialize and hydrate player bank state
- deposit, withdraw, transfer, and deposit earnings - deposit, withdraw, transfer, and deposit earnings
- validate PIN-backed sessions - validate PIN-backed sessions and change ATM PINs
- charge checkout previews and committed purchases - charge checkout previews and committed purchases
- repay organization credit lines with rollback on failure - repay organization credit lines with rollback on failure
- save hot bank state to durable storage - save hot bank state to durable storage

View File

@ -32,6 +32,12 @@ PREP_RECOMPILE_END;
GVAR(BankSessionManager) call ["submitPin", [_uid, _pin]]; GVAR(BankSessionManager) call ["submitPin", [_uid, _pin]];
}] call CFUNC(addEventHandler); }] call CFUNC(addEventHandler);
[QGVAR(requestChangePin), {
params [["_uid", "", [""]], ["_currentPin", "", [""]], ["_newPin", "", [""]]];
GVAR(BankStore) call ["changePin", [_uid, _currentPin, _newPin]];
}] call CFUNC(addEventHandler);
[QGVAR(requestTransfer), { [QGVAR(requestTransfer), {
params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]]; params [["_uid", "", [""]], ["_target", "", [""]], ["_from", "", [""]], ["_amount", 0, [0]]];

View File

@ -496,6 +496,34 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
true 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 { ["withdraw", compileFinal {
params [["_uid", "", [""]], ["_amount", 0, [0]]]; params [["_uid", "", [""]], ["_amount", 0, [0]]];

View File

@ -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, events, UI payloads, membership syncs, and integration with actor, bank, store,
and task flows. and task flows.
Organization registration charges a $50,000 personal funds fee before the
player is assigned to the new organization.
## Dependencies ## Dependencies
- `forge_server_main` - `forge_server_main`
- `forge_server_common` - `forge_server_common`

View File

@ -780,6 +780,91 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
true 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 { ["updateOrgTreasuryFunds", compileFinal {
params [["_orgID", "", [""]], ["_funds", 0, [0]]]; params [["_orgID", "", [""]], ["_funds", 0, [0]]];
@ -1201,24 +1286,36 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["existingOrgId", _existingOrgID] ["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"]; ["org:hot:register", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"];
if !_isSuccess exitWith { if !_isSuccess exitWith {
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
_result set ["message", "Organization service was unavailable during registration."]; _result set ["message", "Organization service was unavailable during registration."];
_result _result
}; };
if !(_rawResult isEqualType "") exitWith { if !(_rawResult isEqualType "") exitWith {
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
_result set ["message", "Organization service returned an invalid registration response."]; _result set ["message", "Organization service returned an invalid registration response."];
_result _result
}; };
if ((_rawResult find "Error:") == 0) exitWith { if ((_rawResult find "Error:") == 0) exitWith {
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
_result set ["message", _rawResult select [7]]; _result set ["message", _rawResult select [7]];
_result _result
}; };
private _envelope = fromJSON _rawResult; private _envelope = fromJSON _rawResult;
if !(_envelope isEqualType createHashMap) exitWith { if !(_envelope isEqualType createHashMap) exitWith {
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
_result set ["message", "Organization service returned malformed registration data."]; _result set ["message", "Organization service returned malformed registration data."];
_result _result
}; };
@ -1232,10 +1329,12 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]]; private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]];
if (_actorPatch isEqualTo createHashMap) exitWith { if (_actorPatch isEqualTo createHashMap) exitWith {
_self call ["refundRegistrationFee", [_uid, _refundPatch]];
_result set ["message", "Failed to assign the player to the new organization."]; _result set ["message", "Failed to assign the player to the new organization."];
_result _result
}; };
_self call ["syncBankPatch", [_uid, _feeCharge getOrDefault ["patch", createHashMap]]];
_result set ["success", true]; _result set ["success", true];
_result set ["message", _envelope getOrDefault ["message", ""]]; _result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["org", _envelope getOrDefault ["org", createHashMap]]; _result set ["org", _envelope getOrDefault ["org", createHashMap]];

View File

@ -58,6 +58,7 @@ pub fn group() -> Group {
.command("deposit_earnings", deposit_earnings_hot_bank) .command("deposit_earnings", deposit_earnings_hot_bank)
.command("transfer", transfer_hot_bank) .command("transfer", transfer_hot_bank)
.command("validate_pin", validate_pin_hot_bank) .command("validate_pin", validate_pin_hot_bank)
.command("change_pin", change_pin_hot_bank)
.command("save", save_hot_bank) .command("save", save_hot_bank)
.command("remove", remove_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 { pub(crate) fn save_hot_bank(call_context: CallContext, key: String) -> String {
let resolved_uid = match resolve_uid(&key, &call_context) { let resolved_uid = match resolve_uid(&key, &call_context) {
Some(uid) => uid, Some(uid) => uid,

View File

@ -118,6 +118,16 @@ pub(super) fn route(
arguments[2].clone(), 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" => { "bank:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?; expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::save_hot_bank(call_context, arguments[0].clone())) Ok(bank::save_hot_bank(call_context, arguments[0].clone()))

View File

@ -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 ## Update an Actor
`actor:update` accepts a JSON object containing only fields to change. `actor:update` accepts a JSON object containing only fields to change.

View File

@ -2,7 +2,8 @@
The bank module stores player account balances, earnings, PINs, and transaction 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 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 ## 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:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. |
| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. | | `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. | | `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:save` | `uid` | Current hot bank JSON and async durable save. |
| `bank:hot:remove` | `uid` | `OK`. | | `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 ## Error Handling
```sqf ```sqf

View File

@ -33,8 +33,8 @@ state.
- bank/ATM mode - bank/ATM mode
- browser ready handling - browser ready handling
- account hydrate and sync responses - account hydrate and sync responses
- deposit, withdrawal, transfer, earnings deposit, credit repayment, and PIN - deposit, withdrawal, transfer, earnings deposit, credit repayment, PIN
requests validation, and PIN change requests
- browser notice delivery - browser notice delivery
## Browser Events ## Browser Events
@ -49,6 +49,7 @@ state.
| `bank::depositEarnings::request` | Request earnings deposit. | | `bank::depositEarnings::request` | Request earnings deposit. |
| `bank::repayCreditLine::request` | Request credit-line repayment. | | `bank::repayCreditLine::request` | Request credit-line repayment. |
| `bank::pin::request` | Forward PIN validation request. | | `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. | | `bank::close` | Dispose bridge screen state and close the display. |
## Browser Response Events ## 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 persistence are server-owned. The client should only display account data and
request mutations through server events. 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 ## Related Guides
- [Bank Usage Guide](./BANK_USAGE_GUIDE.md) - [Bank Usage Guide](./BANK_USAGE_GUIDE.md)

View File

@ -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 bridge for login, registration, membership, invites, credit lines, leave and
disband flows, assets, fleet, and treasury display. 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 ## Open Organization UI
```sqf ```sqf

View File

@ -22,7 +22,7 @@ docs/ Framework-level documentation
| Domain | Purpose | Client addon | Server addon | Service/model layer | Extension group | | 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:*` | | 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:*` | | 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:*` | | 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:*` | | 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: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: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: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. See [Bank Usage Guide](./BANK_USAGE_GUIDE.md) for examples.

View File

@ -45,6 +45,9 @@ Rules validated by the Rust service:
- `funds`, reputation, and credit line amounts cannot be negative. - `funds`, reputation, and credit line amounts cannot be negative.
- Player registration is rejected when the player already belongs to a - Player registration is rejected when the player already belongs to a
non-default organization. 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 ## Durable Commands
@ -159,6 +162,10 @@ private _fleet = createHashMapFromArray [
## Register from UI Context ## 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 ```sqf
private _context = createHashMapFromArray [ private _context = createHashMapFromArray [
["requesterUid", getPlayerUID player], ["requesterUid", getPlayerUID player],

View File

@ -23,7 +23,7 @@ docs/ Framework-level documentation
| Domain | Purpose | Client addon | Server addon | Service/model layer | Extension group | | 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:*` | | 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:*` | | 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:*` | | 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:*` | | 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: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: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: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. See [Bank Usage Guide](/server-modules/bank) for examples.

View File

@ -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 ## Update an Actor
`actor:update` accepts a JSON object containing only fields to change. `actor:update` accepts a JSON object containing only fields to change.

View File

@ -1,6 +1,6 @@
--- ---
title: "Bank Usage Guide" 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 ## 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:transfer` | `source_uid`, `target_uid`, `amount`, `context_json` | Transfer result JSON. |
| `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. | | `bank:hot:charge_checkout` | `uid`, `amount`, `context_json` | `{ account, patch }`. |
| `bank:hot:validate_pin` | `uid`, `pin`, `context_json` | `{}` on success. | | `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:save` | `uid` | Current hot bank JSON and async durable save. |
| `bank:hot:remove` | `uid` | `OK`. | | `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 ## Error Handling
```sqf ```sqf

View File

@ -44,6 +44,9 @@ Rules validated by the Rust service:
- `funds`, reputation, and credit line amounts cannot be negative. - `funds`, reputation, and credit line amounts cannot be negative.
- Player registration is rejected when the player already belongs to a - Player registration is rejected when the player already belongs to a
non-default organization. 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 ## Durable Commands
@ -158,6 +161,10 @@ private _fleet = createHashMapFromArray [
## Register from UI Context ## 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 ```sqf
private _context = createHashMapFromArray [ private _context = createHashMapFromArray [
["requesterUid", getPlayerUID player], ["requesterUid", getPlayerUID player],

View File

@ -32,8 +32,8 @@ state.
- bank/ATM mode - bank/ATM mode
- browser ready handling - browser ready handling
- account hydrate and sync responses - account hydrate and sync responses
- deposit, withdrawal, transfer, earnings deposit, credit repayment, and PIN - deposit, withdrawal, transfer, earnings deposit, credit repayment, PIN
requests validation, and PIN change requests
- browser notice delivery - browser notice delivery
## Browser Events ## Browser Events
@ -48,6 +48,7 @@ state.
| `bank::depositEarnings::request` | Request earnings deposit. | | `bank::depositEarnings::request` | Request earnings deposit. |
| `bank::repayCreditLine::request` | Request credit-line repayment. | | `bank::repayCreditLine::request` | Request credit-line repayment. |
| `bank::pin::request` | Forward PIN validation request. | | `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. | | `bank::close` | Dispose bridge screen state and close the display. |
## Browser Response Events ## 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 persistence are server-owned. The client should only display account data and
request mutations through server events. 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 ## Related Guides
- [Bank Usage Guide](/server-modules/bank) - [Bank Usage Guide](/server-modules/bank)

View File

@ -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." 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 ## Open Organization UI
```sqf ```sqf

View File

@ -56,6 +56,8 @@ pub struct BankCheckoutContext {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct BankPinContext { pub struct BankPinContext {
pub mode: String, pub mode: String,
#[serde(default)]
pub atm_authorized: bool,
} }
impl Bank { impl Bank {

View File

@ -310,6 +310,46 @@ impl<R: BankRepository, H: BankHotRepository> BankHotStateService<R, H> {
Ok(()) 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(&current_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> { pub fn save_bank(&self, key: String) -> Result<Bank, String> {
let bank = self let bank = self
.repository .repository
@ -402,6 +442,10 @@ fn build_patch(bank: &Bank, fields: &[&str]) -> Result<HashMap<String, Value>, S
Ok(patch) 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> { fn validate_atm_access(context: &BankOperationContext, action: &str) -> Result<(), String> {
if context.mode.eq_ignore_ascii_case("atm") && !context.atm_authorized { if context.mode.eq_ignore_ascii_case("atm") && !context.atm_authorized {
return Err(format!("ATM authorization is required before {}.", action)); return Err(format!("ATM authorization is required before {}.", action));