diff --git a/code/__defines/computers.dm b/code/__defines/computers.dm index 710816cec40..bffa88fba25 100644 --- a/code/__defines/computers.dm +++ b/code/__defines/computers.dm @@ -8,8 +8,9 @@ #define NET_FEATURE_RECORDS BITFLAG(5) // Modifying accounts, viewing crew records etc. #define NET_FEATURE_FILESYSTEM BITFLAG(6) // Accessing mainframe filesystems. #define NET_FEATURE_DECK BITFLAG(7) // Control of docking beacons, supply, deck control. +#define NET_FEATURE_FINANCE BITFLAG(8) // Money transfers, other finance. -#define NET_ALL_FEATURES (NET_FEATURE_SOFTWAREDOWNLOAD|NET_FEATURE_COMMUNICATION|NET_FEATURE_SYSTEMCONTROL|NET_FEATURE_SECURITY|NET_FEATURE_ACCESS|NET_FEATURE_RECORDS|NET_FEATURE_FILESYSTEM|NET_FEATURE_DECK) +#define NET_ALL_FEATURES (NET_FEATURE_SOFTWAREDOWNLOAD|NET_FEATURE_COMMUNICATION|NET_FEATURE_SYSTEMCONTROL|NET_FEATURE_SECURITY|NET_FEATURE_ACCESS|NET_FEATURE_RECORDS|NET_FEATURE_FILESYSTEM|NET_FEATURE_DECK|NET_FEATURE_FINANCE) // Transfer speeds, used when downloading/uploading a file/program. #define NETWORK_SPEED_BASE 1/NETWORK_BASE_BROADCAST_STRENGTH // GQ/s transfer speed, multiplied by signal power @@ -44,6 +45,7 @@ #define PROG_UTIL "Utility" #define PROG_SEC "Security" #define PROG_MONITOR "Monitoring" +#define PROG_FINANCE "Finances" #define RECEIVER_WIRELESS 1 #define RECEIVER_STRONG_WIRELESS 2 @@ -74,4 +76,4 @@ #define OS_FILE_NO_WRITE -5 #define OS_HARDDRIVE_SPACE -6 #define OS_NETWORK_ERROR -7 -#define OS_BAD_NAME -8 \ No newline at end of file +#define OS_BAD_NAME -8 \ No newline at end of file diff --git a/code/__defines/machinery.dm b/code/__defines/machinery.dm index b629f4a63de..464385ea1bd 100644 --- a/code/__defines/machinery.dm +++ b/code/__defines/machinery.dm @@ -181,3 +181,4 @@ var/global/defer_powernet_rebuild = 0 // True if net rebuild will be called #define PART_SCANNER /obj/item/stock_parts/computer/scanner // One of several optional scanner attachments. #define PART_D_SLOT /obj/item/stock_parts/computer/drive_slot // Portable drive slot. #define PART_MSTICK /obj/item/stock_parts/computer/charge_stick_slot // Charge-slot component for transactions /w charge sticks. +#define PART_MPRINTER /obj/item/stock_parts/computer/money_printer \ No newline at end of file diff --git a/code/__defines/subsystem-priority.dm b/code/__defines/subsystem-priority.dm index 54c87443595..dc075751506 100644 --- a/code/__defines/subsystem-priority.dm +++ b/code/__defines/subsystem-priority.dm @@ -36,6 +36,7 @@ #define SS_PRIORITY_GHOST_IMAGES 10 // Updates ghost client images. #define SS_PRIORITY_ZCOPY 10 // Builds appearances for Z-Mimic. #define SS_PRIORITY_PROJECTILES 10 // Projectile processing! +#define SS_PRIORITY_MONEY_ACCOUNTS 10 // Money accounts processing. // SS_BACKGROUND #define SS_PRIORITY_OBJECTS 100 // processing_objects processing. diff --git a/code/controllers/configuration.dm b/code/controllers/configuration.dm index 51a5217e7b6..b4bf0921fd4 100644 --- a/code/controllers/configuration.dm +++ b/code/controllers/configuration.dm @@ -254,6 +254,16 @@ var/global/list/gamemode_cache = list() var/dex_malus_brainloss_threshold = 30 //The threshold of when brainloss begins to affect dexterity. + // Economy variables + var/withdraw_period = 1 DAY + var/interest_period = 1 DAY + + var/interest_mod_delay = 2 DAYS + var/withdraw_mod_delay = 3 DAYS + var/transaction_mod_delay = 2 DAYS + var/fractional_reserve_mod_delay = 3 DAYS + var/anti_tamper_mod_delay = 2 DAYS + var/static/list/protected_vars = list( "comms_password", "ban_comms_password", @@ -905,6 +915,22 @@ var/global/list/gamemode_cache = list() if("dexterity_malus_brainloss_threshold") config.dex_malus_brainloss_threshold = text2num(value) + // Economy config. + if("withdraw_period") + config.withdraw_period = value DAYS + if("interest_period") + config.interest_period = value DAYS + + if("interest_mod_delay") + config.interest_mod_delay = value DAYS + if("withdraw_mod_delay") + config.withdraw_mod_delay = value DAYS + if("transaction_mod_delay") + config.transaction_mod_delay = value DAYS + if("fractional_reserve_mod_delay") + config.fractional_reserve_mod_delay = value DAYS + if("anti_tamper_mod_delay") + config.anti_tamper_mod_delay = value DAYS else log_misc("Unknown setting in configuration: '[name]'") diff --git a/code/controllers/subsystems/accounts.dm b/code/controllers/subsystems/accounts.dm new file mode 100644 index 00000000000..bfbfc26983f --- /dev/null +++ b/code/controllers/subsystems/accounts.dm @@ -0,0 +1,73 @@ +SUBSYSTEM_DEF(money_accounts) + name = "Money Accounts" + wait = 5 MINUTES + priority = SS_PRIORITY_MONEY_ACCOUNTS + var/list/datum/money_account/all_accounts = list() + + // Independent, globally accessible accounts not tied to a network + var/list/datum/money_account/all_glob_accounts = list() + + var/list/datum/money_account/escrow/all_escrow_accounts = list() + + var/list/processing_accounts + + var/adjustment = 0 + +/datum/controller/subsystem/money_accounts/fire(resumed = FALSE) + + if(!resumed) + processing_accounts = all_accounts.Copy() + + var/current_time = REALTIMEOFDAY + adjustment + while(processing_accounts.len) + var/datum/money_account/curr_account = processing_accounts[processing_accounts.len] + processing_accounts.len-- + + if(length(curr_account.pending_modifications)) + for(var/datum/account_modification/pending_mod in curr_account.pending_modifications) + if(pending_mod.start_time + pending_mod.mod_delay <= current_time) + pending_mod.modify_account() + + if(istype(curr_account, /datum/money_account/child)) + var/datum/money_account/child/curr_child = curr_account + + if(curr_child.withdrawal_limit && (curr_child.last_withdraw_period + config.withdraw_period <= current_time)) + curr_child.current_withdrawal = 0 + curr_child.last_withdraw_period = current_time + + if(curr_child.interest_rate && (curr_child.last_interest_period + config.interest_period <= current_time)) + curr_child.accrue_interest() + curr_child.last_interest_period = current_time + + if(MC_TICK_CHECK) + return + +/datum/controller/subsystem/money_accounts/proc/accel_day() + adjustment += 1 DAY + +/datum/controller/subsystem/money_accounts/proc/decel_day() + adjustment -= 1 DAY + +/datum/controller/subsystem/money_accounts/proc/get_or_add_escrow(account_id, account_pin, account_provider) + var/datum/money_account/escrow/existing = get_escrow(account_id, account_pin, account_provider) + if(existing) + return existing + + existing = new() + existing.account_id = account_id + existing.remote_access_pin = account_pin + existing.owner_name = account_provider + + all_escrow_accounts |= existing + return existing + +/datum/controller/subsystem/money_accounts/proc/get_escrow(account_id, account_pin, account_provider) + for(var/datum/money_account/escrow/e_account in all_escrow_accounts) + if((e_account.account_id == account_id) && e_account.remote_access_pin == account_pin) + if(!account_provider || e_account.owner_name == account_provider) + return e_account + +/datum/controller/subsystem/money_accounts/proc/get_escrow_provider_ids() + . = list() + for(var/datum/money_account/escrow/e_account in all_escrow_accounts) + . |= e_account.owner_name \ No newline at end of file diff --git a/code/controllers/subsystems/jobs.dm b/code/controllers/subsystems/jobs.dm index 82d9af389e1..9c697c13448 100644 --- a/code/controllers/subsystems/jobs.dm +++ b/code/controllers/subsystems/jobs.dm @@ -517,7 +517,7 @@ SUBSYSTEM_DEF(jobs) var/datum/money_account/department_account = department_accounts[job.primary_department] if(department_account) - remembered_info += "Your department's account number is: #[department_account.account_number]
" + remembered_info += "Your department's account number is: #[department_account.account_id]
" remembered_info += "Your department's account pin is: [department_account.remote_access_pin]
" remembered_info += "Your department's account funds are: [department_account.format_value_by_currency(department_account.money)]
" diff --git a/code/controllers/subsystems/ticker.dm b/code/controllers/subsystems/ticker.dm index 95447d95722..29ea4aa9a8f 100644 --- a/code/controllers/subsystems/ticker.dm +++ b/code/controllers/subsystems/ticker.dm @@ -420,10 +420,10 @@ Helpers if(dronecount) to_world("There [dronecount>1 ? "were" : "was"] [dronecount] industrious maintenance drone\s at the end of this round.") - if(all_money_accounts.len) - var/datum/money_account/max_profit = all_money_accounts[1] - var/datum/money_account/max_loss = all_money_accounts[1] - for(var/datum/money_account/D in all_money_accounts) + if(SSmoney_accounts.all_glob_accounts.len) + var/datum/money_account/max_profit = SSmoney_accounts.all_glob_accounts[1] + var/datum/money_account/max_loss = SSmoney_accounts.all_glob_accounts[1] + for(var/datum/money_account/D in SSmoney_accounts.all_glob_accounts) if(D == vendor_account) //yes we know you get lots of money continue var/saldo = D.get_balance() diff --git a/code/datums/outfits/jobs/job.dm b/code/datums/outfits/jobs/job.dm index a7b79f91b94..b1d7ced48ce 100644 --- a/code/datums/outfits/jobs/job.dm +++ b/code/datums/outfits/jobs/job.dm @@ -19,5 +19,5 @@ return if(H.mind) if(H.mind.initial_account) - C.associated_account_number = H.mind.initial_account.account_number + C.associated_account_id = H.mind.initial_account.account_id return C diff --git a/code/game/jobs/job/_job.dm b/code/game/jobs/job/_job.dm index 587b156d685..3ceccebbdb1 100644 --- a/code/game/jobs/job/_job.dm +++ b/code/game/jobs/job/_job.dm @@ -1,5 +1,5 @@ /datum/job - var/title // The name of the job + var/title // The name of the job var/list/software_on_spawn = list() // Defines the software files that spawn on tablets and labtops var/list/department_types = list() // What departments the job is in. var/autoset_department = TRUE // If department list is empty, use map default. @@ -150,14 +150,14 @@ return // You are too poor for an account. //give them an account in the station database - var/datum/money_account/M = create_account("[H.real_name]'s account", H.real_name, money_amount) + var/datum/money_account/M = create_glob_account("[H.real_name]'s account", H.real_name, money_amount) var/cash_on_hand = create_cash_on_hand(H, M) // Store their financial info. if(H.mind) var/remembered_info = "" - remembered_info += "Your account number is: #[M.account_number]
" + remembered_info += "Your account number is: #[M.account_id]
" remembered_info += "Your account pin is: [M.remote_access_pin]
" - remembered_info += "Your account funds are: [M.format_value_by_currency(M.money)]
" + remembered_info += "Your account funds are: [M.format_value_by_currency(M.get_balance())]
" if(M.transaction_log.len) var/datum/transaction/T = M.transaction_log[1] remembered_info += "Your account was created: [T.time], [T.date] at [T.get_source_name()]
" diff --git a/code/game/machinery/_machines_base/stock_parts/money_printer.dm b/code/game/machinery/_machines_base/stock_parts/money_printer.dm new file mode 100644 index 00000000000..65e405a5845 --- /dev/null +++ b/code/game/machinery/_machines_base/stock_parts/money_printer.dm @@ -0,0 +1,84 @@ +/obj/item/stock_parts/computer/money_printer + name = "cryptographic micro-printer" + desc = "A micro-printer capable of scanning, recycling, and printing cryptographically secured bank notes on ultra thin plastic." + icon_state = "printer" + material = /decl/material/solid/plastic + matter = list( + /decl/material/solid/fiberglass = MATTER_AMOUNT_REINFORCEMENT, + /decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT + ) + max_health = ITEM_HEALTH_NO_DAMAGE + external_slot = TRUE + + var/stored_plastic = 0 // This could be capped, but it'd place an annoying limit on the amount of money you can insert at a given time. + +/obj/item/stock_parts/computer/money_printer/examine(mob/user) + . = ..() + if(Adjacent(user)) + to_chat(user, "It has [round(stored_plastic) / SHEET_MATERIAL_AMOUNT] sheets of plastic in storage.") + +/obj/item/stock_parts/computer/money_printer/attackby(obj/item/W, mob/user) + . = ..() + if(istype(W, /obj/item/stack/material) && W.get_material_type() == /decl/material/solid/plastic) + var/obj/item/stack/material/stack = W + + stored_plastic += SHEET_MATERIAL_AMOUNT * stack.amount + to_chat(user, "You insert the plastic into \the [src]'s storage.") + user.drop_from_inventory(stack) + qdel(stack) + + if(IS_SCREWDRIVER(W) && !istype(loc, /obj/machinery)) + to_chat(user, "You pry out the plastic reserves of \the [src].") + SSmaterials.create_object(/decl/material/solid/plastic, get_turf(src), round(stored_plastic / SHEET_MATERIAL_AMOUNT)) + stored_plastic = 0 + + if(istype(W, /obj/item/cash)) + if(!loc) + return + var/datum/extension/interactive/os/current_os = get_extension(loc, /datum/extension/interactive/os) + if(!current_os) + to_chat(user, SPAN_WARNING("\The [src] must be installed before it can be used!")) + return + var/obj/item/cash/receiving = W + var/decl/currency/receiving_currency = GET_DECL(receiving.currency) + if(receiving_currency.material != /decl/material/solid/plastic) + to_chat(user, SPAN_WARNING("\The [src] cannot accept cash of this currency!")) + return + + var/amount_taken = current_os.process_cash(receiving, usr) + if(!amount_taken) + return + else + playsound(get_turf(src), pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, 1) + to_chat(usr, SPAN_NOTICE("You insert [FLOOR(amount_taken / receiving_currency.absolute_value)] [receiving_currency.name] into \the [src].")) + receiving.adjust_worth(-amount_taken) + + stored_plastic += amount_taken*max(1, round(SHEET_MATERIAL_AMOUNT/10)) + + return + +/obj/item/stock_parts/computer/money_printer/proc/can_print(amount, currency_type) + // TODO: Support for non-plastic currencies + var/decl/currency/printed_currency = GET_DECL(currency_type) + if(printed_currency.material != /decl/material/solid/plastic) + return FALSE + return (stored_plastic >= amount*max(1, round(SHEET_MATERIAL_AMOUNT/10))) + +// BRRRR +/obj/item/stock_parts/computer/money_printer/proc/print_money(amount, currency_type, mob/user) + if(!can_print(amount, currency_type)) + return FALSE + + stored_plastic -= amount*max(1, round(SHEET_MATERIAL_AMOUNT/10)) + + var/obj/item/cash/cash = new(get_turf(src)) + cash.set_currency(currency_type) + cash.adjust_worth(amount) + + if(user) + user.put_in_hands(cash) + + return TRUE + +/obj/item/stock_parts/computer/money_printer/filled + stored_plastic = 50*SHEET_MATERIAL_AMOUNT \ No newline at end of file diff --git a/code/game/objects/items/weapons/cards_ids.dm b/code/game/objects/items/weapons/cards_ids.dm index 14a859a1484..fa7927c30cf 100644 --- a/code/game/objects/items/weapons/cards_ids.dm +++ b/code/game/objects/items/weapons/cards_ids.dm @@ -156,7 +156,7 @@ var/global/const/NO_EMAG_ACT = -50 slot_flags = SLOT_ID var/list/access = list() var/registered_name = "Unknown" // The name registered_name on the card - var/associated_account_number = 0 + var/associated_account_id = 0 // Associated network account. For normal IDs this is simply informational, but for network enabled IDs this is used for group-based access. var/list/associated_network_account = list("login" = "", "password" = "") diff --git a/code/game/objects/items/weapons/circuitboards/machinery/network.dm b/code/game/objects/items/weapons/circuitboards/machinery/network.dm index 2d0397c012e..614673fdabe 100644 --- a/code/game/objects/items/weapons/circuitboards/machinery/network.dm +++ b/code/game/objects/items/weapons/circuitboards/machinery/network.dm @@ -93,4 +93,17 @@ /obj/item/stock_parts/scanning_module = 1, /obj/item/stock_parts/subspace/transmitter = 1, /obj/item/stock_parts/micro_laser/high = 1 - ) \ No newline at end of file + ) + +/obj/item/stock_parts/circuitboard/banking_mainframe + name = "circuitboard (banking mainframe)" + build_path = /obj/machinery/network/bank + origin_tech = "{'programming':4,'magnets':3}" + req_components = list( + /obj/item/stock_parts/capacitor = 1, + /obj/item/stock_parts/scanning_module = 2 + ) + + additional_spawn_components = list( + /obj/item/stock_parts/power/apc/buildable = 1 + ) \ No newline at end of file diff --git a/code/modules/economy/cael/ATM.dm b/code/modules/economy/cael/ATM.dm index 4abffb17519..2ca59f862a4 100644 --- a/code/modules/economy/cael/ATM.dm +++ b/code/modules/economy/cael/ATM.dm @@ -15,7 +15,7 @@ var/datum/money_account/authenticated_account var/number_incorrect_tries = 0 - var/previous_account_number = 0 + var/previous_account_id = 0 var/max_pin_attempts = 3 var/ticks_left_locked_down = 0 var/ticks_left_timeout = 0 @@ -90,7 +90,7 @@ if(!user.unEquip(idcard, src)) return held_card = idcard - if(authenticated_account && held_card.associated_account_number != authenticated_account.account_number) + if(authenticated_account && held_card.associated_account_id != authenticated_account.account_id) authenticated_account = null attack_hand(user) @@ -99,7 +99,10 @@ var/obj/item/cash/dolla = I //deposit the cash - if(authenticated_account.deposit(dolla.absolute_worth, "Credit deposit", machine_id)) + var/err = authenticated_account.deposit(dolla.absolute_worth, "Credit deposit", machine_id) + if(err) + to_chat(user, "Cash deposit failed: [err].") + else playsound(loc, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, 1) to_chat(user, "You insert [I] into [src].") @@ -112,7 +115,10 @@ if(lock.locked) to_chat(user, SPAN_WARNING("Cannot transfer funds from a locked [stick.name].")) else - if(authenticated_account.deposit(stick.loaded_worth, "Credit deposit", machine_id)) + var/err = authenticated_account.deposit(stick.loaded_worth, "Credit deposit", machine_id) + if(err) + to_chat(user, "Cash deposit failed: [err].") + else playsound(loc, pick('sound/items/polaroid1.ogg', 'sound/items/polaroid2.ogg'), 50, 1) to_chat(user, "You insert [I] into [src].") @@ -193,7 +199,7 @@ t += "" t += "Print
" if(TRANSFER_FUNDS) - t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.money)]
" + t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.get_balance())]
" t += "
" t += "" t += "" @@ -203,7 +209,7 @@ t += "
" t += "
" else - t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.money)]" + t += "Account balance: [authenticated_account.format_value_by_currency(authenticated_account.get_balance())]" t += "
" t += "" t += " Cash Chargecard
" @@ -238,7 +244,7 @@ t += "
" t += "
" if(user?.mind?.initial_account) - t += "You recall your personal account number is #[user.mind.initial_account.account_number] and your PIN is [user.mind.initial_account.remote_access_pin].
" + t += "You recall your personal account number is #[user.mind.initial_account.account_id] and your PIN is [user.mind.initial_account.remote_access_pin].
" var/datum/browser/written_digital/popup = new(user, "ATM", machine_id) popup.set_content(jointext(t,null)) @@ -257,12 +263,16 @@ transfer_amount = round(transfer_amount, 0.01) if(transfer_amount <= 0) alert("That is not a valid amount.") - else if(transfer_amount <= authenticated_account.money) - var/target_account_number = text2num(href_list["target_acc_number"]) + else if(transfer_amount <= authenticated_account.get_balance()) + var/target_account_id = text2num(href_list["target_acc_number"]) var/transfer_purpose = href_list["purpose"] - var/datum/money_account/target_account = get_account(target_account_number) - if(target_account && authenticated_account.transfer(target_account, transfer_amount, transfer_purpose)) - to_chat(usr, "[html_icon(src)]Funds transfer successful.") + var/datum/money_account/target_account = get_glob_account(target_account_id) + if(target_account) + var/err = authenticated_account.transfer(target_account, transfer_amount, transfer_purpose) + if(err) + to_chat(usr, "[html_icon(src)]Funds transfer failed: [err].") + else + to_chat(usr, "[html_icon(src)]Funds transfer successful.") else to_chat(usr, "[html_icon(src)]Funds transfer failed.") @@ -287,36 +297,36 @@ var/tried_account_num = text2num(href_list["account_num"]) //We WILL need an account number entered manually if security is high enough, do not automagic account number if(!tried_account_num && login_card && (account_security_level != 2)) - tried_account_num = login_card.associated_account_number + tried_account_num = login_card.associated_account_id var/tried_pin = text2num(href_list["account_pin"]) //We'll need more information if an account's security is greater than zero so let's find out what the security setting is var/datum/money_account/D //Below is to avoid a runtime if(tried_account_num) - D = get_account(tried_account_num) + D = get_glob_account(tried_account_num) if(D) account_security_level = D.security_level - authenticated_account = attempt_account_access(tried_account_num, tried_pin, (login_card?.associated_account_number == tried_account_num)) + authenticated_account = attempt_account_access(tried_account_num, tried_pin, (login_card?.associated_account_id == tried_account_num)) if(!authenticated_account) number_incorrect_tries++ //let's not count an incorrect try on someone who just needs to put in more information - if(previous_account_number == tried_account_num && tried_pin) + if(previous_account_id == tried_account_num && tried_pin) if(number_incorrect_tries >= max_pin_attempts) //lock down the atm ticks_left_locked_down = 30 playsound(src, 'sound/machines/buzz-two.ogg', 50, 1) //create an entry in the account transaction log - var/datum/money_account/failed_account = get_account(tried_account_num) + var/datum/money_account/failed_account = get_glob_account(tried_account_num) if(failed_account) failed_account.log_msg("Unauthorized login attempt", machine_id) else to_chat(usr, "[html_icon(src)] Incorrect pin/account combination entered, [max_pin_attempts - number_incorrect_tries] attempts remaining.") - previous_account_number = tried_account_num + previous_account_id = tried_account_num playsound(src, 'sound/machines/buzz-sigh.ogg', 50, 1) else to_chat(usr, "[html_icon(src)] Unable to log in to account, additional information may be required.") @@ -331,7 +341,7 @@ to_chat(usr, "[html_icon(src)] Access granted. Welcome user '[authenticated_account.owner_name].'") - previous_account_number = tried_account_num + previous_account_id = tried_account_num if("e_withdrawal") var/amount = max(text2num(href_list["funds_amount"]),0) amount = round(amount, 0.01) @@ -343,41 +353,43 @@ alert("That amount exceeds the maximum amount holdable by charge sticks from this machine ([cur.format_value(initial(E.max_worth))]).") else if(authenticated_account && amount > 0) //create an entry in the account transaction log - if(authenticated_account.withdraw(amount, "Credit withdrawal", machine_id)) + var/err = authenticated_account.withdraw(amount, "Credit withdrawal", machine_id) + if(err) + to_chat(usr, "[html_icon(src)]Withdrawal failed: [err].") + else playsound(src, 'sound/machines/chime.ogg', 50, 1) E = new charge_stick_type(loc) E.adjust_worth(amount) E.creator = authenticated_account.owner_name usr.put_in_hands(E) - else - to_chat(usr, "[html_icon(src)]You don't have enough funds to do that!") + if("withdrawal") var/amount = max(text2num(href_list["funds_amount"]),0) amount = round(amount, 0.01) if(amount <= 0) alert("That is not a valid amount.") else if(authenticated_account && amount > 0) - //remove the money - if(authenticated_account.withdraw(amount, "Credit withdrawal", machine_id)) + var/err = authenticated_account.withdraw(amount, "Credit withdrawal", machine_id) + if(err) + to_chat(usr, "[html_icon(src)]Withdrawal failed: [err].") + else playsound(src, 'sound/machines/chime.ogg', 50, 1) var/obj/item/cash/cash = new(get_turf(usr)) cash.adjust_worth(amount) usr.put_in_hands(src) - else - to_chat(usr, "[html_icon(src)]You don't have enough funds to do that!") if("balance_statement") if(authenticated_account) var/txt txt = "Automated Teller Account Statement

