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
|
## 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
|
||||||
|
|||||||
@ -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];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
@ -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",
|
||||||
|
|||||||
@ -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"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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]]];
|
||||||
|
|
||||||
|
|||||||
@ -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]]];
|
||||||
|
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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]];
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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()))
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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],
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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(¤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> {
|
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));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user