" txt += "Account holder: [authenticated_account.owner_name]
" - txt += "Account number: [authenticated_account.account_number]
" - txt += "Balance: [authenticated_account.format_value_by_currency(authenticated_account.money)]
" + txt += "Account number: [authenticated_account.account_id]
" + txt += "Balance: [authenticated_account.format_value_by_currency(authenticated_account.get_balance())]
" txt += "Date and time: [stationtime2text()], [stationdate2text()]

" txt += "Service terminal ID: [machine_id]
" var/obj/item/paper/R = new(src.loc, null, txt, "Account balance: [authenticated_account.owner_name]") R.apply_custom_stamp( - overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR), + overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR), "by the [machine_id]") if(prob(50)) @@ -387,10 +399,10 @@ if ("print_transaction") if(authenticated_account) var/txt - + txt = "Transaction logs
" txt += "Account holder: [authenticated_account.owner_name]
" - txt += "Account number: [authenticated_account.account_number]
" + txt += "Account number: [authenticated_account.account_id]
" txt += "Date and time: [stationtime2text()], [stationdate2text()]

" txt += "Service terminal ID: [machine_id]
" txt += "" @@ -414,7 +426,7 @@ txt += "
" var/obj/item/paper/R = new(src.loc, null, txt, "Transaction logs: [authenticated_account.owner_name]") R.apply_custom_stamp( - overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR), + overlay_image('icons/obj/bureaucracy.dmi', "paper_stamp-boss", flags = RESET_COLOR), "by the [machine_id]") if(prob(50)) diff --git a/code/modules/economy/cael/Accounts.dm b/code/modules/economy/cael/Accounts.dm index ca6ce1288cb..9d8d08ce270 100644 --- a/code/modules/economy/cael/Accounts.dm +++ b/code/modules/economy/cael/Accounts.dm @@ -1,7 +1,7 @@ /datum/money_account var/account_name = "" var/owner_name = "" - var/account_number = 0 + var/account_id = 0 var/remote_access_pin = 0 var/money = 0 var/list/transaction_log = list() @@ -12,10 +12,22 @@ var/account_type = ACCOUNT_TYPE_PERSONAL var/currency + var/list/pending_modifications = list() // Modifications pending for the account. + // For non parent/child accounts this has no implementation. + /datum/money_account/New(var/account_type) account_type = account_type ? account_type : ACCOUNT_TYPE_PERSONAL if(!ispath(currency, /decl/currency)) currency = global.using_map.default_currency + SSmoney_accounts.all_accounts |= src + +/datum/money_account/Destroy(force) + SSmoney_accounts.all_accounts -= src + for(var/datum/account_modification/pending_mod in pending_modifications) + qdel(pending_mod) + + transaction_log.Cut() // Transactions are shared, but don't contain references to accounts after performance + . = ..() /datum/money_account/proc/format_value_by_currency(var/amt) var/decl/currency/cur = GET_DECL(currency) @@ -23,12 +35,19 @@ // is_source inverts the amount. /datum/money_account/proc/add_transaction(var/datum/transaction/T, is_source = FALSE) - money = max(is_source ? money - T.amount : money + T.amount, 0) + var/money_adjusted = is_source ? -T.amount : T.amount + adjust_money(money_adjusted) transaction_log += T + return money_adjusted /datum/money_account/proc/get_balance() return money +// Returns null on success, or an error string otherwise +/datum/money_account/proc/can_afford(amount, /datum/money_account/receiver) + if(money < amount) + return "Insufficient funds" + /datum/money_account/proc/log_msg(msg, machine_id) var/datum/transaction/log/T = new(src, msg, machine_id) return T.perform() @@ -45,57 +64,14 @@ var/datum/transaction/T = new(src, to_account, amount, purpose) return T.perform() +// Adjusts the money in the account by the given amount. Regardless of implementation, this can not fail. +/datum/money_account/proc/adjust_money(amount) + money = max(money + amount, 0) -/proc/create_account(var/account_name = "Default account name", var/owner_name, var/starting_funds = 0, var/account_type = ACCOUNT_TYPE_PERSONAL, var/obj/machinery/computer/account_database/source_db) - - //create a new account - var/datum/money_account/M = new() - M.account_name = account_name - M.owner_name = (owner_name ? owner_name : account_name) - M.account_type = account_type - M.remote_access_pin = rand(1111, 111111) - - //create an entry in the account transaction log for when it was created - //note that using the deposit proc on the account isn't really feasible because we need to change the transaction data before performing it - var/datum/transaction/singular/T = new(M, (source_db ? source_db.machine_id : "NTGalaxyNet Terminal #[rand(111,1111)]"), starting_funds, "Account creation") - if(!source_db) - //set a random date, time and location some time over the past few decades - T.date = "[num2text(rand(1,31))] [pick("January","February","March","April","May","June","July","August","September","October","November","December")], [global.using_map.game_year - rand(8,18)]" - T.time = "[rand(0,24)]:[rand(11,59)]" - - M.account_number = random_id("station_account_number", 111111, 999999) - else - M.account_number = next_account_number - next_account_number += rand(1,25) - - //create a sealed package containing the account details - var/txt - txt += "Account details (confidential)


" - txt += "Account holder: [M.owner_name]
" - txt += "Account number: [M.account_number]
" - txt += "Account pin: [M.remote_access_pin]
" - txt += "Starting balance: [M.format_value_by_currency(M.money)]
" - txt += "Date and time: [stationtime2text()], [stationdate2text()]

" - txt += "Creation terminal ID: [source_db.machine_id]
" - txt += "Authorised officer overseeing creation: [source_db.held_card.registered_name]
" - - var/obj/item/paper/R = new /obj/item/paper(null, null, txt, "Account information: [M.account_name]") - R.apply_custom_stamp(overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), "by the Accounts Database") - new /obj/item/parcel(source_db.loc, null, R) - - //add the account - T.perform() - all_money_accounts.Add(M) - - return M - -//this returns the first account datum that matches the supplied accnum/pin combination, it returns null if the combination did not match any account -/proc/attempt_account_access(var/attempt_account_number, var/attempt_pin_number, var/valid_card) - var/datum/money_account/D = get_account(attempt_account_number) - if(D && (D.security_level != 2 || valid_card) && (!D.security_level || D.remote_access_pin == attempt_pin_number) ) - return D +/datum/money_account/proc/has_mod_type(type) + for(var/datum/account_modification/pending in pending_modifications) + if(ispath(pending.type, type)) + return TRUE -/proc/get_account(var/account_number) - for(var/datum/money_account/D in all_money_accounts) - if(D.account_number == account_number) - return D +/datum/money_account/proc/format_account_id() + return isnum(account_id) ? "#[account_id]" : account_id \ No newline at end of file diff --git a/code/modules/economy/cael/Accounts_DB.dm b/code/modules/economy/cael/Accounts_DB.dm index b6ccb270f14..2219a304bbc 100644 --- a/code/modules/economy/cael/Accounts_DB.dm +++ b/code/modules/economy/cael/Accounts_DB.dm @@ -57,14 +57,14 @@ data["machine_id"] = machine_id data["creating_new_account"] = creating_new_account data["detailed_account_view"] = !!detailed_account_view - data["station_account_number"] = station_account.account_number + data["station_account_id"] = station_account.account_id data["transactions"] = null data["accounts"] = null if (detailed_account_view) - data["account_number"] = detailed_account_view.account_number + data["account_id"] = detailed_account_view.account_id data["owner_name"] = detailed_account_view.owner_name - data["money"] = detailed_account_view.money + data["money"] = detailed_account_view.get_balance() data["suspended"] = detailed_account_view.suspended var/list/trx[0] @@ -81,10 +81,10 @@ data["transactions"] = trx var/list/accounts[0] - for(var/i=1, i<=all_money_accounts.len, i++) - var/datum/money_account/D = all_money_accounts[i] + for(var/i=1, i<= length(SSmoney_accounts.all_glob_accounts), i++) + var/datum/money_account/D = SSmoney_accounts.all_glob_accounts[i] accounts.Add(list(list(\ - "account_number"=D.account_number,\ + "account_id"=D.account_id,\ "owner_name"=D.owner_name,\ "suspended"=D.suspended ? "SUSPENDED" : "",\ "account_index"=i))) @@ -121,10 +121,10 @@ starting_funds = clamp(starting_funds, 0, station_account.money) // Not authorized to put the station in debt. starting_funds = min(starting_funds, fund_cap) // Not authorized to give more than the fund cap. - var/datum/money_account/new_account = create_account("[account_name]'s Personal Account", account_name, starting_funds, ACCOUNT_TYPE_PERSONAL, src) + var/datum/money_account/new_account = create_glob_account("[account_name]'s Personal Account", account_name, starting_funds, ACCOUNT_TYPE_PERSONAL, src) if(starting_funds > 0) //subtract the money - station_account.money -= starting_funds + station_account.adjust_money(-starting_funds) //create a transaction log entry new_account.deposit(starting_funds, "New account activation", machine_id) @@ -150,8 +150,8 @@ if("view_account_detail") var/index = text2num(href_list["account_index"]) - if(index && index <= all_money_accounts.len) - detailed_account_view = all_money_accounts[index] + if(index && index <= length(SSmoney_accounts.all_glob_accounts)) + detailed_account_view = SSmoney_accounts.all_glob_accounts[index] if("view_accounts_list") detailed_account_view = null @@ -168,12 +168,12 @@ var/text var/obj/item/paper/P = new(loc) if (detailed_account_view) - P.SetName("account #[detailed_account_view.account_number] details") - var/title = "Account #[detailed_account_view.account_number] Details" + P.SetName("account #[detailed_account_view.account_id] details") + var/title = "Account #[detailed_account_view.account_id] Details" text = {" [accounting_letterhead(title)] Holder: [detailed_account_view.owner_name]
- Balance: [detailed_account_view.format_value_by_currency(detailed_account_view.money)]
+ Balance: [detailed_account_view.format_value_by_currency(detailed_account_view.get_balance())]
Status: [detailed_account_view.suspended ? "Suspended" : "Active"]
Transactions: ([detailed_account_view.transaction_log.len])
@@ -222,13 +222,13 @@ "} - for(var/i=1, i<=all_money_accounts.len, i++) - var/datum/money_account/D = all_money_accounts[i] + for(var/i=1, i<= length(SSmoney_accounts.all_glob_accounts), i++) + var/datum/money_account/D = SSmoney_accounts.all_glob_accounts[i] text += {" - + - + "} diff --git a/code/modules/economy/cael/EFTPOS.dm b/code/modules/economy/cael/EFTPOS.dm index b82e5eb2a39..87dfcddcdc6 100644 --- a/code/modules/economy/cael/EFTPOS.dm +++ b/code/modules/economy/cael/EFTPOS.dm @@ -35,10 +35,10 @@ txt += "2. Lock the new transaction. If you want to modify or cancel the transaction, you simply have to reset your EFTPOS device.
" txt += "3. Give the EFTPOS device to your customer, he/she must finish the transaction by swiping their ID card or a charge card with enough funds.
" txt += "4. If everything is done correctly, the money will be transferred. To unlock the device you will have to reset the EFTPOS device.
" - + var/obj/item/paper/R = new(src.loc, null, txt, "Steps to success: Correct EFTPOS Usage") R.apply_custom_stamp( - overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), + overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), "by \the [src]") //by default, connect to the station account @@ -46,14 +46,14 @@ linked_account = station_account /obj/item/eftpos/proc/print_reference() - var/obj/item/paper/R = new(src.loc, null, - "[eftpos_name] reference

Access code: [access_code]

Do not lose or misplace this code.
", + var/obj/item/paper/R = new(src.loc, null, + "[eftpos_name] reference

Access code: [access_code]

Do not lose or misplace this code.
", "Reference: [eftpos_name]") R.apply_custom_stamp( - overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), + overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), "by the [src]") - - + + var/obj/item/parcel/D = new(R.loc, null, R, "EFTPOS access code") D.attach_label(usr, null, "EFTPOS access code") @@ -203,11 +203,11 @@ if(linked_account) if(!linked_account.suspended) var/attempt_pin = "" - var/datum/money_account/D = get_account(C.associated_account_number) + var/datum/money_account/D = get_glob_account(C.associated_account_id) if(D && D.security_level) attempt_pin = input("Enter pin code", "EFTPOS transaction") as num D = null - D = attempt_account_access(C.associated_account_number, attempt_pin, TRUE) + D = attempt_account_access(C.associated_account_id, attempt_pin, TRUE) if(D) //transfer the money if(D.transfer(linked_account, transaction_amount, "[transaction_purpose] (via [eftpos_name]/[machine_id])")) diff --git a/code/modules/economy/cael/Transactions.dm b/code/modules/economy/cael/Transactions.dm index 5b1867f3dff..90df67aa58d 100644 --- a/code/modules/economy/cael/Transactions.dm +++ b/code/modules/economy/cael/Transactions.dm @@ -11,8 +11,12 @@ var/datum/money_account/target = null var/datum/money_account/source = null + // After the transaction is performed these are created and target/source nulled for GC purposes. + var/target_name + var/source_name + // Can also do negative amounts to transfer from target to source -/datum/transaction/New(_source, _target, _amount, _purpose) +/datum/transaction/New(datum/money_account/_source, datum/money_account/_target, _amount, _purpose) ..() date = stationdate2text() @@ -24,36 +28,48 @@ source = _source target = _target +/datum/transaction/Destroy(force) + target = null + source = null + . = ..() + +// Whether or not the transaction is valid. Returns null on success, error message on failure. /datum/transaction/proc/valid() // None of the involved accounts can be suspended - if(target.suspended || source.suspended) - return FALSE - + if(target.suspended) + return "Account [target.format_account_id()] ([target.account_name]) is suspended" + if(source.suspended) + return "Account [source.format_account_id()] ([source.account_name]) is suspended" // The payer must be able to afford the transaction - if(!source_can_afford()) - return FALSE - - return TRUE + var/afford_error = source.can_afford(amount, target) + if(afford_error) + return "Account [source.format_account_id()] ([source.account_name]) cannot afford the transaction. [afford_error]" // Whether or not the source account can afford the transaction /datum/transaction/proc/source_can_afford() return (source.money >= amount) /datum/transaction/proc/get_target_name() - return target.account_name + return target ? target.account_name : target_name /datum/transaction/proc/get_source_name() - return source.account_name + return source ? source.account_name : source_name -// Performs the transaction on both the source and target accounts +// Performs the transaction on both the source and target accounts. +// Returns null on success, error on failure. /datum/transaction/proc/perform() - if(!valid()) - return FALSE + var/error = valid() + if(error) + return error target.add_transaction(src) source.add_transaction(src, TRUE) - return TRUE + target_name = get_target_name() + source_name = get_source_name() + + target = null + source = null /* Transactions that only involve one account @@ -65,27 +81,37 @@ return (amount > 0) /datum/transaction/singular/valid() - return !source.suspended && source_can_afford() - -/datum/transaction/singular/source_can_afford() - return source.money + amount >= 0 + if(source.suspended) + return "Account [source.format_account_id()] is suspended" + if(!is_deposit()) + var/afford_error = source.can_afford(-amount, target) + if(afford_error) + return "Account [source.format_account_id()] cannot afford the transaction. [afford_error]" // For deposits: returns the name of the account the money was deposited to. For withdrawals: returns the machine ID of the machine the withdrawal was made at /datum/transaction/singular/get_target_name() - return (is_deposit() ? source.account_name : target) + if(target && source) + return (is_deposit() ? source.account_name : target) + return target_name // For deposits: returns the machine ID of the machine the deposit was made to. For withdrawals: returns the name of the account the money was withdrawn from /datum/transaction/singular/get_source_name() - return (is_deposit() ? target : source.account_name) + if(target && source) + return (is_deposit() ? target : source.account_name) + return source_name /datum/transaction/singular/perform() - if(!valid()) - return FALSE + var/error = valid() + if(error) + return error source.add_transaction(src) - return TRUE + target_name = get_target_name() + source_name = get_source_name() + target = null + source = null /* Log messages These should only be made through the logmsg proc of the account you want to create a log on! @@ -94,20 +120,22 @@ /datum/transaction/log var/machine_id = "ERRID#?" -/datum/transaction/log/New(account, message, _machine_id) +/datum/transaction/log/New(datum/money_account/account, message, _machine_id) machine_id = _machine_id - + target_name = account.account_name ..(null, account, 0, "LOG: [message]") /datum/transaction/log/valid() - return TRUE + return /datum/transaction/log/get_source_name() - return machine_id + return machine_id ? machine_id : "LOG" /datum/transaction/log/perform() - if(!valid()) - return FALSE + var/error = valid() + if(error) + return error target.add_transaction(src) - return TRUE + + target = null \ No newline at end of file diff --git a/code/modules/economy/cael/account_mod.dm b/code/modules/economy/cael/account_mod.dm new file mode 100644 index 00000000000..a2f6bfcb5a0 --- /dev/null +++ b/code/modules/economy/cael/account_mod.dm @@ -0,0 +1,231 @@ +// Holder for a modification happening to an account, or some other temporary condition. +/datum/account_modification + var/name = "Account modification" + var/start_time + var/mod_delay + + var/datum/money_account/affecting + var/suspends_withdrawal_limit = FALSE // Whether or not the account modification puts a hold on withdrawal limits + var/allow_early = TRUE // Whether or not the target account can choose to activate the modification earlier + var/allow_cancel = TRUE // Whether or not a modification can be cancelled prior to activation. + +/datum/account_modification/New(n_affecting) + start_time = REALTIMEOFDAY + affecting = n_affecting + . = ..() + +/datum/account_modification/Destroy(force) + affecting.pending_modifications -= src + affecting = null + . = ..() + +/datum/account_modification/proc/modify_account() + SHOULD_CALL_PARENT(TRUE) + affecting.pending_modifications -= src + qdel(src) + +// Obtains input from the user for creation, modifying variables as necessary. CanInteract() and access etc. must be checked elsewhere. +/datum/account_modification/proc/prompt_creation(mob/user) + +// Returns a human readable notification describing the change. +/datum/account_modification/proc/get_notification() + if(suspends_withdrawal_limit) + return "As a result, withdrawal limits have been suspended." + +// Returns a list of UI data for tabulated viewing +/datum/account_modification/proc/get_ui_data() + return list("name" = name, "desc" = get_short_desc(), "allow_early" = allow_early, "allow_cancel" = allow_cancel, "suspends_wlimit" = suspends_withdrawal_limit, "countdown" = get_readable_countdown()) + +/datum/account_modification/proc/get_short_desc() + +/datum/account_modification/proc/get_readable_countdown() + return minutes_to_readable((start_time + mod_delay - REALTIMEOFDAY)/(1 MINUTES)) + +/datum/account_modification/modify_interest + name = "Interest rate modification" + var/new_interest + +/datum/account_modification/modify_interest/prompt_creation(mob/user) + var/n_interest = input(user, "Enter the new interest rate (between -1 and 1):", "Interest Rate") as num + new_interest = clamp(n_interest, -1, 1) + + var/datum/money_account/child/affected_child = affecting + if(istype(affected_child)) + if(new_interest < affected_child.interest_rate) + suspends_withdrawal_limit = TRUE + else + log_error("An interest account modification was created for a non-child account!") + +/datum/account_modification/modify_interest/New(n_affecting) + mod_delay = config.interest_mod_delay + . = ..() + +/datum/account_modification/modify_interest/modify_account() + var/datum/money_account/child/affected_child = affecting + if(istype(affected_child)) + affected_child.interest_rate = new_interest + else + log_error("An interest account modification was queued for a non-child account!") + + ..() + +/datum/account_modification/modify_interest/get_notification() + var/datum/money_account/child/affected_child = affecting + if(!istype(affected_child)) + return + . = "In [get_readable_countdown()], the interest rate on your account will be [affected_child.interest_rate > new_interest ? "lowered" : "raised"] to [new_interest]." + var/parent_notif = ..() + if(istext(parent_notif)) + return . + " " + parent_notif + +/datum/account_modification/modify_interest/get_short_desc() + var/datum/money_account/child/affected_child = affecting + return "[affected_child.interest_rate > new_interest ? "Lowers" : "Raises"] interest rate to [new_interest]" + +/datum/account_modification/modify_withdrawal + name = "Withdrawal limit modification" + var/new_withdrawal_limit + +/datum/account_modification/modify_withdrawal/prompt_creation(mob/user) + var/n_withdrawal = input(user, "Enter the new withdrawal limit:", "Withdrawal limit") as num + new_withdrawal_limit = max(0, FLOOR(n_withdrawal)) + + var/datum/money_account/child/affected_child = affecting + if(istype(affected_child)) + if(new_withdrawal_limit < affected_child.withdrawal_limit) + // Check to see if the user could get out all their money in time with the NEW withdrawal limit in the modification delay period. + // We check against the new withdrawal limit so that suddenly changing the withdrawal limit extremely low isn't as effective. + suspends_withdrawal_limit = (affected_child.money / affected_child.withdrawal_limit) > (affected_child.withdrawal_limit * new_withdrawal_limit/mod_delay) + else + log_error("A withdrawal limit account modification was created for a non-child account!") + +/datum/account_modification/modify_withdrawal/New(n_affecting) + mod_delay = config.withdraw_mod_delay + . = ..() + +/datum/account_modification/modify_withdrawal/modify_account() + var/datum/money_account/child/affected_child = affecting + if(istype(affected_child)) + affected_child.withdrawal_limit = new_withdrawal_limit + else + log_error("A withdrawal limit account modification was queued for an non-child account!") + ..() + +/datum/account_modification/modify_withdrawal/get_notification() + var/datum/money_account/child/affected_child = affecting + if(!istype(affected_child)) + return + . = "In [get_readable_countdown()], the withdrawal limit on your account will be [affected_child.withdrawal_limit > new_withdrawal_limit ? "lowered" : "raised"] to [new_withdrawal_limit]." + var/parent_notif = ..() + if(istext(parent_notif)) + return . + " " + parent_notif + +/datum/account_modification/modify_withdrawal/get_short_desc() + var/datum/money_account/child/affected_child = affecting + return "[affected_child.withdrawal_limit > new_withdrawal_limit ? "Lowers" : "Raises"] withdrawal limit to [new_withdrawal_limit]" + +/datum/account_modification/modify_transaction + name = "Transaction fee modification" + var/new_transaction_fee + +/datum/account_modification/modify_transaction/prompt_creation(mob/user) + new_transaction_fee = input(user, "Enter the new transaction fee:", "Transaction fee") as num + new_transaction_fee = max(0, FLOOR(new_transaction_fee)) + + +/datum/account_modification/modify_transaction/New(n_affecting) + mod_delay = config.transaction_mod_delay + . = ..() + +/datum/account_modification/modify_transaction/modify_account() + var/datum/money_account/child/affected_child = affecting + if(istype(affected_child)) + affected_child.transaction_fee = new_transaction_fee + else + log_error("A transaction fee account modification was queued for an non-child account!") + ..() + +/datum/account_modification/modify_transaction/get_notification() + var/datum/money_account/child/affected_child = affecting + if(!istype(affected_child)) + return + . = "In [get_readable_countdown()], the transaction fee on your account will be [affected_child.transaction_fee > new_transaction_fee ? "lowered" : "raised"] to [new_transaction_fee]." + var/parent_notif = ..() + if(istext(parent_notif)) + return . + " " + parent_notif + +/datum/account_modification/modify_transaction/get_short_desc() + var/datum/money_account/child/affected_child = affecting + return "[affected_child.transaction_fee > new_transaction_fee ? "Lowers" : "Raises"] transaction fee to [new_transaction_fee]" + +/datum/account_modification/suspend_limit + name = "Suspend withdrawal limit" + suspends_withdrawal_limit = TRUE + mod_delay = 1 DAY + +/datum/account_modification/suspend_limit/get_notification() + return "The withdrawal limit on your account has been suspended for [get_readable_countdown()]" + +/datum/account_modification/suspend_limit/get_short_desc() + return "Suspends withdrawal limit on account for [get_readable_countdown()]" +/datum/account_modification/modify_fractional_reserve + name = "Fractional reserve modification" + var/new_fractional_reserve + allow_early = FALSE + +/datum/account_modification/modify_fractional_reserve/prompt_creation(mob/user) + new_fractional_reserve = input(user, "Enter the new fractional reserve (between 0 and 1):", "Fractional reserve") as num + new_fractional_reserve = clamp(new_fractional_reserve, 0, 1) + + var/datum/money_account/parent/affected_parent = affecting + if(istype(affected_parent)) + if(new_fractional_reserve < affected_parent.fractional_reserve) + suspends_withdrawal_limit = TRUE + else + log_error("A fractional reserve account modification was created for a non-parent account!") + +/datum/account_modification/modify_fractional_reserve/New(n_affecting, n_frac_reserve) + mod_delay = config.fractional_reserve_mod_delay + . = ..() + +/datum/account_modification/modify_fractional_reserve/modify_account() + var/datum/money_account/parent/affected_parent = affecting + if(istype(affected_parent)) + affected_parent.fractional_reserve = new_fractional_reserve + else + log_error("A fractional reserve account modification was queued for an non-parent account!") + ..() + +/datum/account_modification/modify_fractional_reserve/get_notification() + var/datum/money_account/parent/affected_parent = affecting + if(!istype(affected_parent)) + return + . = "In [get_readable_countdown()], the fractional reserve of [affected_parent.account_name] will be [affected_parent.fractional_reserve > new_fractional_reserve ? "lowered" : "raised"] to [new_fractional_reserve]." + var/parent_notif = ..() + if(istext(parent_notif)) + return . + " " + parent_notif + +/datum/account_modification/modify_fractional_reserve/get_short_desc() + var/datum/money_account/parent/affected_parent = affecting + return "[affected_parent.fractional_reserve > new_fractional_reserve ? "Lowers" : "Raises"] fractional reserve to [new_fractional_reserve]" + +/datum/account_modification/theft_prevention + name = "Theft prevention measures" + allow_early = FALSE + allow_cancel = FALSE + +/datum/account_modification/theft_prevention/New() + mod_delay = config.anti_tamper_mod_delay + . = ..() + +/datum/account_modification/theft_prevention/get_notification() + var/datum/money_account/parent/affected_parent = affecting + if(!istype(affected_parent)) + return + . = "Due to a recent theft, enhanced anti-theft measures are in place for [get_readable_countdown()]. Further tampering with money storage devices will trigger an escrow panic." + var/parent_notif = ..() + if(istext(parent_notif)) + return . + " " + parent_notif + +/datum/account_modification/theft_prevention/get_short_desc() + return "Enhanced anti-theft measures are in place." \ No newline at end of file diff --git a/code/modules/economy/cael/escrow.dm b/code/modules/economy/cael/escrow.dm new file mode 100644 index 00000000000..a12e3d422d9 --- /dev/null +++ b/code/modules/economy/cael/escrow.dm @@ -0,0 +1,20 @@ +// An account which temporarily holds money ejected from elsewhere. +/datum/money_account/escrow + + +/datum/money_account/escrow/Destroy(force) + SSmoney_accounts.all_escrow_accounts -= src + . = ..() + +/datum/money_account/escrow/deposit(amount, purpose, machine_id) + return "Account [format_account_id()] ([account_name]) does not allow deposits" + +/datum/money_account/escrow/withdraw(amount, purpose, machine_id) + . = ..() + if(money <= 0) + qdel(src) + +/datum/money_account/escrow/transfer(to_account, amount, purpose) + . = ..() + if(money <= 0) + qdel(src) \ No newline at end of file diff --git a/code/modules/economy/cael/global_accounts.dm b/code/modules/economy/cael/global_accounts.dm new file mode 100644 index 00000000000..d91ff4f3827 --- /dev/null +++ b/code/modules/economy/cael/global_accounts.dm @@ -0,0 +1,54 @@ +// Global accounts accessible from anywhere. Basically, the default player and station accounts accessible through ATMs +/proc/create_glob_account(var/account_name = "Default account name", var/owner_name, var/starting_funds = 0, var/account_type = ACCOUNT_TYPE_PERSONAL, var/obj/machinery/computer/account_database/source_db) + + //create a new account + var/datum/money_account/M = new() + M.account_name = account_name + M.owner_name = (owner_name ? owner_name : account_name) + M.account_type = account_type + M.remote_access_pin = rand(1111, 111111) + + //create an entry in the account transaction log for when it was created + //note that using the deposit proc on the account isn't really feasible because we need to change the transaction data before performing it + var/datum/transaction/singular/T = new(M, (source_db ? source_db.machine_id : "NTGalaxyNet Terminal #[rand(111,1111)]"), starting_funds, "Account creation") + if(!source_db) + //set a random date, time and location some time over the past few decades + T.date = "[num2text(rand(1,31))] [pick("January","February","March","April","May","June","July","August","September","October","November","December")], [global.using_map.game_year - rand(8,18)]" + T.time = "[rand(0,24)]:[rand(11,59)]" + + M.account_id = random_id("station_account_number", 111111, 999999) + else + M.account_id = next_account_number + next_account_number += rand(1,25) + + //create a sealed package containing the account details + var/txt + txt += "Account details (confidential)


" + txt += "Account holder: [M.owner_name]
" + txt += "Account number: [M.format_account_id()]
" + txt += "Account pin: [M.remote_access_pin]
" + txt += "Starting balance: [M.format_value_by_currency(M.money)]
" + txt += "Date and time: [stationtime2text()], [stationdate2text()]

" + txt += "Creation terminal ID: [source_db.machine_id]
" + txt += "Authorised officer overseeing creation: [source_db.held_card.registered_name]
" + + var/obj/item/paper/R = new /obj/item/paper(null, null, txt, "Account information: [M.account_name]") + R.apply_custom_stamp(overlay_image('icons/obj/bureaucracy.dmi', icon_state = "paper_stamp-boss", flags = RESET_COLOR), "by the Accounts Database") + new /obj/item/parcel(source_db.loc, null, R) + + //add the account + T.perform() + SSmoney_accounts.all_glob_accounts.Add(M) + + return M + +//this returns the first account datum that matches the supplied accnum/pin combination, it returns null if the combination did not match any account +/proc/attempt_account_access(var/attempt_account_id, var/attempt_pin_number, var/valid_card) + var/datum/money_account/D = get_glob_account(attempt_account_id) + if(D && (D.security_level != 2 || valid_card) && (!D.security_level || D.remote_access_pin == attempt_pin_number) ) + return D + +/proc/get_glob_account(var/account_id) + for(var/datum/money_account/D in SSmoney_accounts.all_glob_accounts) + if(D.account_id == account_id) + return D diff --git a/code/modules/economy/cael/related_accounts.dm b/code/modules/economy/cael/related_accounts.dm new file mode 100644 index 00000000000..88e5de2f3ad --- /dev/null +++ b/code/modules/economy/cael/related_accounts.dm @@ -0,0 +1,164 @@ +// Parent accounts hold the true money distributed among its child accounts. Child accounts check against +// and add/subtract to the parent accounts during transactions. Parent accounts may only be required to +// keep a certain amount of the total money in its child accounts (fractional reserve) as actual funds. +/datum/money_account/parent + var/fractional_reserve = 1 + var/child_totals + + var/open_escrow_on_destroy = TRUE // Whether or not escrow accounts will be opened for child accounts in Destroy(). + + var/list/datum/money_account/child/children = list() + +/datum/money_account/parent/Destroy(force) + QDEL_NULL(children) + . = ..() + +/datum/money_account/parent/can_afford(amount, datum/money_account/receiver) + . = ..() + if(.) + return + // If the receiver is a child account, then the money itself isn't moving, but we need to be able + // to afford the new fractional reserve requirement. + if(receiver in children) + if(fractional_reserve*(child_totals + amount) > money) + return "Transaction violates fractional reserve requirements" + else if(fractional_reserve*child_totals > money - amount) + return "Transaction violates fractional reserve requirements" + +// Dumps as much money as possible into globally available, withdraw only escrow accounts. +/datum/money_account/parent/proc/escrow_panic() + if(!child_totals) + return + + for(var/datum/money_account/child/to_escrow in children) + to_escrow.on_escrow() + +// Returns the maximum amount that can be withdrawn from the account with current fractional reserve requirement. +/datum/money_account/parent/proc/get_max_withdraw() + return max(0, money - fractional_reserve*child_totals) + +// Child accounts hold a 'fake' amount of money which actually reflects a share in the parent account. +// Child accounts can receive interest from the parent account at fixed intervals, and may be subject to a withdrawal limit. +// If the parent account only requires a fractional reserve, then the sum of all the money in the children accounts +// may not equal the money in the parent account. +/datum/money_account/child + + var/withdrawal_limit = 0 // Maximum amount of money which can be withdrawn in a period. Defaults to no limit. + var/current_withdrawal = 0 // Amount of money withdrawn within the last period. + var/last_withdraw_period + + var/transaction_fee = 0 // Fee paid per transaction/withdraw out of the account. Transaction fees do not apply + // between parent/child and children with the same parent account. + + var/interest_rate = 0 + var/last_interest_period + + var/datum/money_account/parent/parent_account + +/datum/money_account/child/New(account_type, p_account, n_interest, n_withdrawal_limit, n_transaction_fee) + parent_account = p_account + parent_account.children |= src + interest_rate = n_interest + withdrawal_limit = n_withdrawal_limit + transaction_fee = n_transaction_fee + last_withdraw_period = REALTIMEOFDAY + last_interest_period = REALTIMEOFDAY + . = ..() + +/datum/money_account/child/Destroy(force) + parent_account.children -= src + parent_account.child_totals -= money + parent_account = null + . = ..() + +/datum/money_account/child/can_afford(amount, datum/money_account/receiver) + if(!parent_account) + // CASH GONE. + return "Transaction failed. Contact your financial provider" + . = ..() + if(.) + return + + if(withdrawal_limit && !withdrawal_limit_suspended()) + if(current_withdrawal + amount > withdrawal_limit) + return "Transaction goes over withdrawal limit" + + if(amount + transaction_fee > money) + return "Transaction fee cannot be afforded" + + // Accounts with the same parent can continue passing around monopoly money even if the parent is insolvent. + if(receiver in parent_account.children) + return + + // Likely due to theft of some sort. + if(parent_account.money < amount) + return "Transaction failed. Financial provider may be insolvent" + +/datum/money_account/child/add_transaction(datum/transaction/T, is_source) + var/amount_adjusted = ..() + // Parent accounts are technically allowed to run negative as a measure of 'debt' to child accounts. + // This should not occur from normal transactions + if(parent_account) + if(amount_adjusted < 0) + // Transaction fee doesn't apply to accounts with the same parent. + var/adj_transaction_fee = T.target && (T.target in (parent_account.children | parent_account)) ? 0 : transaction_fee + // Remove the transaction fee, if it exists. + adjust_money(-adj_transaction_fee) + + current_withdrawal += abs(T.amount) + + parent_account.adjust_money(T.amount) + parent_account.child_totals += (T.amount - adj_transaction_fee) + else + parent_account.adjust_money(T.amount) + parent_account.child_totals += T.amount + +/datum/money_account/child/proc/accrue_interest() + if(!interest_rate || !parent_account) + return + + var/interest_amount = round(interest_rate*money) + if(interest_amount == 0) + return + + // This just reduces the amount of money in the child account and the total, but we process it as a normal + // transaction for logging purposes. + if(interest_amount < 0) + var/err = transfer(parent_account, -interest_amount, "Interest payment") + if(err) + parent_account.log_msg("Interest did not accrue for account [format_account_id()]: [err].") + return + + // This checks if accruing interest would violate reserve limits etc. + var/err = parent_account.transfer(src, interest_amount, "Interest payment") + if(err) + log_msg("Interest did not accrue for this period: [err]. Contact your financial provider.") + return + +/datum/money_account/child/proc/withdrawal_limit_suspended() + for(var/datum/account_modification/pending_mod in pending_modifications) + if(pending_mod.suspends_withdrawal_limit) + return TRUE + + if(parent_account) + for(var/datum/account_modification/pending_mod in parent_account.pending_modifications) + if(pending_mod.suspends_withdrawal_limit) + return TRUE + +// Returns the amount actually escrowed +/datum/money_account/child/proc/on_escrow() + if(!parent_account || !money) // Nothing to escrow! + return + + var/solvency = max(1, parent_account.money / parent_account.child_totals) + + var/escrowed_money = FLOOR(min(solvency*money, parent_account.money)) + var/datum/money_account/escrow/child_escrow = SSmoney_accounts.get_or_add_escrow(account_id, remote_access_pin, parent_account.owner_name) + child_escrow.money += escrowed_money + + money -= escrowed_money + + parent_account.money -= escrowed_money + parent_account.child_totals -= escrowed_money + + return escrowed_money \ No newline at end of file diff --git a/code/modules/events/money_hacker.dm b/code/modules/events/money_hacker.dm index 4806c8c166b..b6b02e5014b 100644 --- a/code/modules/events/money_hacker.dm +++ b/code/modules/events/money_hacker.dm @@ -7,8 +7,8 @@ var/global/account_hack_attempted = 0 /datum/event/money_hacker/setup() end_time = world.time + 6000 - if(all_money_accounts.len) - affected_account = pick(all_money_accounts) + if(SSmoney_accounts.all_glob_accounts.len) + affected_account = pick(SSmoney_accounts.all_glob_accounts) account_hack_attempted = 1 else @@ -22,7 +22,7 @@ var/global/account_hack_attempted = 0 break if(MS) // Hide the account number for now since it's all you need to access a standard-security account. Change when that's no longer the case. - var/message = "A brute force hack has been detected (in progress since [stationtime2text()]). The target of the attack is: Financial account #[affected_account.account_number], \ + var/message = "A brute force hack has been detected (in progress since [stationtime2text()]). The target of the attack is: Financial account [affected_account.format_account_id()], \ without intervention this attack will succeed in approximately 10 minutes. Required intervention: temporary suspension of affected accounts until the attack has ceased. \ Notifications will be sent as updates occur." var/my_department = "[location_name()] Firewall Subroutines" @@ -54,7 +54,7 @@ var/global/account_hack_attempted = 0 var/time1 = rand(0, 99999999) var/time2 = "[round(time1 / 36000)+12]:[(time1 / 600 % 60) < 10 ? add_zero(time1 / 600 % 60, 1) : time1 / 600 % 60]" T.time = pick("", stationtime2text(), time2) - + T.perform() var/obj/machinery/network/message_server/MS @@ -64,4 +64,3 @@ var/global/account_hack_attempted = 0 var/my_department = "[location_name()] Firewall Subroutines" MS.send_rc_message("XO's Desk", my_department, message, "", "", 2) break - \ No newline at end of file diff --git a/code/modules/events/money_lotto.dm b/code/modules/events/money_lotto.dm index 0cd4a07d1fb..8e433309571 100644 --- a/code/modules/events/money_lotto.dm +++ b/code/modules/events/money_lotto.dm @@ -7,8 +7,8 @@ /datum/event/money_lotto/start() winner_sum = pick(5000, 10000, 50000, 100000, 500000, 1000000, 1500000) if(prob(50)) - if(all_money_accounts.len) - winner_account = pick(all_money_accounts) + if(SSmoney_accounts.all_glob_accounts.len) + winner_account = pick(SSmoney_accounts.all_glob_accounts) winner_name = winner_account.owner_name deposit_success = winner_account.deposit(winner_sum, "Nyx Daily Loan Lottery winner!", "Biesel TCD Terminal #[rand(111,333)]") else diff --git a/code/modules/fabrication/designs/general/designs_general.dm b/code/modules/fabrication/designs/general/designs_general.dm index 7d63d375f6b..19c531f7efb 100644 --- a/code/modules/fabrication/designs/general/designs_general.dm +++ b/code/modules/fabrication/designs/general/designs_general.dm @@ -161,3 +161,6 @@ path = /obj/item/stack/package_wrap /datum/fabricator_recipe/gift_wrapper path = /obj/item/stack/package_wrap/gift + +/datum/fabricator_recipe/network_pos + path = /obj/item/network_pos diff --git a/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm b/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm index 90d67bb6b3d..b0476a4fb4e 100644 --- a/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm +++ b/code/modules/fabrication/designs/protolathe/designs_computer_accessories.dm @@ -10,7 +10,7 @@ /datum/fabricator_recipe/protolathe/comp_acc/mstickbroadcaster path = /obj/item/stock_parts/computer/charge_stick_slot/broadcaster - + /datum/fabricator_recipe/protolathe/comp_acc/nanoprinter path = /obj/item/stock_parts/computer/nano_printer @@ -32,6 +32,9 @@ /datum/fabricator_recipe/protolathe/comp_acc/medical_scanner path = /obj/item/stock_parts/computer/scanner/medical +/datum/fabricator_recipe/protolathe/comp_acc/money_printer + path = /obj/item/stock_parts/computer/money_printer + ////////////////////////////////////////////////////////////////// // Frames ////////////////////////////////////////////////////////////////// diff --git a/code/modules/goals/definitions/personal.dm b/code/modules/goals/definitions/personal.dm index 8707f55f4f5..9df3591d9fe 100644 --- a/code/modules/goals/definitions/personal.dm +++ b/code/modules/goals/definitions/personal.dm @@ -28,7 +28,7 @@ /datum/goal/money/update_strings() target_amount = rand(100, 200) var/datum/mind/mind = owner - for(var/datum/money_account/acct in all_money_accounts) + for(var/datum/money_account/acct in SSmoney_accounts.all_glob_accounts) if(acct.owner_name == mind.current.real_name) target_amount = acct.get_balance() * rand(2,3) break @@ -36,7 +36,7 @@ /datum/goal/money/check_success() var/datum/mind/mind = owner - for(var/datum/money_account/acct in all_money_accounts) + for(var/datum/money_account/acct in SSmoney_accounts.all_glob_accounts) if(acct.owner_name == mind.current.real_name) return acct.get_balance() > target_amount return FALSE \ No newline at end of file diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm index d0e5ce447d9..ed32d402c6e 100644 --- a/code/modules/modular_computers/file_system/program.dm +++ b/code/modules/modular_computers/file_system/program.dm @@ -176,6 +176,9 @@ else return -1 +// Returns the amount of cash taken, if any. +/datum/computer_file/program/proc/process_cash(var/obj/item/cash/received_cash) + /datum/nano_module/program available_to_ai = FALSE var/datum/computer_file/program/program = null // Program-Based computer program that runs this nano module. Defaults to null. diff --git a/code/modules/modular_computers/file_system/programs/command/card.dm b/code/modules/modular_computers/file_system/programs/command/card.dm index 1866a762abd..58d48b67a1d 100644 --- a/code/modules/modular_computers/file_system/programs/command/card.dm +++ b/code/modules/modular_computers/file_system/programs/command/card.dm @@ -32,7 +32,7 @@ if(card_slot) var/obj/item/card/id/id_card = card_slot.stored_card data["has_id"] = !!id_card - data["id_account_number"] = id_card ? id_card.associated_account_number : null + data["id_account_id"] = id_card ? id_card.associated_account_id : null data["network_account_login"] = id_card ? id_card.associated_network_account["login"] : null data["network_account_password"] = id_card ? stars(id_card.associated_network_account["password"], 0) : null data["id_rank"] = id_card && id_card.assignment ? id_card.assignment : "Unassigned" @@ -160,7 +160,7 @@ For: [id_card.registered_name ? id_card.registered_name : "Unregistered"]

Assignment: [id_card.assignment]
- Account Number: #[id_card.associated_account_number]
+ Account Number: #[id_card.associated_account_id]
Network account: [id_card.associated_network_account["login"]] Network password: [stars(id_card.associated_network_account["password"], 0)] Blood Type: [id_card.blood_type]

@@ -214,8 +214,8 @@ else computer.show_error(usr, "Invalid name entered!") else if(href_list["account"]) - var/account_num = text2num(input("Enter account number.", "Account", id_card.associated_account_number)) - id_card.associated_account_number = account_num + var/account_num = text2num(input("Enter account number.", "Account", id_card.associated_account_id)) + id_card.associated_account_id = account_num else if(href_list["alogin"]) var/account_login = input("Enter network account login.", "Network account login", id_card.associated_network_account["login"]) id_card.associated_network_account["login"] = account_login diff --git a/code/modules/modular_computers/file_system/programs/command/finances.dm b/code/modules/modular_computers/file_system/programs/command/finances.dm new file mode 100644 index 00000000000..c9985d898d2 --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/command/finances.dm @@ -0,0 +1,573 @@ +#define STATE_ERROR -1 +#define STATE_MENU 0 +#define STATE_PARENT 1 +#define STATE_CHILD 2 + +#define TRANSACTIONS_PER_PAGE 5 +/datum/computer_file/program/finances + filename = "financeman" + filedesc = "Financial management program" + nanomodule_path = /datum/nano_module/program/finances + program_icon_state = "id" + program_key_state = "id_key" + program_menu_icon = "key" + extended_desc = "Program for managing finances exchanged and stored on the network." + size = 8 + category = PROG_FINANCE + + var/static/list/parent_mod_types = list(/datum/account_modification/modify_fractional_reserve) + var/static/list/child_mod_types = list( /datum/account_modification/modify_interest, + /datum/account_modification/modify_transaction, + /datum/account_modification/modify_withdrawal, + /datum/account_modification/suspend_limit) + +/datum/nano_module/program/finances + name = "Financial management program" + var/prog_state = STATE_MENU + var/datum/computer_file/data/account/selected_account + + var/log_page = 0 + + // For parent account transfers. + var/transfer_amount + var/transfer_purpose + var/target_account + var/target_network + +/datum/nano_module/program/finances/ui_interact(mob/user, ui_key, datum/nanoui/ui, force_open, datum/nanoui/master_ui, datum/topic_state/state) + . = ..() + + var/list/data = host.initial_data() + var/datum/computer_network/network = get_network() + var/datum/extension/network_device/bank/banking_mainframe + if(!network) + data["error"] = "Unable to connect to the network. Check network connectivity." + prog_state = STATE_ERROR + else + var/list/accesses = get_access(user) + banking_mainframe = network.banking_mainframe + if(!banking_mainframe || !banking_mainframe.has_access(accesses)) + data["error"] = "A banking mainframe does not exist on the network, or you lack the required access." + prog_state = STATE_ERROR + + if(prog_state == STATE_ERROR && !data["error"]) + prog_state = STATE_MENU + + switch(prog_state) + if(STATE_PARENT) + var/datum/money_account/parent/network/bank_account = banking_mainframe.get_parent_account() + if(!bank_account) + data["create_parent"] = TRUE + else + data["parent_name"] = bank_account.account_name + data["parent_balance"] = bank_account.format_value_by_currency(bank_account.money) + data["child_totals"] = bank_account.format_value_by_currency(bank_account.child_totals) + data["fractional_reserve"] = "[bank_account.fractional_reserve*100] %" + data["no_child_accounts"] = length(bank_account.children) + + data["pending_parent_mods"] = list() + var/index = 0 + for(var/datum/account_modification/pending_mod in bank_account.pending_modifications) + index++ + data["pending_parent_mods"] += list(pending_mod.get_ui_data() + list("index" = index)) + + data["admin_accounts"] = banking_mainframe.admin_accounts + + // Transactions from parent + data["trans_amount"] = transfer_amount + data["trans_purpose"] = transfer_purpose + data["trans_account"] = target_account + data["trans_network"] = target_network + + // Parent transaction log + data["parent_transactions"] = list() + var/list/transaction_log = reverselist(bank_account.transaction_log) + var/log_length = length(transaction_log) + log_page = clamp(log_page, 0, CEILING(log_length/TRANSACTIONS_PER_PAGE)) + + data["next_page"] = log_length > (log_page + 1)*TRANSACTIONS_PER_PAGE + data["prev_page"] = log_page != 0 + transaction_log = transaction_log.Copy(min(log_length, log_page*TRANSACTIONS_PER_PAGE + 1), min(log_length+1, (log_page + 1)*TRANSACTIONS_PER_PAGE + 1)) + for(var/datum/transaction/logged in transaction_log) + data["parent_transactions"] += list(list( + "date" = logged.date, + "time" = logged.time, + "purpose" = logged.purpose, + "amount" = bank_account.format_value_by_currency(logged.amount), + "target" = logged.get_target_name(), + "source" = logged.get_source_name() + )) + + if(STATE_CHILD) + if(!istype(selected_account)) + var/list/accounts = network.get_accounts() + data["accounts"] = list() + for(var/datum/computer_file/data/account/A in accounts) + data["accounts"] += list(list( + "account" = A.login, + "fullname" = A.fullname, + "money" = A.money_account ? A.money_account.format_value_by_currency(A.money_account.get_balance()) : "No financial account" + )) + var/datum/money_account/parent/network/bank_account = banking_mainframe.get_parent_account() + if(!bank_account) + data["auto_accounts"] = FALSE + else + data["auto_accounts"] = banking_mainframe.auto_money_accounts + data["auto_interest_rate"] = banking_mainframe.auto_interest_rate + data["auto_withdrawal_limit"] = bank_account.format_value_by_currency(banking_mainframe.auto_withdrawal_limit) + data["auto_transaction_fee"] = bank_account.format_value_by_currency(banking_mainframe.auto_transaction_fee) + + else if(selected_account.money_account) + var/datum/money_account/child/selected_m_account = selected_account.money_account + data["child_name"] = selected_m_account.account_name + data["child_balance"] = selected_m_account.format_value_by_currency(selected_m_account.get_balance()) + data["interest_rate"] = selected_m_account.interest_rate + data["withdrawal_limit"] = selected_m_account.format_value_by_currency(selected_m_account.withdrawal_limit) + data["current_withdrawal"] = selected_m_account.format_value_by_currency(selected_m_account.current_withdrawal) + data["transaction_fee"] = selected_m_account.format_value_by_currency(selected_m_account.transaction_fee) + + data["pending_child_mods"] = list() + var/index = 0 + for(var/datum/account_modification/pending_mod in selected_m_account.pending_modifications) + index++ + data["pending_child_mods"] += list(pending_mod.get_ui_data() + list("index" = index)) + + // Child Transactions + data["transactions"] = list() + var/list/transaction_log = reverselist(selected_m_account.transaction_log) + var/log_length = length(transaction_log) + log_page = clamp(log_page, 0, CEILING(log_length/TRANSACTIONS_PER_PAGE)) + + data["next_page"] = log_length > (log_page + 1)*TRANSACTIONS_PER_PAGE + data["prev_page"] = log_page != 0 + transaction_log = transaction_log.Copy(min(log_length, log_page*TRANSACTIONS_PER_PAGE + 1), min(log_length + 1, (log_page + 1)*TRANSACTIONS_PER_PAGE + 1)) + for(var/datum/transaction/logged in transaction_log) + data["child_transactions"] += list(list( + "date" = logged.date, + "time" = logged.time, + "purpose" = logged.purpose, + "amount" = selected_m_account.format_value_by_currency(logged.amount), + "target" = logged.get_target_name(), + "source" = logged.get_source_name() + )) + data["prog_state"] = prog_state + ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open) + if (!ui) + ui = new(user, src, ui_key, "finance_management.tmpl", name, 600, 700, state = state) + ui.auto_update_layout = 1 + ui.set_initial_data(data) + ui.open() + +/datum/computer_file/program/finances/Topic(href, href_list, state) + if(..()) + return TOPIC_REFRESH + + var/mob/user = usr + + var/datum/computer_network/network = computer.get_network() + var/datum/nano_module/program/finances/module = NM + if(!network) + return TOPIC_REFRESH + + var/list/accesses = module.get_access(user) + var/datum/extension/network_device/bank/banking_mainframe = network.banking_mainframe + if(!banking_mainframe || !banking_mainframe.has_access(accesses)) + return TOPIC_REFRESH + + var/datum/money_account/parent/network/bank_account = banking_mainframe.get_parent_account() + + if(href_list["next_page"]) + module.log_page += 1 + return TOPIC_REFRESH + if(href_list["prev_page"]) + module.log_page = max(module.log_page - 1, 0) + return TOPIC_REFRESH + + switch(module.prog_state) + if(STATE_ERROR) + if(href_list["back"]) + module.prog_state = STATE_MENU + return TOPIC_REFRESH + + if(STATE_MENU) + if(href_list["parent_mode"]) + module.log_page = 0 + module.prog_state = STATE_PARENT + return TOPIC_REFRESH + if(href_list["child_mode"]) + module.prog_state = STATE_CHILD + return TOPIC_REFRESH + + if(STATE_PARENT) + if(href_list["back"]) + module.prog_state = STATE_MENU + return TOPIC_REFRESH + + if(href_list["create_parent_account"]) + if(bank_account) + return TOPIC_REFRESH + var/name = input(user, "Enter the name for the parent financial account (this may be changed later):", "Account name") as text|null + name = sanitize(name) + if(!CanInteract(user,state)) + return TOPIC_HANDLED + + if(!name) + to_chat(user, SPAN_WARNING("Invalid name!")) + return TOPIC_HANDLED + + var/fractional_reserve = input(user, "Enter the fractional reserve for the parent financial account (between 0 and 1):", "Fractional Reserve") as num|null + fractional_reserve = clamp(fractional_reserve, 0, 1) + if(!CanInteract(user, state)) + return TOPIC_HANDLED + + var/confirmation = alert(user, "You are about to create a parent financial account with name '[name]' and a fractional reserve of '[fractional_reserve]'. Are you sure?", "Finalize account", "No", "Yes") + if(confirmation != "Yes" || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/success = banking_mainframe.create_parent_account(name, fractional_reserve) + if(!success) + to_chat(user, SPAN_WARNING("Unable to create parent financial account!")) + return TOPIC_REFRESH + + if(!bank_account) + return TOPIC_REFRESH + + if(href_list["withdrawal"]) + var/obj/item/stock_parts/computer/money_printer/printer = computer.get_component(PART_MPRINTER) + if(!printer) + to_chat(user, SPAN_WARNING("There's no cryptographic printer installed!")) + return TOPIC_HANDLED + + var/amount = input(user, "Input the amount of cash to withdrawal:", "Cash withdrawal", 0) as num|null + if(!amount || !CanInteract(user,state)) + return TOPIC_HANDLED + + if(printer.can_print(amount, banking_mainframe.get_currency())) + var/err = banking_mainframe.withdraw(amount, "Cash withdrawal", computer.get_nid()) + if(err) + to_chat(user, SPAN_WARNING("Cash withdrawal failed: [err].")) + else + printer.print_money(amount, banking_mainframe.get_currency(), user) + + return TOPIC_REFRESH + else + to_chat(user, "\The [printer] does not have enough stored plastic!") + return TOPIC_HANDLED + + if(href_list["add_modification"]) + var/list/options = list() + for(var/mod in parent_mod_types) + var/datum/account_modification/mod_type = mod + options[initial(mod_type.name)] = mod + var/chosen_mod = input(user, "Select the type of policy modification you would like to perform:", "Policy modification") as anything in options|null + if(!chosen_mod || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/chosen_mod_type = options[chosen_mod] + if(banking_mainframe.has_mod_type(chosen_mod_type)) + to_chat(user, "A modification of that type has already been queued!") + return TOPIC_HANDLED + var/datum/account_modification/new_mod = new chosen_mod_type(bank_account) + new_mod.prompt_creation(user) + var/withdrawal_warning = new_mod.suspends_withdrawal_limit ? " During this time, withdrawal limits will be suspended." : "" + var/confirm = alert(user, "You are about to create a [lowertext(new_mod.name)]. It will activate in [new_mod.get_readable_countdown()].[withdrawal_warning] Are you sure?", "Confirm", "No", "Yes") + + if(confirm != "Yes" || !CanInteract(user, state)) + qdel(new_mod) + return TOPIC_REFRESH + bank_account.pending_modifications += new_mod + + var/datum/computer_file/data/email_message/notification = new() + notification.title = "Notification of financial policy modification" + notification.source = "financial-services@[network.network_id]" + notification.stored_data = "A change to your financial provider's policies has been initiated. [new_mod.get_notification()] Please contact your financial provider for more information." + + network.email_child_accounts(notification) + banking_mainframe.email_admin_accounts(notification) + + return TOPIC_REFRESH + + if(href_list["activate_modification"]) + var/index = text2num(href_list["activate_modification"]) + var/list/pending_mods = banking_mainframe.get_pending_modifications() + if(length(pending_mods) < index) + return TOPIC_REFRESH + + var/datum/account_modification/pending_mod = pending_mods[index] + if(!pending_mod.allow_early) + return TOPIC_HANDLED + + var/confirm = alert(user, "Are you sure you would like to activate this policy modification early?", "Early activation", "No", "Yes") + if(confirm != "Yes" || !CanInteract(user, state)) + return TOPIC_HANDLED + + pending_mod.modify_account() + return TOPIC_REFRESH + + if(href_list["cancel_modification"]) + var/index = text2num(href_list["cancel_modification"]) + var/list/pending_mods = banking_mainframe.get_pending_modifications() + if(length(pending_mods) < index) + return TOPIC_REFRESH + var/datum/account_modification/pending_mod = pending_mods[index] + var/confirm = alert(user, "Are you sure you would like to cancel this policy modification?", "Cancel modification", "No", "Yes") + if(confirm != "Yes" || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/datum/computer_file/data/email_message/notification = new() + notification.title = "Cancellation of financial policy modification" + notification.source = "financial-services@[network.network_id]" + notification.stored_data = "Your financial provider has cancelled a pending change to financial policy: [lowertext(pending_mod.name)]. Please contact your financial provider for more information." + + network.email_child_accounts(notification) + banking_mainframe.email_admin_accounts(notification) + + qdel(pending_mod) + return TOPIC_REFRESH + + if(href_list["transfer_account"]) + var/account = input(user, "Enter the account login you wish to transfer funds to. Leave blank if you wish to transfer money to a network's parent account:") as text|null + if(!account) + return TOPIC_HANDLED + + module.target_account = sanitize_for_account(account) + return TOPIC_REFRESH + + if(href_list["transfer_network"]) + var/target_network = input(user, "Enter the network you wish to transfer funds to. Leave blank if you wish to transfer money on the current network:") as text|null + if(!target_network) + return TOPIC_HANDLED + + module.target_network = sanitize(target_network) + return TOPIC_REFRESH + + if(href_list["transfer_amount"]) + var/amount = input(user, "Enter the amount to transfer:") as num|null + if(!amount) + return TOPIC_HANDLED + + module.transfer_amount = round(abs(amount)) + return TOPIC_REFRESH + + if(href_list["transfer_purpose"]) + var/purpose = input(user, "Enter the purpose for this transaction:") as text|null + if(!purpose) + return + module.transfer_purpose = sanitize(purpose) + return TOPIC_REFRESH + + if(href_list["perform_transfer"]) + if(!module.transfer_amount) + to_chat(user, SPAN_WARNING("You must enter an amount to transfer!")) + return TOPIC_HANDLED + + var/datum/money_account/target_account = network.get_financial_account(module.target_account, module.target_network ? module.target_network : network.network_id) + if(!istype(target_account)) + to_chat(user, SPAN_WARNING("Unable to perform transaction. [target_account].")) + return TOPIC_HANDLED + + var/err = bank_account.transfer(target_account, module.transfer_amount, module.transfer_purpose) + if(err) + to_chat(user, SPAN_WARNING("Funds transfer failed: [err].")) + return TOPIC_HANDLED + else + to_chat(user, SPAN_NOTICE("Funds transfer successful!")) + module.transfer_amount = 0 + module.transfer_purpose = null + module.target_account = null + module.target_account = null + return TOPIC_REFRESH + + if(href_list["add_admin"]) + var/admin_login = input(user, "Enter the login of the admin you would like to add.") as text + if(!admin_login || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/success = banking_mainframe.add_admin_account(admin_login) + if(success) + return TOPIC_REFRESH + + to_chat(user, SPAN_WARNING("An account with that login could not be found.")) + return TOPIC_HANDLED + + if(href_list["remove_admin"]) + banking_mainframe.remove_admin_account(href_list["remove_admin"]) + return TOPIC_REFRESH + + if(STATE_CHILD) + if(!bank_account) + return TOPIC_REFRESH + if(href_list["back"]) + if(!module.selected_account) + module.prog_state = STATE_MENU + else + module.log_page = 0 + module.selected_account = null + return TOPIC_REFRESH + + if(module.selected_account) + var/datum/money_account/child/network/selected_child = module.selected_account.money_account + if(!selected_child) + module.selected_account = null + return TOPIC_REFRESH + + if(href_list["add_modification"]) + var/list/options = list() + for(var/mod in child_mod_types) + var/datum/account_modification/mod_type = mod + options[initial(mod_type.name)] = mod + var/chosen_mod = input(user, "Select the type of client modification you would like to perform:", "Client modification") as anything in options + if(!chosen_mod || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/chosen_mod_type = options[chosen_mod] + if(selected_child.has_mod_type(chosen_mod_type)) + to_chat(user, "A modification of that type has already been queued!") + + var/datum/account_modification/new_mod = new chosen_mod_type(selected_child) + new_mod.prompt_creation(user) + var/withdrawal_warning = new_mod.suspends_withdrawal_limit ? " During this time, withdrawal limits will be suspended." : "" + var/confirm = alert(user, "You are about to create a [lowertext(new_mod.name)]. It will activate in [new_mod.get_readable_countdown()].[withdrawal_warning] Are you sure?", "Confirm", "No", "Yes") + + if(confirm != "Yes" || !CanInteract(user, state)) + qdel(new_mod) + return TOPIC_HANDLED + selected_child.pending_modifications += new_mod + + var/datum/computer_file/data/email_message/notification = new() + notification.title = "Notification of financial modification" + notification.source = "financial-services@[network.network_id]" + notification.stored_data = "A change to your client account has been initiated. [new_mod.get_notification()] Please contact your financial provider for more information." + + network.receive_email(module.selected_account, "financial-services@[network.network_id]", notification) + return TOPIC_REFRESH + + if(href_list["cancel_modification"]) + var/index = text2num(href_list["cancel_modification"]) + var/list/pending_mods = selected_child.pending_modifications + if(length(pending_mods) < index) + return TOPIC_REFRESH + + var/datum/account_modification/pending_mod = pending_mods[index] + if(!pending_mod.allow_cancel) + return TOPIC_HANDLED + var/confirm = alert(user, "Are you sure you would like to cancel this policy modification?", "Cancel modification", "No", "Yes") + if(confirm != "Yes" || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/datum/computer_file/data/email_message/notification = new() + notification.title = "Cancellation of financial policy modification" + notification.source = "financial-services@[network.network_id]" + notification.stored_data = "Your financial provider has cancelled a pending change to your client account: [lowertext(pending_mod.name)]." + + network.receive_email(module.selected_account, "financial-services@[network.network_id]", notification) + qdel(pending_mod) + return TOPIC_REFRESH + + if(href_list["close_account"]) + var/confirm = alert(user, "Are you SURE you want to close this account? The owner will be notified by e-mail, and their outstanding balance moved to an escrow account.", "Confirm account closure", "No", "Yes") + if(confirm != "Yes" || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/datum/computer_file/data/email_message/notification = new() + notification.title = "Notification of account closure" + notification.source = "financial-services@[network.network_id]" + if(selected_child.money) + notification.stored_data = "Your financial provider has closed your financial account. An escrow account has been opened for you containing some or all of your outstanding balance of your account. \ + Escrow accounts are accessible from any financial terminal using your prior account information, and the financial provider ID '[bank_account.owner_name]'." + else + notification.stored_data = "Your financial provider has closed your financial account. Please contact your financial provider for more information." + + network.receive_email(module.selected_account, "financial-services@[network.network_id]", notification) + + // Don't send the normal escrow panic e-mails. + selected_child.on_escrow(TRUE) + module.selected_account = null + qdel(selected_child) + to_chat(user, SPAN_NOTICE("Account successfully closed!")) + return TOPIC_REFRESH + else + if(href_list["toggle_auto_accounts"]) + banking_mainframe.auto_money_accounts = !banking_mainframe.auto_money_accounts + return TOPIC_REFRESH + + if(href_list["change_auto_withdrawal"]) + var/n_withdrawal = input(user, "Enter the new preset withdrawal limit:", "Withdrawal limit") as num + if(!CanInteract(user, state)) + return TOPIC_HANDLED + banking_mainframe.auto_withdrawal_limit = max(0, FLOOR(n_withdrawal)) + return TOPIC_REFRESH + + if(href_list["change_auto_interest"]) + var/n_interest = input(user, "Enter the new preset interest rate (between -1 and 1):", "Interest Rate") as num + if(!CanInteract(user, state)) + return TOPIC_HANDLED + banking_mainframe.auto_interest_rate = clamp(n_interest, -1, 1) + return TOPIC_REFRESH + + if(href_list["change_auto_transaction"]) + var/n_withdrawal = input(user, "Enter the new preset transaction fee:", "Transaction fee") as num + if(!CanInteract(user, state)) + return TOPIC_HANDLED + banking_mainframe.auto_transaction_fee = max(0, FLOOR(n_withdrawal)) + return TOPIC_REFRESH + + if(href_list["select_account"]) + var/datum/computer_file/data/account/selected = network.find_account_by_login(href_list["select_account"]) + if(selected) + . = TOPIC_REFRESH + if(!selected.money_account) + var/confirm = alert(user, "This network account does not have an attached client account. Would you like to create one?", "Create client account", "No", "Yes") + if(confirm == "Yes" && CanInteract(user, state)) + + var/interest = input(user, "Enter the interest rate for this account (between -1 and 1):") as num + interest = clamp(interest, -1, 1) + if(!CanInteract(user, state)) + return + var/withdrawal_limit = input(user, "Enter the withdrawal limit for this account:") as num + withdrawal_limit = max(0, FLOOR(withdrawal_limit)) + if(!CanInteract(user, state)) + return + var/transaction_fee = input(user, "Enter the transaction fee for this account:") as num + transaction_fee = max(0, FLOOR(transaction_fee)) + if(!CanInteract(user, state)) + return + + if(!selected || selected.money_account || !bank_account) + return + + var/confirm2 = alert(user, "Create a client account for '[selected.login]' with an interest rate of [interest], a withdrawal limit of [withdrawal_limit] and a transaction fee of [transaction_fee]?", "Create client account", "No", "Yes") + if(confirm2 != "Yes") + return + + selected.money_account = new(null, bank_account, interest, withdrawal_limit, transaction_fee, selected) + else + return TOPIC_HANDLED + module.selected_account = selected + +/datum/computer_file/program/finances/process_cash(obj/item/cash/received, mob/user) + var/datum/computer_network/network = computer.get_network() + if(!network) + return + var/datum/nano_module/program/accounts/module = NM + if(module.prog_state != STATE_PARENT) // Can only deposit into the parent account. + return + + var/list/accesses = module.get_access(user) + var/datum/extension/network_device/bank/banking_mainframe = network.banking_mainframe + if(!banking_mainframe || !banking_mainframe.has_access(accesses)) + return // Shouldn't be able to access this UI regardless. + + var/err = banking_mainframe.deposit(received.absolute_worth, "Cash deposit", computer.get_nid()) + if(err) + to_chat(user, SPAN_WARNING("Cash deposit failed: [err].")) + else + SSnano.update_uis(module) + return received.absolute_worth + +#undef TRANSACTIONS_PER_PAGE + +#undef STATE_ERROR +#undef STATE_MENU +#undef STATE_PARENT +#undef STATE_CHILD \ No newline at end of file diff --git a/code/modules/modular_computers/file_system/programs/generic/atm.dm b/code/modules/modular_computers/file_system/programs/generic/atm.dm new file mode 100644 index 00000000000..0004966bc7c --- /dev/null +++ b/code/modules/modular_computers/file_system/programs/generic/atm.dm @@ -0,0 +1,331 @@ +#define STATE_LOGIN 0 +#define STATE_MAIN 1 +#define STATE_TRANSFER 2 +#define STATE_LOG 3 + +#define TRANSACTIONS_PER_PAGE 8 +/datum/computer_file/program/atm + filename = "atmprog" + filedesc = "ATM Client" + extended_desc = "This program may be used to manage finances attached to your network account." + program_icon_state = "generic" + program_key_state = "generic_key" + program_menu_icon = "mail-closed" + size = 3 + available_on_network = 1 + usage_flags = PROGRAM_ALL + category = PROG_FINANCE + + nanomodule_path = /datum/nano_module/program/atm + +/datum/computer_file/program/atm/Topic(href, href_list, state) + if(..()) + return TOPIC_REFRESH + var/mob/living/user = usr + var/datum/computer_file/data/account/current_account = computer.get_account() + var/datum/computer_network/network = computer.get_network() + var/datum/nano_module/program/atm/module = NM + + if(href_list["access_escrow"]) + var/provider_id = input(user, "Enter the network ID for the account which was escrowed.") + if(!provider_id || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/escrow_login = input(user, "Enter the login for the account which was escrowed.") + if(!escrow_login || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/escrow_pass = input(user, "Enter the PIN for the account which was escrowed. If a PIN was not set, enter the account password.") + if(!escrow_pass || !CanInteract(user, state)) + return TOPIC_HANDLED + + var/datum/money_account/escrow/escrowed_account = SSmoney_accounts.get_escrow(escrow_login, escrow_pass, provider_id) + if(!escrowed_account) + to_chat(user, SPAN_WARNING("An escrow account matching the input information could not be found.")) + return TOPIC_HANDLED + + var/confirm = alert(user, "An escrow account matching the input information was found. Would you like to withdraw the available balance and close the account?", "Close escrow account", "No", "Yes") + if(confirm == "Yes" && CanInteract(user, state)) + var/obj/item/stock_parts/computer/money_printer/printer = computer.get_component(PART_MPRINTER) + if(!printer) + to_chat(user, SPAN_WARNING("There's no cryptographic printer installed!")) + return TOPIC_HANDLED + + if(printer.can_print(escrowed_account.money, escrowed_account.currency)) + // This shouldn't occur barring admin intervention, but just in case. + var/money_to_print = escrowed_account.money + var/err = escrowed_account.withdraw(escrowed_account.money, "Cash withdrawal", computer.get_nid()) + if(err) + to_chat(user, SPAN_WARNING("Cash withdrawal failed: [err].")) + else + printer.print_money(money_to_print, escrowed_account.currency, user) + qdel(escrowed_account) + return TOPIC_REFRESH + else + to_chat(user, "\The [printer] does not have enough stored plastic!") + return TOPIC_HANDLED + return TOPIC_HANDLED + + var/datum/money_account/child/network/current_money_account = current_account?.money_account + if(!current_money_account) + return TOPIC_REFRESH + + if(href_list["change_mode"]) + if(module.prog_mode == STATE_LOGIN) + return TOPIC_REFRESH + + switch(href_list["change_mode"]) + if("main") + module.prog_mode = STATE_MAIN + if("transfer") + module.prog_mode = STATE_TRANSFER + if("log") + module.prog_mode = STATE_LOG + + return TOPIC_REFRESH + + switch(module.prog_mode) + if(STATE_LOGIN) + if(href_list["pin_entry"]) + var/pin = input(user, "Enter the PIN for this account:", "PIN entry") as text|null + pin = sanitize(pin) + if(!pin || !CanInteract(user, state)) + return TOPIC_HANDLED + if(pin == current_money_account.remote_access_pin) + to_chat(user, SPAN_NOTICE("Login successful. Welcome [current_account.fullname]!")) + module.current_pin = pin + module.prog_mode = STATE_MAIN + return TOPIC_REFRESH + + if(STATE_MAIN) // Withdrawals, activating modifications early, changing security level. + if(href_list["withdrawal"]) + var/obj/item/stock_parts/computer/money_printer/printer = computer.get_component(PART_MPRINTER) + if(!printer) + to_chat(user, SPAN_WARNING("There's no cryptographic printer installed!")) + return TOPIC_HANDLED + + var/amount = input(user, "Input the amount of cash to withdrawal:", "Cash withdrawal", 0) as num|null + + if(!amount || !CanInteract(user,state)) + return TOPIC_HANDLED + + if(printer.can_print(amount, current_money_account.currency)) + var/err = current_money_account.withdraw(amount, "Cash withdrawal", computer.get_nid()) + if(err) + to_chat(user, SPAN_WARNING("Cash withdrawal failed: [err].")) + else + printer.print_money(amount, current_money_account.currency, user) + + return TOPIC_REFRESH + else + to_chat(user, "\The [printer] does not have enough stored plastic!") + return TOPIC_HANDLED + + if(href_list["activate_modification"]) + var/index = text2num(href_list["activate_modification"]) + var/list/pending_mods = current_money_account.pending_modifications + if(length(pending_mods) < index) + return TOPIC_REFRESH + + var/datum/account_modification/pending_mod = pending_mods[index] + if(!pending_mod.allow_early) + return TOPIC_HANDLED + + var/confirm = alert(user, "Are you sure you would like to activate this client modification early?", "Early activation", "No", "Yes") + if(confirm != "Yes" || !CanInteract(user, state)) + return TOPIC_HANDLED + + pending_mod.modify_account() + return TOPIC_REFRESH + + if(href_list["change_sec_level"]) + if(current_money_account.security_level == 0) + var/new_pin = input("Enter the PIN you'd like to add to the account:") as text|null + new_pin = sanitize(new_pin) + if(!new_pin) + return TOPIC_HANDLED + var/confirm = alert(user, "You are about to set the PIN for this account to '[new_pin]'. Are you sure?", "Add PIN", "No", "Yes") + if(confirm == "Yes" && CanInteract(user, state)) + current_money_account.security_level = 1 + current_money_account.remote_access_pin = new_pin + module.current_pin = new_pin + return TOPIC_REFRESH + else + current_money_account.security_level = 0 + current_money_account.remote_access_pin = null + module.current_pin = null + return TOPIC_REFRESH + + if(STATE_TRANSFER) + if(href_list["transfer_account"]) + var/account = input(user, "Enter the account login you wish to transfer funds to. Leave blank if you wish to transfer money to a network's parent account:") as text|null + if(!account) + return TOPIC_HANDLED + + module.target_account = sanitize_for_account(account) + return TOPIC_REFRESH + if(href_list["transfer_network"]) + var/target_network = input(user, "Enter the network you wish to transfer funds to. Leave blank if you wish to transfer money on the current network:") as text|null + if(!target_network) + return TOPIC_HANDLED + + module.target_network = sanitize(target_network) + return TOPIC_REFRESH + + if(href_list["transfer_amount"]) + var/amount = input(user, "Enter the amount to transfer:") as num|null + if(!amount) + return TOPIC_HANDLED + + module.transfer_amount = round(abs(amount)) // No negative values. + return TOPIC_REFRESH + + if(href_list["transfer_purpose"]) + var/purpose = input(user, "Enter the purpose for this transaction:") as text|null + if(!purpose) + return + module.transfer_purpose = sanitize(purpose) + return TOPIC_REFRESH + + if(href_list["perform_transfer"]) + if(!module.transfer_amount) + to_chat(user, SPAN_WARNING("You must enter an amount to transfer!")) + return TOPIC_HANDLED + + var/datum/money_account/target_account = network.get_financial_account(module.target_account, module.target_network ? module.target_network : network.network_id) + if(!istype(target_account)) + to_chat(user, SPAN_WARNING("Unable to perform transaction. [target_account]")) + return TOPIC_HANDLED + + var/err = current_money_account.transfer(target_account, module.transfer_amount, module.transfer_purpose) + if(err) + to_chat(user, SPAN_WARNING("Funds transfer failed: [err].")) + return TOPIC_HANDLED + else + to_chat(user, SPAN_NOTICE("Funds transfer successful!")) + module.transfer_amount = 0 + module.transfer_purpose = null + module.target_account = null + module.target_account = null + + module.prog_mode = STATE_MAIN + return TOPIC_REFRESH + + if(STATE_LOG) + if(href_list["next_page"]) + module.log_page += 1 + return TOPIC_REFRESH + if(href_list["prev_page"]) + module.log_page = max(module.log_page - 1, 0) + return TOPIC_REFRESH + +/datum/nano_module/program/atm + var/transfer_amount + var/transfer_purpose + var/target_account + var/target_network + + var/prog_mode = STATE_LOGIN + var/current_pin + + var/log_page = 0 + +/datum/nano_module/program/atm/ui_interact(mob/user, ui_key = "main", var/datum/nanoui/ui = null, var/force_open = 1, var/datum/topic_state/state = global.default_topic_state) + var/list/data = host.initial_data() + var/datum/computer_file/data/account/current_account = program.computer.get_account() + var/datum/money_account/child/network/current_money_account = current_account?.money_account + if(!istype(current_account)) + prog_mode = STATE_LOGIN + data["login_prompt"] = "No account logged in. Please login through your system to proceed." + else if(!current_money_account) + prog_mode = STATE_LOGIN + data["login_prompt"] = "No financial account is associated with this network account. Please contact your financial provider." + else if(current_money_account.security_level > 0 && current_pin != current_money_account.remote_access_pin) + current_pin = null + prog_mode = STATE_LOGIN + data["login_prompt"] = "Enter the remote access pin for this account:" + data["prompt_pin"] = TRUE + else + if(prog_mode == STATE_LOGIN) + prog_mode = STATE_MAIN + + switch(prog_mode) + if(STATE_MAIN) + data["account_name"] = current_money_account.account_name + data["balance"] = current_money_account.format_value_by_currency(current_money_account.get_balance()) + data["interest_rate"] = current_money_account.interest_rate + data["withdrawal_limit"] = current_money_account.format_value_by_currency(current_money_account.withdrawal_limit) + data["current_withdrawal"] = current_money_account.format_value_by_currency(current_money_account.current_withdrawal) + data["transaction_fee"] = current_money_account.format_value_by_currency(current_money_account.transaction_fee) + + if(length(current_money_account.pending_modifications)) + data["pending_mods"] = list() + var/index = 0 + for(var/datum/account_modification/pending_mod in current_money_account.pending_modifications) + index++ + data["pending_mods"] += list(pending_mod.get_ui_data() + list("index" = index)) + + data["pin_secured"] = current_money_account.security_level >= 1 + + if(STATE_TRANSFER) + data["trans_amount"] = transfer_amount + data["trans_purpose"] = transfer_purpose + data["trans_account"] = target_account + data["trans_network"] = target_network + + if(STATE_LOG) + data["transactions"] = list() + var/list/transaction_log = reverselist(current_money_account.transaction_log) + var/log_length = length(transaction_log) + log_page = clamp(log_page, 0, CEILING(log_length/TRANSACTIONS_PER_PAGE)) + + data["next_page"] = log_length > (log_page + 1)*TRANSACTIONS_PER_PAGE + data["prev_page"] = log_page != 0 + transaction_log = transaction_log.Copy(min(log_length, log_page*TRANSACTIONS_PER_PAGE + 1), min(log_length + 1, (log_page + 1)*TRANSACTIONS_PER_PAGE + 1)) + for(var/datum/transaction/logged in transaction_log) + data["transactions"] += list(list( + "date" = logged.date, + "time" = logged.time, + "purpose" = logged.purpose, + "amount" = current_money_account.format_value_by_currency(logged.amount), + "target" = logged.get_target_name(), + "source" = logged.get_source_name() + )) + + if(prog_mode == STATE_LOGIN && length(SSmoney_accounts.all_escrow_accounts)) + data["escrow_providers"] = english_list(SSmoney_accounts.get_escrow_provider_ids()) + + data["prog_mode"] = prog_mode + ui = SSnano.try_update_ui(user, src, ui_key, ui, data, force_open) + if (!ui) + ui = new(user, src, ui_key, "atm_program.tmpl", "Automated Teller Utility", 600, 450, state = state) + if(host.update_layout()) + ui.auto_update_layout = 1 + ui.set_auto_update(1) + ui.set_initial_data(data) + ui.open() + +/datum/computer_file/program/atm/process_cash(obj/item/cash/received, mob/user) + var/datum/nano_module/program/atm/module = NM + if(module.prog_mode == STATE_LOGIN) // Can only deposit while logged in. + return + + var/datum/computer_file/data/account/current_account = computer.get_account() + var/datum/money_account/child/network/current_money_account = current_account?.money_account + + if(!current_money_account) + return + + var/err = current_money_account.deposit(received.absolute_worth, "Cash deposit", computer.get_nid()) + if(err) + to_chat(user, SPAN_WARNING("Cash deposit failed: [err].")) + else + SSnano.update_uis(module) + return received.absolute_worth + +#undef TRANSACTIONS_PER_PAGE + +#undef STATE_LOGIN +#undef STATE_MAIN +#undef STATE_TRANSFER +#undef STATE_LOG \ No newline at end of file diff --git a/code/modules/modular_computers/file_system/programs/generic/email_client.dm b/code/modules/modular_computers/file_system/programs/generic/email_client.dm index dd73361bea8..dc19be186ed 100644 --- a/code/modules/modular_computers/file_system/programs/generic/email_client.dm +++ b/code/modules/modular_computers/file_system/programs/generic/email_client.dm @@ -184,7 +184,7 @@ if(message_source) data["folder"] = folder var/list/all_messages = list() - for(var/datum/computer_file/data/email_message/message in message_source) + for(var/datum/computer_file/data/email_message/message in reverselist(message_source)) all_messages.Add(list(list( "title" = message.title, "body" = digitalPencode2html(message.stored_data), diff --git a/code/modules/modular_computers/file_system/programs/generic/folding.dm b/code/modules/modular_computers/file_system/programs/generic/folding.dm index f6767f081dc..3025669b2fe 100644 --- a/code/modules/modular_computers/file_system/programs/generic/folding.dm +++ b/code/modules/modular_computers/file_system/programs/generic/folding.dm @@ -45,7 +45,7 @@ if(!I) to_chat(usr, SPAN_WARNING("Unable to locate ID card for transaction.")) return TOPIC_HANDLED - var/datum/money_account/account = get_account(I.associated_account_number) + var/datum/money_account/account = get_glob_account(I.associated_account_id) var/earned = current_interval * (SCIENCE_MONEY_PER_MINUTE * computer.get_processing_power()) account.deposit(earned, "Completed FOLDING@SPACE project.") to_chat(usr, SPAN_NOTICE("Transferred [earned] to your account.")) diff --git a/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm b/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm index 54aadb3a8b0..5c886e87632 100644 --- a/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm +++ b/code/modules/modular_computers/file_system/programs/generic/ntdownloader.dm @@ -151,7 +151,7 @@ // Only those programs our user can run will show in the list if(!P.can_run(get_access(user), user, FALSE) && P.requires_access_to_download) continue - if(!P.is_supported_by_hardware(program.computer.get_hardware_flag(), user, TRUE)) + if(!P.is_supported_by_hardware(program.computer.get_hardware_flag(), user, FALSE)) continue category_list.Add(list(list( "filename" = P.filename, diff --git a/code/modules/modular_computers/networking/_network.dm b/code/modules/modular_computers/networking/_network.dm index 50f2649419e..bc56b357eb2 100644 --- a/code/modules/modular_computers/networking/_network.dm +++ b/code/modules/modular_computers/networking/_network.dm @@ -20,6 +20,10 @@ var/datum/extension/network_device/modem/modem var/datum/extension/network_device/acl/access_controller + var/datum/extension/network_device/bank/banking_mainframe + var/datum/money_account/parent/network/parent_account + var/list/datum/extension/network_device/money_cube/money_cubes = list() + var/network_features_enabled = NET_ALL_FEATURES var/intrusion_detection_enabled var/intrusion_detection_alarm @@ -34,6 +38,17 @@ SSnetworking.networks[network_id] = src /datum/computer_network/Destroy() + // Backup the parent account on the bank mainframe, if it exists + if(parent_account) + money_to_storage() + + if(banking_mainframe) + banking_mainframe.backup = parent_account + parent_account = null + else // Too bad! Trigger an escrow panic and delete the parent account. + trigger_escrow_panic() + QDEL_NULL(parent_account) + for(var/datum/extension/network_device/D in devices) D.disconnect(TRUE) QDEL_NULL_LIST(chat_channels) @@ -49,7 +64,7 @@ return FALSE if(D in devices) return TRUE - + if(!check_connection(D)) return FALSE @@ -77,7 +92,31 @@ else if(istype(D, /datum/extension/network_device/camera)) var/datum/extension/network_device/camera/C = D add_camera_to_channels(C, C.channels) - + else if(istype(D, /datum/extension/network_device/bank)) + if(banking_mainframe) + return FALSE + var/datum/extension/network_device/bank/B = D + if(B.backup) + if(!parent_account) + parent_account = B.backup + parent_account.owner_name = network_id + add_log("Recovered financial account from backup", newtag) + else if(parent_account != B.backup) + return FALSE + + B.backup = null + + banking_mainframe = D + add_log("New banking mainframe set", newtag) + else if(istype(D, /datum/extension/network_device/money_cube)) + if(!parent_account) + return FALSE + var/datum/extension/network_device/money_cube/cube = D + money_cubes |= cube + if(banking_mainframe) + parent_account.adjust_money(cube.stored_money) + cube.stored_money = 0 + D.network_tag = newtag devices |= D devices_by_tag[D.network_tag] = D @@ -95,6 +134,8 @@ else if(D in relays) relays -= D add_log("Relay OFFLINE", D.network_tag) + else if(D in money_cubes) + money_cubes -= D else if(istype(D, /datum/extension/network_device/camera)) var/datum/extension/network_device/camera/C = D remove_camera_from_channels(C, C.channels) @@ -108,9 +149,15 @@ if(!router) add_log("Router offline, network shutting down", D.network_tag) qdel(src) - if(D == access_controller) + else if(D == access_controller) access_controller = null add_log("Access controller offline. Network security offline.", D.network_tag) + else if(D == modem) + modem = null + add_log("Modem offline. PLEXUS access disabled.", D.network_tag) + else if(D == banking_mainframe) + banking_mainframe = null + add_log("Banking mainframe offline. Financial services disabled.") return TRUE /datum/computer_network/proc/get_unique_tag(nettag) @@ -161,7 +208,7 @@ if(!broadcast_strength) continue - // For long ranged devices, checking to make sure there's at least a functional broadcaster somewhere. + // For long ranged devices, checking to make sure there's at least a functional broadcaster somewhere. functional_broadcaster = TRUE var/d_z = get_z(D.holder) @@ -169,7 +216,7 @@ if(!ARE_Z_CONNECTED(d_z, b_z)) continue - + if(d_z != b_z) // If the broadcaster is not in the same z-level as the device, the broadcast strength is halved. broadcast_strength = round(broadcast_strength/2) var/distance = get_dist(get_turf(B.holder), get_turf(D.holder)) @@ -222,7 +269,7 @@ var/datum/computer_network/target_network = SSnetworking.networks[target_id] if(!target_network) return - + if(target_network == src) if(network_features_enabled & feature) return target_network @@ -286,6 +333,15 @@ results += device.holder return results +/datum/computer_network/proc/get_device_extensions_by_type(type, list/accesses) + var/list/results = list() + var/bypass_auth = !accesses + for(var/datum/extension/network_device/device in devices) + if(istype(device, type)) + if(bypass_auth || device.has_access(accesses)) + results += device + return results + /datum/computer_network/proc/get_tags_by_type(var/type) var/list/results = list() for(var/tag in devices_by_tag) diff --git a/code/modules/modular_computers/networking/accounts/_network_accounts.dm b/code/modules/modular_computers/networking/accounts/_network_accounts.dm index 9ef74773dd8..41980d51d64 100644 --- a/code/modules/modular_computers/networking/accounts/_network_accounts.dm +++ b/code/modules/modular_computers/networking/accounts/_network_accounts.dm @@ -25,7 +25,7 @@ continue if(E.backup) result[E.login] = E - + return result /datum/computer_network/proc/add_account(datum/computer_file/data/account/acc, accesses) @@ -55,7 +55,11 @@ if(add_account(EA, accesses)) if(user) user.store_account_credentials(EA.login, EA.password, network_id) - return TRUE + . = TRUE + + if(banking_mainframe && parent_account && banking_mainframe.auto_money_accounts) + EA.money_account = new(null, parent_account, banking_mainframe.auto_interest_rate, banking_mainframe.auto_withdrawal_limit, banking_mainframe.auto_transaction_fee, EA) + return FALSE /datum/computer_network/proc/rename_account(old_login, desired_login) diff --git a/code/modules/modular_computers/networking/accounts/account.dm b/code/modules/modular_computers/networking/accounts/account.dm index 91aa2dfbe47..0c6170ed2ea 100644 --- a/code/modules/modular_computers/networking/accounts/account.dm +++ b/code/modules/modular_computers/networking/accounts/account.dm @@ -24,6 +24,8 @@ var/notification_sound = "*beep*" var/backup = FALSE // Backups are not returned when searching for accounts, but can be recovered using the accounts program. + var/datum/money_account/child/network/money_account + copy_string = "(Backup)" /datum/computer_file/data/account/calculate_size() @@ -54,6 +56,10 @@ groups.Cut() parent_groups.Cut() + if(money_account) + money_account.on_escrow(TRUE) // Don't bother sending an e-mail since it's about to be deleted anyway. + QDEL_NULL(money_account) + QDEL_NULL_LIST(inbox) QDEL_NULL_LIST(outbox) QDEL_NULL_LIST(spam) @@ -66,7 +72,15 @@ /datum/computer_file/data/account/proc/all_incoming_emails() return (inbox | spam | deleted) -/datum/computer_file/data/account/proc/receive_mail(var/datum/computer_file/data/email_message/received_message, var/datum/computer_network/network) +/datum/computer_file/data/account/proc/receive_mail(datum/computer_file/data/email_message/received_message) + inbox.Add(received_message) + + for(var/weakref/os_ref in logged_in_os) + var/datum/extension/interactive/os/os = os_ref.resolve() + if(istype(os)) + os.mail_received(received_message) + else + logged_in_os -= os_ref /datum/computer_file/data/account/Clone(rename) . = ..(TRUE) // We always rename the file since a copied account is always a backup. diff --git a/code/modules/modular_computers/networking/accounts/id_card.dm b/code/modules/modular_computers/networking/accounts/id_card.dm index ff02525b5a6..ea5f9eb8ab5 100644 --- a/code/modules/modular_computers/networking/accounts/id_card.dm +++ b/code/modules/modular_computers/networking/accounts/id_card.dm @@ -1,5 +1,4 @@ /obj/item/card/id/network - var/network_id // The network_id that this card is paired to. var/weakref/current_account color = COLOR_GRAY80 detail_color = COLOR_SKY_BLUE @@ -22,11 +21,11 @@ for(var/group in access_account.parent_groups) // Membership in a child group grants access to anything with an access requirement set to the parent group. . += "[group].[location]" -/obj/item/card/id/network/proc/resolve_account() +/obj/item/card/id/network/proc/resolve_account(net_feature = NET_FEATURE_ACCESS) if(!current_account) return var/datum/extension/network_device/D = get_extension(src, /datum/extension/network_device) - var/datum/computer_network/network = D.get_network(NET_FEATURE_ACCESS) + var/datum/computer_network/network = D.get_network(net_feature) var/login = associated_network_account["login"] var/password = associated_network_account["password"] @@ -86,7 +85,7 @@ current_account = null return TOPIC_REFRESH - + if(href_list["login_account"]) if(login_account()) to_chat(usr, SPAN_NOTICE("Account successfully logged in.")) @@ -112,6 +111,11 @@ current_account = weakref(check_account) return TRUE +/obj/item/card/id/network/proc/get_network_id() + var/datum/extension/network_device/D = get_extension(src, /datum/extension/network_device) + var/datum/computer_network/network = D?.get_network() + return network?.network_id + /obj/item/card/id/network/verb/adjust_settings() set name = "Adjust Settings" set category = "Object" diff --git a/code/modules/modular_computers/networking/device_types/bank.dm b/code/modules/modular_computers/networking/device_types/bank.dm new file mode 100644 index 00000000000..79cadea0239 --- /dev/null +++ b/code/modules/modular_computers/networking/device_types/bank.dm @@ -0,0 +1,227 @@ +// Interaction with the network's parent account should go through this device. Wraps many normal functions of accounts. +/datum/extension/network_device/bank + var/datum/money_account/parent/backup // If the network goes down, the bank device can hold onto it until it's recovered. + + var/list/admin_accounts = list() // Currently these accounts don't have any additional access, but are alerted when something goes wrong. + + var/auto_money_accounts = FALSE // Whether creating a network account will auto-create a money account + + // Preset values for automatically generated money accounts + var/auto_interest_rate + var/auto_withdrawal_limit + var/auto_transaction_fee + +/datum/extension/network_device/bank/Destroy() + backup?.escrow_panic() + qdel(backup) + . = ..() + +/datum/extension/network_device/bank/connect() + . = ..() + if(.) + // Setup for pre-mapped banking. + var/datum/computer_network/net = SSnetworking.networks[network_id] + if(!net) + return + var/obj/machinery/network/bank/bank_holder = holder + if(!istype(bank_holder)) + return + if(!net.parent_account && bank_holder.preset_account_name) + create_parent_account(bank_holder.preset_account_name, bank_holder.preset_fractional_reserve) + + if(bank_holder.auto_money_accounts) + auto_money_accounts = TRUE + auto_interest_rate = bank_holder.auto_interest_rate + auto_withdrawal_limit = bank_holder.auto_withdrawal_limit + auto_transaction_fee = bank_holder.auto_transaction_fee + + +/datum/extension/network_device/bank/disconnect(net_down) + var/datum/computer_network/net = SSnetworking.networks[network_id] + if(!net || !net.parent_account) // At this point the network should still exist, something has gone wrong. + return ..() + + // Send out a warning e-mail to admin accounts. + var/datum/computer_file/data/email_message/warning = new() + + warning.title = "URGENT: Financial services have entered recovery mode" + warning.source = "financial_services@[network_id]" + warning.stored_data = "Due to unknown circumstances, the banking mainframe on the network has been disconnected. All non-physical\ + assets have been frozen awaiting the reactivation of the banking mainframe." + email_admin_accounts(warning) + + // Store the money within money cubes on the network. + var/num_cubes = length(net.money_cubes) + var/money_per_cube = FLOOR(net.parent_account.money / num_cubes) + for(var/i in 1 to net.money_cubes) + var/datum/extension/network_device/money_cube/cube = net.money_cubes[i] + cube.stored_money += money_per_cube + + if(i == num_cubes) + cube.stored_money += net.parent_account.money % num_cubes + + net.parent_account.money = 0 + + . = ..() + +/datum/extension/network_device/bank/proc/theft_alert(datum/extension/network_device/money_cube/cube, lost_funds) + var/datum/computer_file/data/email_message/theft_email = new() + var/datum/computer_network/network = get_network() + var/datum/money_account/parent/victim = get_parent_account() + if(!victim) + return + + theft_email.title = "URGENT: Theft of finances detected!" + theft_email.source = "financial_services@[network_id]" + theft_email.stored_data = "A theft has been detected by our automated loss prevention systems, leading to a loss of [victim.format_value_by_currency(lost_funds)]. \ + The compromised financial storage device had the ID tag '[cube.network_tag]'. Loss prevention measures have been activated, and will be triggered if further losses occur." + + email_admin_accounts(theft_email) + + if(network) + var/datum/computer_file/data/email_message/child_theft_email = new() + child_theft_email.title = "Notice of financial loss prevention" + child_theft_email.source = "financial_services@[network_id]" + child_theft_email.stored_data = "Automated loss prevention systems have been triggered on the network, and withdrawal limits on all accounts have been temporarily lifted. Please contact your financial provider for more information." + + network.email_child_accounts(child_theft_email) + + var/datum/account_modification/theft_prevention/mod = new(victim) + victim.pending_modifications += mod + +/datum/extension/network_device/bank/proc/get_parent_account() + var/datum/computer_network/network = get_network() + + return network?.parent_account + +/datum/extension/network_device/bank/proc/create_parent_account(account_name, fractional_reserve) + var/datum/computer_network/network = get_network() + + if(!network || network.parent_account) + return FALSE + + network.parent_account = new(null, network.network_id) + network.parent_account.account_name = account_name + network.parent_account.account_id = account_name // Not used for actual access, but useful for display. + network.parent_account.fractional_reserve = fractional_reserve + return TRUE + +// For internal use only, does not check fractional reserve limits. Otherwise, use transactions. +/datum/extension/network_device/bank/proc/adjust_money(amount) + var/datum/money_account/parent/parent_account = get_parent_account() + parent_account?.adjust_money(amount) + +/datum/extension/network_device/bank/proc/log_msg(msg, machine_id) + var/datum/money_account/parent/parent_account = get_parent_account() + + if(!parent_account) + return "Unable to access account. Contact your financial provider" + + return parent_account.log_msg(msg, machine_id) + +/datum/extension/network_device/bank/proc/deposit(amount, purpose, machine_id) + var/datum/money_account/parent/parent_account = get_parent_account() + + if(!parent_account) + return "Unable to access account. Contact your financial provider" + + return parent_account.deposit(amount, purpose, machine_id) + +/datum/extension/network_device/bank/proc/withdraw(amount, purpose, machine_id) + var/datum/money_account/parent/parent_account = get_parent_account() + + if(!parent_account) + return "Unable to access account. Contact your financial provider" + + return parent_account.withdraw(amount, purpose, machine_id) + +/datum/extension/network_device/bank/proc/transfer(to_account, amount, purpose) + var/datum/money_account/parent/parent_account = get_parent_account() + + if(!parent_account) + return "Unable to access account. Contact your financial provider" + + return parent_account.transfer(to_account, amount, purpose) + +/datum/extension/network_device/bank/proc/rename_account(new_name) + var/datum/money_account/parent/parent_account = get_parent_account() + + if(parent_account) + parent_account.account_name = sanitize(new_name) + +// Getters follow +/datum/extension/network_device/bank/proc/get_balance() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.money + +/datum/extension/network_device/bank/proc/get_formatted_balance() + var/datum/money_account/parent/parent_account = get_parent_account() + if(!parent_account) + return + + return parent_account.format_value_by_currency(parent_account.money) + +/datum/extension/network_device/bank/proc/get_currency() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.currency + +/datum/extension/network_device/bank/proc/get_account_name() + var/datum/money_account/parent/parent_account = get_parent_account() + + return parent_account?.account_name + +/datum/extension/network_device/bank/proc/get_child_accounts() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.children + +/datum/extension/network_device/bank/proc/get_child_totals() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.child_totals + +/datum/extension/network_device/bank/proc/get_formatted_child_totals() + var/datum/money_account/parent/parent_account = get_parent_account() + if(!parent_account) + return + + return parent_account.format_value_by_currency(parent_account.child_totals) + +/datum/extension/network_device/bank/proc/get_fractional_reserve() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.fractional_reserve + +/datum/extension/network_device/bank/proc/get_pending_modifications() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.pending_modifications + +/datum/extension/network_device/bank/proc/get_transaction_log() + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.transaction_log + +/datum/extension/network_device/bank/proc/has_mod_type(type) + var/datum/money_account/parent/parent_account = get_parent_account() + return parent_account?.has_mod_type(type) + +/datum/extension/network_device/bank/proc/add_admin_account(var/account_login) + var/datum/computer_network/net = get_network() + if(!net) + return FALSE + var/datum/computer_file/data/account/admin = net.find_account_by_login(account_login) + if(admin) + admin_accounts |= account_login + return TRUE + +/datum/extension/network_device/bank/proc/remove_admin_account(account_login) + admin_accounts -= account_login + +/datum/extension/network_device/bank/proc/email_admin_accounts(datum/computer_file/data/email_message/to_send) + var/datum/computer_network/net = get_network() + if(!net) + return + + for(var/admin_login in admin_accounts) + var/datum/computer_file/data/account/admin = net.find_account_by_login(admin_login) + if(!admin) + admin_accounts -= admin_login + continue + + net.receive_email(admin, "financial-services@[net.network_id]", to_send, FALSE) \ No newline at end of file diff --git a/code/modules/modular_computers/networking/emails/_email.dm b/code/modules/modular_computers/networking/emails/_email.dm index 87872ea6cae..9c611ee0e1c 100644 --- a/code/modules/modular_computers/networking/emails/_email.dm +++ b/code/modules/modular_computers/networking/emails/_email.dm @@ -21,21 +21,15 @@ add_log("EMAIL LOG: [sender.login]@[network_id] -> [recipient.login]@[recipient_network.network_id] title: [sent.title].") return TRUE -/datum/computer_network/proc/receive_email(datum/computer_file/data/account/recipient, sender_address, datum/computer_file/data/email_message/received) - if(!(recipient in get_accounts_unsorted())) +/datum/computer_network/proc/receive_email(datum/computer_file/data/account/recipient, sender_address, datum/computer_file/data/email_message/received, safety = TRUE) + if(safety && !(recipient in get_accounts_unsorted())) return FALSE - + var/datum/computer_file/data/email_message/received_copy = received.Clone() received_copy.set_timestamp() - recipient.inbox.Add(received_copy) - - for(var/weakref/os_ref in recipient.logged_in_os) - var/datum/extension/interactive/os/os = os_ref.resolve() - if(istype(os)) - os.mail_received(received_copy) - else - recipient.logged_in_os -= os_ref - + + recipient.receive_mail(received_copy) + if(recipient.broadcaster) for(var/datum/computer_file/data/account/email_account in get_accounts_unsorted()) if(email_account.broadcaster) diff --git a/code/modules/modular_computers/networking/finance/money_accounts.dm b/code/modules/modular_computers/networking/finance/money_accounts.dm new file mode 100644 index 00000000000..d58d2aec9ae --- /dev/null +++ b/code/modules/modular_computers/networking/finance/money_accounts.dm @@ -0,0 +1,107 @@ +// Money accounts specifically for use with networks. Child accounts are attached to user accounts. +/datum/money_account/parent/network + var/datum/computer_file/data/email_message/bankrupt_email + var/datum/computer_file/data/email_message/escrow_email + + var/get_network_error = FALSE // Network accounts need at least one money storage device and a banking mainframe to function. + +/datum/money_account/parent/network/New(account_type, network_id) + owner_name = network_id + if(owner_name) + generate_emails() + . = ..() + +/datum/money_account/parent/network/deposit(amount, purpose, machine_id) + var/err = get_network_error() + if(err) + return err + . = ..() + +/datum/money_account/parent/network/withdraw(amount, purpose, machine_id) + var/err = get_network_error() + if(err) + return err + . = ..() + +/datum/money_account/parent/network/transfer(to_account, amount, purpose) + var/err = get_network_error() + if(err) + return err + . = ..() + +/datum/money_account/parent/network/proc/get_network_error() + var/datum/computer_network/net = SSnetworking.networks[owner_name] + if(!net || !net.banking_mainframe) + return "The financial system is currently in recovery mode. Please contact your financial provider for further information" + if(!length(net.money_cubes)) + return "Finances are currently frozen due to a lack of financial storage devices. Please contact your financial provider for further information" + +// Generates e-mails to send in the case escrow accounts are generated. +/datum/money_account/parent/network/proc/generate_emails() + bankrupt_email = new() + bankrupt_email.title = "URGENT: Notification of financial insolvency" + bankrupt_email.source = "financial_services@[owner_name]" + bankrupt_email.stored_data = "Due to unforseen circumstances, an escrow panic was triggered for your financial provider. \ + Unfortunately, due to acute insolvency, an escrow account for your outstanding balance was \ + unable to be created. Please contact your financial provider or legal services for further information." + escrow_email = new() + escrow_email.title = "URGENT: Notification of financial issue" + escrow_email.source = "financial_services@[owner_name]" + escrow_email.stored_data = "Due to unforseen circumstances, an escrow panic was triggered for your financial provider. \ + An escrow account has been opened for you containing some or all of your outstanding balance of your account. \ + Escrow accounts are accessible from any financial terminal using your prior account information, \ + and the financial provider ID '[owner_name]'. Please contact your financial provider for further information." +/datum/money_account/child/network + var/weakref/network_account + +/datum/money_account/child/network/New(account_type, p_account, n_interest, n_withdrawal_limit, n_transaction_fee, datum/computer_file/data/account/attached_account) + if(attached_account) + account_name = "[attached_account.fullname]'s account" + account_id = attached_account.login + remote_access_pin = attached_account.password // Temporary 'pin', not actually referenced until security level is changed. + network_account = weakref(attached_account) + . = ..() + +/datum/money_account/child/network/Destroy(force) + var/datum/computer_file/data/account/attached_account = network_account.resolve() + if(attached_account) + attached_account.money_account = null + . = ..() + +/datum/money_account/child/network/on_escrow(ignore_email = FALSE) + if(!money || ignore_email) // We didn't have any money to escrow, so leave it be. + return ..() + . = ..() // The amount escrowed. + // On escrow, send e-mails explaining the situation to players. These are magic e-mails that don't check normal network requirements. + var/datum/money_account/parent/network/net_parent = parent_account + if(!istype(net_parent) || !net_parent.escrow_email || !net_parent.bankrupt_email) + return + + var/datum/computer_file/data/account/attached_account = network_account.resolve() + if(!istype(attached_account)) + return + + var/datum/computer_file/data/email_message/to_send = . > 0 ? net_parent.escrow_email.Clone() : net_parent.bankrupt_email.Clone() + to_send.set_timestamp() + attached_account.receive_mail(to_send) + +/datum/money_account/child/network/deposit(amount, purpose, machine_id) + var/datum/money_account/parent/network/net_parent = parent_account + var/err = net_parent.get_network_error() + if(err) + return err + . = ..() + +/datum/money_account/child/network/withdraw(amount, purpose, machine_id) + var/datum/money_account/parent/network/net_parent = parent_account + var/err = net_parent.get_network_error() + if(err) + return err + . = ..() + +/datum/money_account/child/network/transfer(to_account, amount, purpose) + var/datum/money_account/parent/network/net_parent = parent_account + var/err = net_parent.get_network_error() + if(err) + return err + . = ..() diff --git a/code/modules/modular_computers/networking/finance/money_cube.dm b/code/modules/modular_computers/networking/finance/money_cube.dm new file mode 100644 index 00000000000..019fb4ae444 --- /dev/null +++ b/code/modules/modular_computers/networking/finance/money_cube.dm @@ -0,0 +1,154 @@ +/obj/item/money_cube + name = "cryptographic currency storage device" + desc = "A digital currency storage device." + + icon = 'icons/obj/items/money_cube.dmi' + icon_state = ICON_STATE_WORLD + + w_class = ITEM_SIZE_LARGE // Not actually that large, but quite heavy. + + origin_tech = "{'programming':2,'magnets':3,'materials':3}" + + material = /decl/material/solid/metal/titanium + matter = list( + /decl/material/solid/metal/platinum = MATTER_AMOUNT_PRIMARY, + /decl/material/solid/metal/stainlesssteel = MATTER_AMOUNT_REINFORCEMENT, + /decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT + ) + + // These cubes can't manually connect to a network. Either set these or require players to connect one to a financial database + var/initial_network + var/initial_network_key + +/obj/item/money_cube/Initialize(ml, material_key) + . = ..() + set_extension(src, /datum/extension/network_device/money_cube, initial_network, initial_network_key, RECEIVER_STRONG_WIRELESS, initial_network ? TRUE : FALSE) + +/obj/item/money_cube/examine(mob/user, distance, infix, suffix) + . = ..() + var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device) + if(cube.check_connection()) + to_chat(user, "It appears to be active.") + else + to_chat(user, "It must be activated by connection to a financial database prior to use.") + +/obj/item/money_cube/attackby(obj/item/W, mob/user) + . = ..() + if(IS_MULTITOOL(W)) + if(user.skill_check(SKILL_FINANCE, SKILL_ADEPT) || user.skill_check(SKILL_DEVICES, SKILL_EXPERT)) + var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device) + if(!cube || (!cube.check_connection() && !cube.stored_money)) + to_chat(user, SPAN_WARNING("\The [src] doesn't respond to your atttempts to interface with it.")) + return + user.visible_message("[usr] begins fiddling with \the [src]!") + if(do_after(user, 30 SECONDS, src)) // This takes quite awhile. + remove_cash(user) + else + to_chat(user, SPAN_WARNING("You have no idea how to interface with \the [src]!")) + return + +/obj/item/money_cube/proc/remove_cash(mob/user) + var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device) + if(!cube) + return + + var/amount_to_dump = 0 + var/currency_to_dump + var/datum/computer_network/network = cube.get_network() + if(!network) + amount_to_dump = cube.stored_money + cube.stored_money = 0 + else + // Remove the money cube's imaginary share in the parent account's money. + if(network.banking_mainframe) + var/datum/extension/network_device/bank/parent_bank = network.banking_mainframe + + var/num_cubes = length(network.money_cubes) + if(!num_cubes) // Cube is aware of the network but not vice versa. Something has gone wrong. + return + + currency_to_dump = parent_bank.get_currency() + // At most, only half the money stored can be stolen. + amount_to_dump = round(parent_bank.get_balance() / max(num_cubes, 2)) + cube.stored_money + parent_bank.adjust_money(-amount_to_dump) + cube.stored_money = 0 // This should be 0 regardless, but just in case. + + if(num_cubes == 1 || parent_bank.has_mod_type(/datum/account_modification/theft_prevention)) + network.trigger_escrow_panic() + else + parent_bank.theft_alert(cube, amount_to_dump) + + else // There's no active bank device, tampering will the cube will drop some money but trigger a total escrow panic. + amount_to_dump = FLOOR(cube.stored_money/2) + cube.stored_money -= amount_to_dump + network.trigger_escrow_panic() + + cube.disconnect() + cube.network_id = null + cube.key = null + if(amount_to_dump) + var/obj/item/cash/cash = new(get_turf(src)) + cash.set_currency(currency_to_dump ? currency_to_dump : global.using_map.default_currency) + cash.adjust_worth(amount_to_dump) + if(user) + // This should dispense a single holographic banknote or something eventually + user.visible_message("\The [src] emits a warning tone before rapidly dispensing cash from its internal printer!") + update_icon() + +/obj/item/money_cube/afterattack(atom/target, mob/user, proximity_flag, click_parameters) + . = ..() + if(istype(target, /obj/machinery/network/bank)) + var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device) + var/datum/extension/network_device/bank/attacked_bank = get_extension(target, /datum/extension/network_device) + if(!attacked_bank || !attacked_bank.check_connection()) + to_chat(user, SPAN_WARNING("\The [target] appears to be non-functional!")) + return + + cube.set_new_id(attacked_bank.network_id) + cube.set_new_key(attacked_bank.key) + + if(cube.check_connection()) + playsound(user, 'sound/machines/chime.ogg', 30, 1) + to_chat(user, SPAN_NOTICE("\The [src] pings as it successfully links to \the [target]!")) + update_icon() + else + to_chat(user, SPAN_WARNING("\The [src] fails to connect to \the [target].")) + +/obj/item/money_cube/on_update_icon() + . = ..() + icon_state = get_world_inventory_state() + var/datum/extension/network_device/money_cube/cube = get_extension(src, /datum/extension/network_device) + var/active = cube?.check_connection() + if(active) + icon_state = "[icon_state]-active" + +/datum/extension/network_device/money_cube + var/stored_money // Money temporarily stored in the case that the parent account on the network vanishes. + internet_allowed = TRUE + +/datum/extension/network_device/money_cube/disconnect(net_down) + var/obj/H = holder + H.queue_icon_update() + if(!stored_money) + return ..() + + var/datum/computer_network/net = SSnetworking.networks[network_id] + if(!net_down) + // Divide up the stored money among other cubes. If no other cubes exist, trigger a partial/full escrow panic. + if(!net) + return ..() + var/list/other_cubes = net.money_cubes - src + var/num_cubes = length(other_cubes) + if(num_cubes) + var/money_per_cube = FLOOR(stored_money / num_cubes) + for(var/i in 1 to num_cubes) + var/datum/extension/network_device/money_cube/other_cube = other_cubes[other_cubes.len] + if(i != num_cubes) + other_cube.stored_money += money_per_cube + stored_money -= money_per_cube + else + other_cube.stored_money += stored_money + stored_money = 0 + else + net.trigger_escrow_panic() + . = ..() \ No newline at end of file diff --git a/code/modules/modular_computers/networking/finance/network_finance.dm b/code/modules/modular_computers/networking/finance/network_finance.dm new file mode 100644 index 00000000000..6f5394ebdb3 --- /dev/null +++ b/code/modules/modular_computers/networking/finance/network_finance.dm @@ -0,0 +1,75 @@ +// Rather than keeping track of all the money stored in each money cube, we assume that money is distributed evenly among them, +// and only call this when the banking mainframe or network goes down. +/datum/computer_network/proc/money_to_storage(var/additional_money) + if(!parent_account) + return + + var/total_money = parent_account.money + additional_money + if(!total_money) + return + + var/num_cubes = length(money_cubes) + + if(!num_cubes) // No money cubes on the network + return + + var/money_per_cube = FLOOR(total_money / num_cubes) + + for(var/i in 1 to num_cubes) + var/datum/extension/network_device/money_cube/cube = money_cubes[i] + cube.stored_money += money_per_cube + if(i == num_cubes) + cube.stored_money += total_money % num_cubes + + parent_account.money = 0 + +// Re-collect all money from money cubes and trigger an escrow panic. +/datum/computer_network/proc/trigger_escrow_panic() + if(!parent_account) + return + + var/cube_money = 0 + for(var/datum/extension/network_device/money_cube/cube in money_cubes) + cube_money += cube.stored_money + cube.stored_money = 0 + + parent_account.money += cube_money + parent_account.escrow_panic() + + if(banking_mainframe && length(banking_mainframe.admin_accounts)) + var/datum/computer_file/data/email_message/escrow_email = new() + escrow_email.title = "URGENT: Escrow panic triggered" + escrow_email.source = "financial_services@[network_id]" + escrow_email.stored_data = "Due to the potential for extreme financial loss, an escrow panic has been automatically triggered for the parent financial account on the network.\ + Escrow accounts have been opened for all client accounts on the network." + + var/debt = parent_account.child_totals // Money still remains in children accounts. + if(debt) + escrow_email.stored_data += " Due to acute insolvency, a debt of [parent_account.format_value_by_currency(debt)] has been incurred for client accounts." + + banking_mainframe.email_admin_accounts(escrow_email) + +/datum/computer_network/proc/email_child_accounts(datum/computer_file/data/email_message/to_send) + for(var/datum/computer_file/data/account/child in get_accounts_unsorted()) + if(child.money_account) + receive_email(child, "financial-services@[network_id]", to_send, FALSE) + +// Returns money account datum on success. Returns error text on failure. +/datum/computer_network/proc/get_financial_account(target_login, target_network_id) + . = "Unable to locate financial account over the network. Please try again later" + var/datum/computer_network/target_network = get_internet_connection(target_network_id, NET_FEATURE_FINANCE) + if(!target_network || !target_network.parent_account) + return + + if(!target_login) + return target_network.parent_account + + var/datum/computer_file/data/account/target_account = target_network.find_account_by_login(target_login) + + if(!target_account) + return + + if(!target_account.money_account) + return + + return target_account.money_account \ No newline at end of file diff --git a/code/modules/modular_computers/networking/finance/network_pos.dm b/code/modules/modular_computers/networking/finance/network_pos.dm new file mode 100644 index 00000000000..ac9c2ed0a3a --- /dev/null +++ b/code/modules/modular_computers/networking/finance/network_pos.dm @@ -0,0 +1,215 @@ +// Variation of the EFTPOS for network use +// TODO: Could possibly be turned into a modular program or similar. +/obj/item/network_pos + name = "network point of sale system" + desc = "A network enabled point of sale system, used to perform quick transactions." + icon = 'icons/obj/items/device/eftpos.dmi' + icon_state = "eftpos" + material = /decl/material/solid/plastic + matter = list(/decl/material/solid/silicon = MATTER_AMOUNT_REINFORCEMENT, /decl/material/solid/metal/copper = MATTER_AMOUNT_REINFORCEMENT) + + var/account_id + var/account_provider // Target network ID. + + var/transaction_amount + var/transaction_purpose + var/transaction_authorized + +/obj/item/network_pos/Initialize(ml, material_key) + . = ..() + set_extension(src, /datum/extension/network_device) + +/obj/item/network_pos/attack_self(var/mob/user) + ui_interact(user) + return TRUE + +/obj/item/network_pos/attackby(obj/item/W, mob/user) + . = ..() + if(istype(W, /obj/item/card/id/network)) + if(!transaction_amount) + to_chat(user, SPAN_NOTICE("No transaction in progress!")) + return + var/obj/item/card/id/network/payment_card = W + var/payment_id = payment_card.associated_network_account["login"] + var/payment_network = payment_card.get_network_id() + + if(!payment_id) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: no user account detected!")) + return + var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device) + var/datum/computer_network/net = device?.get_network() + if(!net) + to_chat(user, SPAN_WARNING("\The [src] flashes a network connection error.")) + return + if(!account_id) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: no account connected!")) + return + if(!transaction_authorized) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: transaction is not authorized!")) + return + var/datum/money_account/target_account = get_money_account() + if(!istype(target_account)) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: unable to find connected account!")) + return + var/datum/money_account/payment_account = net.get_financial_account(payment_id, payment_network) + if(!istype(payment_account)) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: unable to find user account!")) + return + if(payment_account.currency != target_account.currency) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: accounts do not hold the same currency.")) + return + + // Hold this here so no one can pull a fast one by rapidly changing the amount. + var/old_transaction = transaction_amount + var/confirm = alert(user, "Confirm transaction: [payment_account.format_value_by_currency(old_transaction)]", "Confirm Transaction", "Cancel", "Confirm") + if(confirm != "Confirm" || !CanInteract(usr, DefaultTopicState())) + return + + if(payment_account.security_level > 0) + var/pin = input("Enter the PIN for this account:", "PIN Entry") as text + pin = sanitize(pin) + if(pin != payment_account.remote_access_pin) + to_chat(user, SPAN_WARNING("\The [src] flashes an error: incorrect PIN!")) + return + + var/err = payment_account.transfer(target_account, old_transaction, transaction_purpose) + if(!err) + visible_message(SPAN_NOTICE("[html_icon(src)] \The [src] pings: transaction successful!")) + playsound(src, 'sound/machines/chime.ogg', 50, 1) + + // Reset transaction info. + transaction_amount = null + transaction_purpose = null + transaction_authorized = FALSE + else + to_chat(user, SPAN_WARNING("\The [src] flashes an error: [err]")) + return + + if(IS_PEN(W) || IS_MULTITOOL(W)) + visible_message("\The [user] begins resetting \the [src] with \the [W].") + if(do_after(user, 5 SECONDS, src)) + to_chat(user, SPAN_NOTICE("You reset \the [src]!")) + account_id = null + account_provider = null + transaction_authorized = FALSE + +/obj/item/network_pos/ui_interact(mob/user, ui_key = "main",var/datum/nanoui/ui = null) + var/data[0] + var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device) + if(!device) + data["error"] = "Error in device initialization: contact administrator." + else + var/datum/computer_network/net = device?.get_network() + if(!net) + data["error"] = "Unable to connect to network. Please check your network settings." + else + var/datum/money_account/target_account = get_money_account() + + data["authorized"] = transaction_authorized + data["transaction_amount"] = transaction_amount ? transaction_amount : "" + data["transaction_purpose"] = transaction_purpose ? transaction_purpose : "" + + if(target_account) + data["account_id"] = account_id + data["account_provider"] = account_provider ? account_provider : net.network_id + + ui = SSnano.try_update_ui(user, src, ui_key, ui, data) + if (!ui) + ui = new(user, src, ui_key, "network_pos.tmpl", "Network POS Settings", 540, 326) + ui.set_initial_data(data) + ui.open() + +/obj/item/network_pos/OnTopic(mob/user, href_list, datum/topic_state/state) + var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device) + if(href_list["network_settings"]) + device.ui_interact(user) + return TOPIC_HANDLED + + var/datum/computer_network/net = device?.get_network() + if(!net) + return TOPIC_REFRESH + + if(href_list["connect_account"]) + if(!account_id) + var/new_id = input(user, "Enter the ID of the account you would like to connect to \the [src]:", "ID Entry") as text + new_id = sanitize(new_id) + + var/new_provider = input(user, "Enter the ID of the network the account is located on. Leave blank to use device network:", "Network Entry") as text + var/datum/money_account/target_account = net.get_financial_account(new_id, new_provider ? new_provider : net.network_id) + if(!istype(target_account)) + to_chat(user, SPAN_WARNING("An error occured: [target_account]")) + return TOPIC_REFRESH + + if(target_account.security_level < 1) + to_chat(user, SPAN_WARNING("For security reasons, all accounts connected to \the [src] must have a PIN.")) + return + account_id = new_id + account_provider = new_provider + return TOPIC_REFRESH + else + to_chat(user, SPAN_WARNING("You must reset \the [src] first!")) + return TOPIC_REFRESH + + if(href_list["authorize_transaction"]) + if(transaction_authorized) + transaction_authorized = FALSE + return TOPIC_REFRESH + + var/datum/money_account/target_account = get_money_account() + if(!target_account) + return TOPIC_REFRESH + + if(target_account.security_level < 1) + to_chat(user, SPAN_WARNING("For security reasons, all accounts connected to \the [src] must have a PIN.")) + account_id = null + account_provider = null + return TOPIC_REFRESH + + var/pin_entry = input(user, "Please enter the PIN for the connected account [account_id]:", "PIN Entry") as text + pin_entry = sanitize(pin_entry) + if(!pin_entry || !CanInteract(usr, DefaultTopicState())) + return TOPIC_HANDLED + + if(pin_entry != target_account.remote_access_pin) + to_chat(user, SPAN_WARNING("Incorrect PIN! Please try again.")) + return TOPIC_HANDLED + + transaction_authorized = TRUE + playsound(src, 'sound/machines/chime.ogg', 50, 1) + return TOPIC_REFRESH + + if(href_list["transaction_amount"]) + var/amount = input(user, "Enter the amount to transfer:") as num|null + + transaction_amount = round(amount) + transaction_authorized = FALSE + return TOPIC_REFRESH + + if(href_list["transaction_purpose"]) + var/purpose = input(user, "Enter the purpose for this transaction:") as text|null + if(!purpose) + return + transaction_purpose = sanitize(purpose) + return TOPIC_REFRESH + + if(href_list["settings"]) + var/datum/extension/network_device/D = get_extension(src, /datum/extension/network_device) + D.ui_interact(user) + return TOPIC_HANDLED + +/obj/item/network_pos/proc/get_money_account() + if(!account_id) + return + var/datum/extension/network_device/device = get_extension(src, /datum/extension/network_device) + var/datum/computer_network/net = device?.get_network() + if(!net) // If the net goes down, don't reset the authorization. + return + var/datum/money_account/target_account = net.get_financial_account(account_id, account_provider ? account_provider : net.network_id) + + // Reset the system. + if(!istype(target_account)) + account_id = null + account_provider = null + return + + return target_account \ No newline at end of file diff --git a/code/modules/modular_computers/networking/machinery/bank.dm b/code/modules/modular_computers/networking/machinery/bank.dm new file mode 100644 index 00000000000..11f09e36034 --- /dev/null +++ b/code/modules/modular_computers/networking/machinery/bank.dm @@ -0,0 +1,38 @@ +/obj/machinery/network/bank + name = "banking mainframe" + desc = "A mainframe used for managing network finances. It must be interfaced with remotely." + icon = 'icons/obj/machines/tcomms/hub.dmi' + icon_state = "hub" + network_device_type = /datum/extension/network_device/bank + main_template = "banking_mainframe.tmpl" + construct_state = /decl/machine_construction/default/panel_closed + uncreated_component_parts = null + base_type = /obj/machinery/network/bank + + var/preset_account_name + var/preset_fractional_reserve + + var/auto_money_accounts = FALSE + var/auto_interest_rate + var/auto_withdrawal_limit + var/auto_transaction_fee + +/obj/machinery/network/bank/ui_data(mob/user, ui_key) + . = ..() + + var/datum/extension/network_device/bank = get_extension(src, /datum/extension/network_device) + if(!bank) + return + + var/datum/computer_network/network = bank.get_network() + if(!network) + return + + .["money_cubes"] = list() + for(var/datum/extension/network_device/money_cube/cube in network.money_cubes) + var/z = get_z(cube.holder) + var/obj/effect/overmap/visitable/cube_location = z ? global.overmap_sectors["[z]"] : null + .["money_cubes"] += list(list( + "tag" = cube.network_tag, + "location" = cube_location ? cube_location.name : "Unknown" + )) \ No newline at end of file diff --git a/code/modules/modular_computers/networking/machinery/modem.dm b/code/modules/modular_computers/networking/machinery/modem.dm index deb1b71e07f..d91bb1cbc83 100644 --- a/code/modules/modular_computers/networking/machinery/modem.dm +++ b/code/modules/modular_computers/networking/machinery/modem.dm @@ -15,7 +15,8 @@ "Communication Systems" = NET_FEATURE_COMMUNICATION, "Access systems" = NET_FEATURE_ACCESS, "Security systems" = NET_FEATURE_SECURITY, - "Filesystem access" = NET_FEATURE_FILESYSTEM + "Filesystem access" = NET_FEATURE_FILESYSTEM, + "Financial systems" = NET_FEATURE_FINANCE ) /obj/machinery/network/modem/OnTopic(mob/user, href_list, datum/topic_state/state) @@ -47,5 +48,5 @@ fdata["name"] = feature fdata["enabled"] = M.allowed_features & feature_options[feature] features.Add(list(fdata)) - + .["features"] = features \ No newline at end of file diff --git a/code/modules/modular_computers/os/_os.dm b/code/modules/modular_computers/os/_os.dm index c4ad2b1c037..c1dc94e4fc8 100644 --- a/code/modules/modular_computers/os/_os.dm +++ b/code/modules/modular_computers/os/_os.dm @@ -17,7 +17,7 @@ var/default_icon = "generic" //Overlay icon for programs that have a screen overlay the host doesn't have. // Used for deciding if various tray icons need to be updated - var/last_battery_percent + var/last_battery_percent var/last_world_time var/list/last_header_icons @@ -62,7 +62,7 @@ var/datum/computer_file/data/account/access_account = get_account() if(access_account) var/datum/computer_network/network = get_network() - if(network) + if(network) var/location = "[network.network_id]" . += "[access_account.login]@[location]" // User access uses '@' for(var/group in access_account.groups) @@ -73,7 +73,7 @@ var/obj/item/card/id/I = user.GetIdCard() if(I) . += I.GetAccess(access_account?.login) // Ignore any access that's already on the user account. - + // Returns the current account, if possible. User var is passed only for updating program access from ID, if no account is found. /datum/extension/interactive/os/proc/get_account(var/mob/user) if(!current_account) @@ -137,7 +137,7 @@ var/new_password = sanitize(input(user, "Enter your account password:", "Account password", default_password) as text|null) if(!new_password || !CanUseTopic(user, global.default_topic_state)) return - + if(login_account(new_login, new_password, user)) to_chat(user, SPAN_NOTICE("Account login successful: Welcome [new_login]!")) else @@ -147,7 +147,7 @@ on = FALSE for(var/datum/computer_file/program/P in running_programs) kill_program(P, 1) - + var/obj/item/stock_parts/computer/network_card/network_card = get_component(PART_NETWORK) if(network_card) var/datum/extension/network_device/D = get_extension(network_card, /datum/extension/network_device) @@ -307,4 +307,15 @@ /datum/extension/interactive/os/proc/mail_received(datum/computer_file/data/email_message/received) var/datum/computer_file/program/email_client/e_client = locate() in running_programs if(e_client) - e_client.mail_received(received) \ No newline at end of file + e_client.mail_received(received) + +// Returns the amount of cash taken, if any. +/datum/extension/interactive/os/proc/process_cash(obj/item/cash/received_cash, mob/user) + if(active_program) + return active_program.process_cash(received_cash, user) + +/datum/extension/interactive/os/proc/get_nid() + var/obj/item/stock_parts/computer/network_card/card = get_component(PART_NETWORK) + if(!card) + return + return card.get_nid() \ No newline at end of file diff --git a/code/modules/modular_computers/os/files.dm b/code/modules/modular_computers/os/files.dm index 20c3dc26d23..131c2601ba9 100644 --- a/code/modules/modular_computers/os/files.dm +++ b/code/modules/modular_computers/os/files.dm @@ -548,7 +548,7 @@ left_to_transfer = max(0, left_to_transfer - get_transfer_speed()) if(!left_to_transfer) if(copying) - return transfer_to.store_file(transferring, directory_to, TRUE) + return transfer_to.store_file(transferring.Clone(), directory_to, TRUE) else . = transfer_from.delete_file(transferring) // Check if we can delete the file. if(. == OS_FILE_SUCCESS) diff --git a/config/example/game_options.txt b/config/example/game_options.txt index f74b06332bc..ea383accca3 100644 --- a/config/example/game_options.txt +++ b/config/example/game_options.txt @@ -67,6 +67,19 @@ MONKEY_DELAY 0 ALIEN_DELAY 0 ANIMAL_DELAY 0 +### Economy ### + +## Values for these settings are given in days. + +WITHDRAW_PERIOD 1 +INTEREST_PERIOD 1 + +## Delays for modifications to accounts. +INTEREST_MOD_DELAY 2 +WITHDRAW_MOD_DELAY 3 +TRANSACTION_MOD_DELAY 2 +FRACTIONAL_RESERVE_MOD_DELAY 3 +ANTI_TAMPER_MOD_DELAY 2 ### Miscellaneous ### diff --git a/icons/obj/items/money_cube.dmi b/icons/obj/items/money_cube.dmi new file mode 100644 index 00000000000..145490db0a8 Binary files /dev/null and b/icons/obj/items/money_cube.dmi differ diff --git a/maps/~mapsystem/maps.dm b/maps/~mapsystem/maps.dm index 5b79e29f1d6..eba9655b703 100644 --- a/maps/~mapsystem/maps.dm +++ b/maps/~mapsystem/maps.dm @@ -312,7 +312,7 @@ var/global/const/MAP_HAS_RANK = 2 //Rank system, also togglable news_network.CreateFeedChannel("The Gibson Gazette", "Editor Mike Hammers", 1, 1) if(!station_account) - station_account = create_account("[station_name()] Primary Account", "[station_name()]", starting_money, ACCOUNT_TYPE_DEPARTMENT) + station_account = create_glob_account("[station_name()] Primary Account", "[station_name()]", starting_money, ACCOUNT_TYPE_DEPARTMENT) for(var/job in allowed_jobs) var/datum/job/J = SSjobs.get_by_path(job) @@ -323,9 +323,9 @@ var/global/const/MAP_HAS_RANK = 2 //Rank system, also togglable for(var/department in station_departments) var/decl/department/dept = SSjobs.get_department_by_type(department) if(istype(dept)) - department_accounts[department] = create_account("[dept.name] Account", "[dept.name]", department_money, ACCOUNT_TYPE_DEPARTMENT) + department_accounts[department] = create_glob_account("[dept.name] Account", "[dept.name]", department_money, ACCOUNT_TYPE_DEPARTMENT) - department_accounts["Vendor"] = create_account("Vendor Account", "Vendor", 0, ACCOUNT_TYPE_DEPARTMENT) + department_accounts["Vendor"] = create_glob_account("Vendor Account", "Vendor", 0, ACCOUNT_TYPE_DEPARTMENT) vendor_account = department_accounts["Vendor"] /datum/map/proc/map_info(var/client/victim) diff --git a/maps/~mapsystem/maps_currency.dm b/maps/~mapsystem/maps_currency.dm index 5365ad66ddd..c8d82799763 100644 --- a/maps/~mapsystem/maps_currency.dm +++ b/maps/~mapsystem/maps_currency.dm @@ -17,7 +17,7 @@ credstick.creator = owner.real_name credstick.currency = owner_account.currency credstick.loaded_worth = min(credstick.max_worth, FLOOR(owner_account.money * transfer_mult)) - owner_account.money -= credstick.loaded_worth + owner_account.adjust_money(-credstick.loaded_worth) return list(credstick) /decl/starting_cash_choice/credstick/half @@ -31,8 +31,8 @@ var/obj/item/cash/cash = new cash.set_currency(owner_account.currency) cash.adjust_worth(FLOOR(owner_account.money * transfer_mult)) - owner_account.money -= cash.absolute_worth - return list(cash) + owner_account.adjust_money(cash.absolute_worth) + return list(cash) /decl/starting_cash_choice/cash/half name = "split between bank account and cash" @@ -47,14 +47,14 @@ var/obj/item/cash/cash = new cash.set_currency(owner_account.currency) cash.adjust_worth(FLOOR(owner_account.money * transfer_mult)) - . += cash + . += cash var/obj/item/charge_stick/credstick = new credstick.creator = owner.real_name credstick.currency = owner_account.currency credstick.loaded_worth = min(credstick.max_worth, FLOOR(owner_account.money * transfer_mult)) . += credstick - owner_account.money -= cash.absolute_worth - owner_account.money -= credstick.loaded_worth + owner_account.adjust_money(-cash.absolute_worth) + owner_account.adjust_money(-credstick.loaded_worth) /decl/starting_cash_choice/split/even name = "split between bank account, cash and charge stick" diff --git a/mods/persistence/modules/world_save/saved_vars/saved_misc.dm b/mods/persistence/modules/world_save/saved_vars/saved_misc.dm index 8c23f7b4f8b..19c67c3abb6 100644 --- a/mods/persistence/modules/world_save/saved_vars/saved_misc.dm +++ b/mods/persistence/modules/world_save/saved_vars/saved_misc.dm @@ -150,7 +150,7 @@ SAVED_VAR(/datum/memory, _owner_ckey) SAVED_VAR(/datum/money_account, account_name) SAVED_VAR(/datum/money_account, owner_name) -SAVED_VAR(/datum/money_account, account_number) +SAVED_VAR(/datum/money_account, account_id) SAVED_VAR(/datum/money_account, remote_access_pin) SAVED_VAR(/datum/money_account, money) SAVED_VAR(/datum/money_account, transaction_log) @@ -745,7 +745,7 @@ SAVED_VAR(/obj/item/radio, listening) /////////////////////////////////////////////////////////////////////////////// SAVED_VAR(/obj/item/card/id, access) SAVED_VAR(/obj/item/card/id, registered_name) -SAVED_VAR(/obj/item/card/id, associated_account_number) +SAVED_VAR(/obj/item/card/id, associated_account_id) SAVED_VAR(/obj/item/card/id, associated_network_account) SAVED_VAR(/obj/item/card/id, age) SAVED_VAR(/obj/item/card/id, blood_type) diff --git a/nano/templates/accounts_terminal.tmpl b/nano/templates/accounts_terminal.tmpl index ce3f41022b4..b7c3bf52ad8 100644 --- a/nano/templates/accounts_terminal.tmpl +++ b/nano/templates/accounts_terminal.tmpl @@ -62,7 +62,7 @@ Account Number:
- #{{:data.account_number}} + #{{:data.account_id}}
@@ -133,7 +133,7 @@
Payroll:
- {{:helper.link('Revoke', 'transferthick-e-w', {'choice' : 'revoke_payroll'}, data.account_number == data.station_account_number ? 'disabled' : null, 'linkDanger')}} + {{:helper.link('Revoke', 'transferthick-e-w', {'choice' : 'revoke_payroll'}, data.account_id == data.ME_number ? 'disabled' : null, 'linkDanger')}} {{else}} @@ -144,7 +144,7 @@
#[D.account_number]#[D.account_id] [D.owner_name][D.format_value_by_currency(D.money)][D.format_value_by_currency(D.get_balance())] [D.suspended ? "Suspended" : "Active"]
{{for data.accounts}} - + diff --git a/nano/templates/atm_program.tmpl b/nano/templates/atm_program.tmpl new file mode 100644 index 00000000000..58424ea3093 --- /dev/null +++ b/nano/templates/atm_program.tmpl @@ -0,0 +1,140 @@ +{{if data.prog_mode == 0}} +

Welcome to the automated teller utility:

+

{{:data.login_prompt}}

+ {{if data.prompt_pin}} + {{:helper.link('Enter PIN', 'key', {'pin_entry' : 1}, null)}} + {{/if}} + {{if data.escrow_providers}} +

+ Escrow accounts are currently open for the following financial providers: {{:data.escrow_providers}} +
+ {{:helper.link('Access Escrow Account', 'key', {'access_escrow' : 1}, null)}} + {{/if}} +{{else}} +
{{:helper.link('#' + value.account_number, '', {'choice' : 'view_account_detail', 'account_index' : value.account_index})}}{{:helper.link('#' + value.account_id, '', {'choice' : 'view_account_detail', 'account_index' : value.account_index})}} {{:value.owner_name}} {{:value.suspended}}
+ {{:helper.link('Account Info', null, {'change_mode' : "main"}, data.prog_mode == 1 ? 'selected' : null)}} + {{:helper.link('Transactions', null, {'change_mode' : "transfer"}, data.prog_mode == 2 ? 'selected' : null)}} + {{:helper.link('Logs', null, {'change_mode' : "log"}, data.prog_mode == 3 ? 'selected' : null)}} +
+
+ {{if data.prog_mode == 1}} +

{{:data.account_name}}

+ {{if data.pin_secured}} + Account is currently secured with a PIN. + {{:helper.link('Remove PIN', 'cancel', {'change_sec_level' : 1}, null)}} + {{else}} + Account is currently unsecured. + {{:helper.link('Add PIN', 'key', {'change_sec_level' : 1}, null)}} + {{/if}} +
+
+
+ Current Balance: +
+
+ {{:data.balance}} +
+
+
+
+
+ Interest Rate: +
+
+ {{:data.interest_rate}} +
+
+
+
+
+ Withdrawal Limit: +
+
+ {{:data.current_withdrawal}} / {{:data.withdrawal_limit}} +
+
+
+
+
+ Transaction Fee: +
+
+ {{:data.transaction_fee}} +
+
+ {{:helper.link('Withdrawal', null, {'withdrawal' : 1}, null)}} + {{if data.pending_mods}} +

+

Account modifications

+ + + + + + + {{/for}} +
DescriptionActivation timeEarly triggerCancel + {{for data.pending_mods}} +
{{:value.desc}}{{:value.countdown}} + {{if value.allow_early}} + {{:helper.link('', 'alert', {'activate_modification' : value.index})}} + {{/if}} + + {{if value.allow_cancel}} + {{:helper.link('', 'cancel', {'cancel_modification' : value.index})}} + {{/if}} +
+ {{/if}} + {{else data.prog_mode == 2}} +

Transaction:

+
+
+ Transaction Amount: +
+
+ {{:helper.link(data.trans_amount ? data.trans_amount : '', null, {'transfer_amount' : 1})}} +
+
+
+
+ Target account: +
+
+ {{:helper.link(data.trans_account ? data.trans_account : '', null, {'transfer_account' : 1})}} +
+
+
+
+ Target network: +
+
+ {{:helper.link(data.trans_network ? data.trans_network : '', null, {'transfer_network' : 1})}} +
+
+
+
+ Transfer Purpose: +
+
+ {{:data.trans_purpose ? data.trans_purpose : ''}}{{:helper.link('', 'pencil', {'transfer_purpose' : 1})}} +
+
+
+ {{:helper.link('Perform transfer', null, {'perform_transfer' : 1})}} + {{else data.prog_mode == 3}} +

Transaction Logs

+ + + + + + + + {{/for}} +
ToFromAmountPurposeTime + {{for data.transactions}} +
{{:value.target}}{{:value.source}}{{:value.amount}}{{:value.purpose}}{{:value.time}}, {{:value.date}}
+ {{:helper.link('<', null, {'prev_page' : 1}, data.prev_page ? null : 'disabled')}} + {{:helper.link('>', null, {'next_page' : 1}, data.next_page ? null : 'disabled')}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/nano/templates/banking_mainframe.tmpl b/nano/templates/banking_mainframe.tmpl new file mode 100644 index 00000000000..2891a7dee5d --- /dev/null +++ b/nano/templates/banking_mainframe.tmpl @@ -0,0 +1,23 @@ +{{if data.error}} +

An error has occured:

+ Additional information: {{:data.error}}
+ Please try again. If the problem persists contact your system administrator for assistance. +
+ {{:helper.link('REFRESH', null, { "refresh" : 1 })}} + {{:helper.link("NETWORK SETTINGS", null, { "settings" : 1 })}} +
+{{else}} + Financial management must be done remotely.
+
+ {{:helper.link("NETWORK SETTINGS", null, { "settings" : 1 })}} +
+

Financial Storage Devices:

+ + + + + {{/for}} +
Network TagLocation + {{for data.money_cubes}} +
{{:value.tag}}{{:value.location}}
+{{/if}} \ No newline at end of file diff --git a/nano/templates/finance_management.tmpl b/nano/templates/finance_management.tmpl new file mode 100644 index 00000000000..0332b4e2b30 --- /dev/null +++ b/nano/templates/finance_management.tmpl @@ -0,0 +1,260 @@ +{{if data.prog_state == -1}} +
+
+ An error has occured: +
+
+ {{:data.error}} +
+
+ {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}} +{{else data.prog_state == 0}} +

Welcome to the financial management utility:

+ {{:helper.link('Manage parent financial account', null, {'parent_mode' : 1}, null)}} + {{:helper.link('Manage client accounts', null, {'child_mode' : 1}, null)}} +{{else data.prog_state == 1}} + {{if data.create_parent}} +

No parent account currently exists on the network.

+ {{:helper.link('Create parent account', null, {'create_parent_account' : 1}, null)}} + {{else}} +

Financial management: {{:data.parent_name}}

+
+
+ Current Balance: +
+
+ {{:data.parent_balance}} +
+
+
+
+ Client Balance Total: +
+
+ {{:data.child_totals}} +
+
+
+
+ Number of clients: +
+
+ #{{:data.no_child_accounts}} +
+
+
+
+ Fractional Reserve: +
+
+ {{:data.fractional_reserve}} +
+
+ {{:helper.link('Withdrawal', null, {'withdrawal' : 1}, null)}} +

+

Account modifications

+ + + + + + + {{/for}} +
DescriptionActivation timeEarly triggerCancel + {{for data.pending_parent_mods}} +
{{:value.desc}}{{:value.countdown}} + {{if value.allow_early}} + {{:helper.link('', 'alert', {'activate_modification' : value.index})}} + {{/if}} + + {{if value.allow_cancel}} + {{:helper.link('', 'cancel', {'cancel_modification' : value.index})}} + {{/if}} +
+ {{:helper.link('Add modification', null, {'add_modification' : 1}, null)}} +

Admin accounts

+ Admin accounts are notified in the case of financial incidents or changes to financial policy. + + + + + {{/for}} +
LoginRemove + {{for data.admin_accounts}} +
{{:value}} + {{:helper.link('', 'cancel', {'remove_admin' : value})}} +
+ {{:helper.link('Add admin', null, {'add_admin' : 1}, null)}} +

+

Perform transaction:

+
+
+ Transaction Amount: +
+
+ {{:helper.link(data.trans_amount ? data.trans_amount : '', null, {'transfer_amount' : 1})}} +
+
+
+
+ Target account: +
+
+ {{:helper.link(data.trans_account ? data.trans_account : '', null, {'transfer_account' : 1})}} +
+
+
+
+ Target network: +
+
+ {{:helper.link(data.trans_network ? data.trans_network : '', null, {'transfer_network' : 1})}} +
+
+
+
+ Transfer Purpose: +
+
+ {{:data.trans_purpose ? data.trans_purpose : ''}}{{:helper.link('', 'pencil', {'transfer_purpose' : 1})}} +
+
+
+ {{:helper.link('Perform transfer', null, {'perform_transfer' : 1})}} +

+

Transaction Logs:

+ + + + + + + + {{/for}} +
ToFromAmountPurposeTime + {{for data.parent_transactions}} +
{{:value.target}}{{:value.source}}{{:value.amount}}{{:value.purpose}}{{:value.time}}, {{:value.date}}
+ {{:helper.link('<', null, {'prev_page' : 1}, data.prev_page ? null : 'disabled')}} + {{:helper.link('>', null, {'next_page' : 1}, data.next_page ? null : 'disabled')}} +

+ {{/if}} + {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}} +{{else data.prog_state == 2}} + {{if data.accounts}} +

Client Accounts:

+ + +
Account LoginReal NameBalance + {{for data.accounts}} +
{{:helper.link(value.account, '', {'select_account' : value.account})}} + {{:value.fullname}} + {{:value.money}} + {{/for}} +
+
+
+
+ Auto-generate Client Accounts: +
+
+ {{:helper.link(data.auto_accounts ? 'Enabled' : 'Disabled', null, {'toggle_auto_accounts' : 1}, null)}} +
+
+ {{if data.auto_accounts}} +
+
+ Preset interest rate: +
+
+ {{:helper.link(data.auto_interest_rate, null, {'change_auto_interest' : 1}, null)}} +
+
+
+
+ Preset withdrawal limit: +
+
+ {{:helper.link(data.auto_withdrawal_limit, null, {'change_auto_withdrawal' : 1}, null)}} +
+
+
+
+ Preset transaction fee: +
+
+ {{:helper.link(data.auto_transaction_fee, null, {'change_auto_transaction' : 1}, null)}} +
+
+ {{/if}} + {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}} + {{else}} +

Client account: {{:data.child_name}}

+
+
+ Current Balance: +
+
+ {{:data.child_balance}} +
+
+
+
+ Interest Rate: +
+
+ {{:data.interest_rate}} +
+
+
+
+ Withdrawal Limit: +
+
+ {{:data.current_withdrawal}} / {{:data.withdrawal_limit}} +
+
+
+
+ Transaction Fee: +
+
+ {{:data.transaction_fee}} +
+
+
+ {{:helper.link('Close account', 'alert', {'close_account' : 1}, null)}} +

+

Account modifications

+ + + + + + {{/for}} +
DescriptionActivation timeCancel + {{for data.pending_child_mods}} +
{{:value.desc}}{{:value.countdown}} + {{if value.allow_cancel}} + {{:helper.link('', 'cancel', {'cancel_modification' : value.index})}} + {{/if}} +
+ {{:helper.link('Add modification', null, {'add_modification' : 1}, null)}} +

+

Client Transaction Logs:

+ + + + + + + + {{/for}} +
ToFromAmountPurposeTime + {{for data.child_transactions}} +
{{:value.target}}{{:value.source}}{{:value.amount}}{{:value.purpose}}{{:value.time}}, {{:value.date}}
+ {{:helper.link('<', null, {'prev_page' : 1}, data.prev_page ? null : 'disabled')}} + {{:helper.link('>', null, {'next_page' : 1}, data.next_page ? null : 'disabled')}} +

+ {{:helper.link('Go back', 'arrowthickstop-1-w', {'back' : 1}, null)}} + {{/if}} +{{/if}} diff --git a/nano/templates/identification_computer.tmpl b/nano/templates/identification_computer.tmpl index 72277b1ea7d..0aeecefc098 100644 --- a/nano/templates/identification_computer.tmpl +++ b/nano/templates/identification_computer.tmpl @@ -57,7 +57,7 @@ Account Number:
- {{:helper.link(data.id_account_number, 'pencil', {'action' : 'edit', 'account' : 1})}} + {{:helper.link(data.id_account_id, 'pencil', {'action' : 'edit', 'account' : 1})}}
diff --git a/nano/templates/network_pos.tmpl b/nano/templates/network_pos.tmpl new file mode 100644 index 00000000000..ec08ad3d117 --- /dev/null +++ b/nano/templates/network_pos.tmpl @@ -0,0 +1,43 @@ +
+ {{:helper.link("Network Settings", null, { "settings" : 1 })}} +
+
+{{if data.error}} +
+
+ An error has occured: +
+
+ {{:data.error}} +
+
+{{else}} +
+
Transaction Amount:
+
+ {{:helper.link(data.transaction_amount, null, { "transaction_amount" : 1 })}} +
+
+
+
Transaction Purpose:
+
+ {{:helper.link(data.transaction_purpose, null, { "transaction_purpose" : 1 })}} +
+
+
+
Transaction Authorized:
+
+ {{if data.authorized}} + {{:helper.link('AUTHORIZED', 'check', {"authorize_transaction" : 1 })}} + {{else}} + {{:helper.link('UNAUTHORIZED', null, {"authorize_transaction" : 1 })}} + {{/if}} +
+
+
+ {{if data.account_id}} + Connected to account '{{:data.account_id}}@{{:data.account_provider}}' + {{else}} + {{:helper.link('Connect Account', null, { "connect_account" : 1 })}} + {{/if}} +{{/if}} \ No newline at end of file diff --git a/nebula.dme b/nebula.dme index 545dd443671..44caf023f65 100644 --- a/nebula.dme +++ b/nebula.dme @@ -194,6 +194,7 @@ #include "code\controllers\evacuation\evacuation_predicate.dm" #include "code\controllers\evacuation\evacuation_shuttle.dm" #include "code\controllers\evacuation\~evac.dm" +#include "code\controllers\subsystems\accounts.dm" #include "code\controllers\subsystems\air.dm" #include "code\controllers\subsystems\alarm.dm" #include "code\controllers\subsystems\ambience.dm" @@ -779,6 +780,7 @@ #include "code\game\machinery\_machines_base\stock_parts\disk_reader.dm" #include "code\game\machinery\_machines_base\stock_parts\item_holder.dm" #include "code\game\machinery\_machines_base\stock_parts\legacy_parts.dm" +#include "code\game\machinery\_machines_base\stock_parts\money_printer.dm" #include "code\game\machinery\_machines_base\stock_parts\network_lock.dm" #include "code\game\machinery\_machines_base\stock_parts\network_receiver.dm" #include "code\game\machinery\_machines_base\stock_parts\shielding.dm" @@ -1871,10 +1873,14 @@ #include "code\modules\economy\worth_stacks.dm" #include "code\modules\economy\worth_vendomat.dm" #include "code\modules\economy\cael\_economy_misc.dm" +#include "code\modules\economy\cael\account_mod.dm" #include "code\modules\economy\cael\Accounts.dm" #include "code\modules\economy\cael\Accounts_DB.dm" #include "code\modules\economy\cael\ATM.dm" #include "code\modules\economy\cael\EFTPOS.dm" +#include "code\modules\economy\cael\escrow.dm" +#include "code\modules\economy\cael\global_accounts.dm" +#include "code\modules\economy\cael\related_accounts.dm" #include "code\modules\economy\cael\Transactions.dm" #include "code\modules\emotes\emote_define.dm" #include "code\modules\emotes\emote_mob.dm" @@ -2594,6 +2600,7 @@ #include "code\modules\modular_computers\file_system\programs\command\accounts.dm" #include "code\modules\modular_computers\file_system\programs\command\card.dm" #include "code\modules\modular_computers\file_system\programs\command\comm.dm" +#include "code\modules\modular_computers\file_system\programs\command\finances.dm" #include "code\modules\modular_computers\file_system\programs\engineering\alarm_monitor.dm" #include "code\modules\modular_computers\file_system\programs\engineering\atmos_control.dm" #include "code\modules\modular_computers\file_system\programs\engineering\network_monitoring.dm" @@ -2602,6 +2609,7 @@ #include "code\modules\modular_computers\file_system\programs\engineering\shields_monitor.dm" #include "code\modules\modular_computers\file_system\programs\engineering\shutoff_valve.dm" #include "code\modules\modular_computers\file_system\programs\engineering\supermatter_monitor.dm" +#include "code\modules\modular_computers\file_system\programs\generic\atm.dm" #include "code\modules\modular_computers\file_system\programs\generic\camera.dm" #include "code\modules\modular_computers\file_system\programs\generic\configurator.dm" #include "code\modules\modular_computers\file_system\programs\generic\crew_manifest.dm" @@ -2656,6 +2664,7 @@ #include "code\modules\modular_computers\networking\accounts\id_card.dm" #include "code\modules\modular_computers\networking\device_types\_network_device.dm" #include "code\modules\modular_computers\networking\device_types\acl.dm" +#include "code\modules\modular_computers\networking\device_types\bank.dm" #include "code\modules\modular_computers\networking\device_types\broadcaster.dm" #include "code\modules\modular_computers\networking\device_types\id_card.dm" #include "code\modules\modular_computers\networking\device_types\mainframe.dm" @@ -2665,8 +2674,13 @@ #include "code\modules\modular_computers\networking\device_types\stock_part.dm" #include "code\modules\modular_computers\networking\emails\_email.dm" #include "code\modules\modular_computers\networking\emails\email_message.dm" +#include "code\modules\modular_computers\networking\finance\money_accounts.dm" +#include "code\modules\modular_computers\networking\finance\money_cube.dm" +#include "code\modules\modular_computers\networking\finance\network_finance.dm" +#include "code\modules\modular_computers\networking\finance\network_pos.dm" #include "code\modules\modular_computers\networking\machinery\_network_machine.dm" #include "code\modules\modular_computers\networking\machinery\acl.dm" +#include "code\modules\modular_computers\networking\machinery\bank.dm" #include "code\modules\modular_computers\networking\machinery\mainframe.dm" #include "code\modules\modular_computers\networking\machinery\modem.dm" #include "code\modules\modular_computers\networking\machinery\relay.dm"