def home_get(): """ Endpoint for the homepage """ cookie = request.cookies.get('session') if cookie is None or cookie != util.get_session_cookie(): return render_template("start.html") config = bunq.retrieve_config() bunqkeymode = config.get("mode") iftttkeyset = (util.get_ifttt_service_key("") is not None) accounts = util.get_bunq_accounts_with_permissions(config) enableexternal = util.get_external_payment_enabled() bunq_oauth = storage.get_value("bunq2IFTTT", "bunq_oauth") if bunq_oauth is not None and bunqkeymode != "APIkey": expire = arrow.get(bunq_oauth["timestamp"] + 90 * 24 * 3600) oauth_expiry = "{} ({})".format(expire.humanize(), expire.isoformat()) else: oauth_expiry = None # Google AppEngine does not provide fixed ip addresses defaultallips = (os.getenv("GAE_INSTANCE") is not None) return render_template("main.html",\ iftttkeyset=iftttkeyset, bunqkeymode=bunqkeymode, accounts=accounts,\ enableexternal=enableexternal, defaultallips=defaultallips,\ oauth_expiry=oauth_expiry)
def ifttt_account_options(include_any, enable_key): """ Option values for account selection """ errmsg = check_ifttt_service_key() if errmsg: return errmsg, 401 config = bunq.retrieve_config() accounts = util.get_bunq_accounts_with_permissions(config) if include_any: data = {"data": [{"label": "ANY", "value": "ANY"}]} else: data = {"data": []} for acc in accounts: if enable_key is None or (enable_key in acc["perms"] and acc["perms"][enable_key]): ibanstr = acc["iban"] iban_formatted = "" while len(ibanstr) > 4: iban_formatted += ibanstr[:4] + " " ibanstr = ibanstr[4:] iban_formatted += ibanstr data["data"].append({ "label": "{} ({})".format(acc["description"], iban_formatted), "value": acc["iban"] }) return json.dumps(data)
def ifttt_bunq_payment(internal, draft): """ Execute a draft, internal or external payment """ config = bunq.retrieve_config() data = request.get_json() print("[action_payment] input: {}".format(json.dumps(data))) errmsg = None if not internal and not draft and not util.get_external_payment_enabled(): errmsg = "external payments disabled" if "actionFields" not in data: errmsg = "missing actionFields" if errmsg: print("[action_payment] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # get the payment message fields = data["actionFields"] msg = create_payment_message(internal, fields, config) if "errors" in msg or "data" in msg: # error or test payment return json.dumps(msg), 400 if "errors" in msg else 200 # find the source account id source_accid, enabled = check_source_account(internal, draft, config, fields["source_account"]) if source_accid is None: errmsg = "unknown source account: " + fields["source_account"] if not enabled: errmsg = "Payment type not enabled for account: "+\ fields["source_account"] if errmsg: print("[action_payment] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # execute the payment if draft: msg = {"number_of_required_accepts": 1, "entries": [msg]} result = bunq.post( "v1/user/{}/monetary-account/{}/draft-payment".format( config["user_id"], source_accid), msg) else: result = bunq.post( "v1/user/{}/monetary-account/{}/payment".format( config["user_id"], source_accid), msg) print(result) if "Error" in result: return json.dumps({ "errors": [{ "status": "SKIP", "message": result["Error"][0]["error_description"] }] }), 400 return json.dumps( {"data": [{ "id": str(result["Response"][0]["Id"]["id"]) }]})
def get_bunq_accounts(permission=None, config=None): """ Return the list of accounts for the given permission """ if config is None: config = bunq.retrieve_config() result = [] for acc in config["accounts"]: if permission is None or (acc["iban"] in config["permissions"] and \ permission in config["permissions"][acc["iban"]] \ and config["permissions"][acc["iban"]][permission]): result.append(acc) return result
def get_bunq_cards(): """ Return the list of bunq cards """ config = bunq.retrieve_config() data = bunq.get("v1/user/{}/card".format(config["user_id"]), config) results = [] for item in data["Response"]: for typ in item: card = item[typ] if card["status"] == "ACTIVE": if card["type"] != "MASTERCARD_VIRTUAL": print(card) results.append({ "label": card["second_line"], "value": str(card["id"]) }) return sorted(results, key=lambda k: k["label"])
def account_change_permission(iban, permission, value): """ Change a permission on an account """ if permission not in ["Internal", "Draft", "Mutation", "Request", "Card"] \ and not (permission == "External" and get_external_payment_enabled()): print("Invalid permission: " + permission) return False if value not in ["true", "false"]: print("Invalid value: " + value) return False value = (value == "true") config = bunq.retrieve_config() if "permissions" not in config: config["permissions"] = {} if iban not in config["permissions"]: config["permissions"][iban] = {} config["permissions"][iban][permission] = value bunq.save_config(config) return True
def update_bunq_accounts(): """ Update the list of bunq accounts """ config = bunq.retrieve_config() bunq.retrieve_accounts(config) sync_permissions(config) bunq.save_config(config)
def change_card_account(): """ Execute a change card account action """ data = request.get_json() print("[change_card_account] input: {}".format(json.dumps(data))) errmsg = None if "actionFields" not in data: errmsg = "missing actionFields" else: fields = data["actionFields"] expected_fields = ["account", "card"] for field in expected_fields: if field not in fields: errmsg = "missing field: " + field if errmsg: print("[change_card_account] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 fields["account"] = fields["account"].replace(" ", "") # the account NL42BUNQ0123456789 is used for test payments if fields["account"] == "NL42BUNQ0123456789": return json.dumps({"data": [{"id": uuid.uuid4().hex}]}) accountid = None for acc in util.get_bunq_accounts("Card"): if acc["iban"] == fields["account"]: accountid = acc["id"] if accountid is None: errmsg = "unknown account: " + fields["account"] print("[change_card_account] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 if "pin_ordinal" in fields: pinord = fields["pin_ordinal"] else: pinord = "PRIMARY" msg = { "pin_code_assignment": [{ "type": pinord, "monetary_account_id": int(accountid), }] } config = bunq.retrieve_config() data = bunq.get("v1/user/{}/card".format(config["user_id"]), config) for item in data["Response"]: for typ in item: card = item[typ] if str(card["id"]) == str(fields["card"]): for pca in card["pin_code_assignment"]: if pca["type"] != pinord: msg["pin_code_assignment"].append({ "type": pca["type"], "monetary_account_id": pca["monetary_account_id"] }) res = bunq.session_request_encrypted( "PUT", "v1/user/{}/card/{}".format(config["user_id"], fields["card"]), msg, config) if "Error" in res: print(json.dumps(res)) errmsg = "Bunq API call failed, see the logs!" return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 return json.dumps({"data": [{"id": uuid.uuid4().hex}]})
def request_inquiry(): """ Execute a request inquiry action """ data = request.get_json() print("[request_inquiry] input: {}".format(json.dumps(data))) errmsg = None if "actionFields" not in data: errmsg = "missing actionFields" else: fields = data["actionFields"] expected_fields = ["amount", "account", "phone_email_iban"] for field in expected_fields: if field not in fields: errmsg = "missing field: " + field if errmsg: print("[request_inquiry] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 fields["account"] = fields["account"].replace(" ", "") # the account NL42BUNQ0123456789 is used for test payments if fields["account"] == "NL42BUNQ0123456789": return json.dumps({"data": [{"id": uuid.uuid4().hex}]}) accountid = None for acc in util.get_bunq_accounts("PaymentRequest"): if acc["iban"] == fields["account"]: accountid = acc["id"] if accountid is None: errmsg = "unknown account: " + fields["account"] print("[request_inquiry] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # check amount try: amount = float(fields["amount"]) except ValueError: amount = -1 if amount <= 0: errmsg = "only positive amounts allowed: " + fields["amount"] print("[action_payment] ERROR: " + errmsg) return {"errors": [{"status": "SKIP", "message": errmsg}]} # check phone or email bmvalue = fields["phone_email_iban"].replace(" ", "") if "@" in bmvalue: bmtype = "EMAIL" elif bmvalue[:1] == "+" and bmvalue[1:].isdecimal(): bmtype = "PHONE_NUMBER" elif bmvalue[:2].isalpha() and bmvalue[2:4].isdecimal(): bmtype = "IBAN" else: errmsg = "Unrecognized as email, phone or iban: " + bmvalue print("[request_inquiry] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 description = fields["description"] if "description" in fields else "" msg = { "amount_inquired": { "value": "{:.2f}".format(amount), "currency": "EUR", }, "counterparty_alias": { "type": bmtype, "name": bmvalue, "value": bmvalue }, "description": description, "allow_bunqme": True, } print(json.dumps(msg)) config = bunq.retrieve_config() data = bunq.post("v1/user/{}/monetary-account/{}/request-inquiry".format(\ config["user_id"], accountid), msg, config) print(data) if "Error" in data: return json.dumps({ "errors": [{ "status": "SKIP", "message": data["Error"][0]["error_description"] }] }), 400 return json.dumps({"data": [{"id": uuid.uuid4().hex}]})
def target_balance_internal(): """ Execute a target balance internal action """ data = request.get_json() print("[target_balance_internal] input: {}".format(json.dumps(data))) if "actionFields" not in data: errmsg = "missing actionFields" print("[target_balance_internal] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 fields = data["actionFields"] errmsg = check_fields(True, fields) if errmsg: print("[target_balance_internal] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # the account NL42BUNQ0123456789 is used for test payments if fields["account"] == "NL42BUNQ0123456789": return json.dumps({"data": [{"id": uuid.uuid4().hex}]}) # retrieve balance config = bunq.retrieve_config() if fields["payment_type"] == "DIRECT": balance = get_balance(config, fields["account"], fields["other_account"]) if isinstance(balance, tuple): balance, balance2 = balance transfer_amount = fields["amount"] - balance if transfer_amount > balance2: transfer_amount = balance2 else: balance = get_balance(config, fields["account"]) if isinstance(balance, float): transfer_amount = fields["amount"] - balance if isinstance(balance, str): errmsg = balance print("[target_balance_internal] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # construct payment message if "{:.2f}".format(fields["amount"]) == "0.00": errmsg = "No transfer needed, balance already ok" print("[target_balance_internal] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 if transfer_amount > 0 and "top up" in fields["direction"]: paymentmsg = { "amount": { "value": "{:.2f}".format(transfer_amount), "currency": "EUR" }, "counterparty_alias": { "type": "IBAN", "value": fields["account"], "name": "x" }, "description": fields["description"] } account = fields["other_account"] elif transfer_amount < 0 and "skim" in fields["direction"]: paymentmsg = { "amount": { "value": "{:.2f}".format(-transfer_amount), "currency": "EUR" }, "counterparty_alias": { "type": "IBAN", "value": fields["other_account"], "name": "x" }, "description": fields["description"] } account = fields["account"] else: errmsg = "No transfer needed, balance already ok" print("[target_balance_internal] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 print(paymentmsg) # get id and check permissions if fields["payment_type"] == "DIRECT": accid, enabled = payment.check_source_account(True, False, config, account) else: accid, enabled = payment.check_source_account(False, True, config, account) if accid is None: errmsg = "unknown account: " + account if not enabled: errmsg = "Payment type not enabled for account: " + account if errmsg: print("[target_balance_internal] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # execute the payment if fields["payment_type"] == "DIRECT": result = bunq.post( "v1/user/{}/monetary-account/{}/payment".format( config["user_id"], accid), paymentmsg) else: paymentmsg = {"number_of_required_accepts": 1, "entries": [paymentmsg]} result = bunq.post( "v1/user/{}/monetary-account/{}/draft-payment".format( config["user_id"], accid), paymentmsg) print(result) if "Error" in result: return json.dumps({ "errors": [{ "status": "SKIP", "message": result["Error"][0]["error_description"] }] }), 400 return json.dumps( {"data": [{ "id": str(result["Response"][0]["Id"]["id"]) }]})
def target_balance_external(): """ Execute a target balance external action """ data = request.get_json() print("[target_balance_external] input: {}".format(json.dumps(data))) if "actionFields" not in data: errmsg = "missing actionFields" print("[target_balance_external] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 fields = data["actionFields"] errmsg = check_fields(False, fields) if errmsg: print("[target_balance_external] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # the account NL42BUNQ0123456789 is used for test payments if fields["account"] == "NL42BUNQ0123456789": return json.dumps({"data": [{"id": uuid.uuid4().hex}]}) # retrieve balance config = bunq.retrieve_config() balance = get_balance(config, fields["account"]) if isinstance(balance, str): errmsg = balance print("[target_balance_external] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 transfer_amount = fields["amount"] - balance # check for zero transfer if "{:.2f}".format(fields["amount"]) == "0.00": errmsg = "No transfer needed, balance already ok" print("[target_balance_external] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # get account id and check permission if transfer_amount > 0: accid = None for acc in config["accounts"]: if acc["iban"] == fields["account"]: accid = acc["id"] enabled = False if "permissions" in config: if fields["account"] in config["permissions"]: if "PaymentRequest" in config["permissions"]\ [fields["account"]]: enabled = config["permissions"][fields["account"]]\ ["PaymentRequest"] else: accid, enabled = payment.check_source_account(False, True, config, fields["account"]) if accid is None: errmsg = "unknown account: " + fields["account"] if not enabled: errmsg = "Not permitted for account: " + fields["account"] if errmsg: print("[target_balance_external] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 # send request / execute payment if transfer_amount > 0 and "top up" in fields["direction"]: bmvalue = fields["request_phone_email_iban"].replace(" ", "") if "@" in bmvalue: bmtype = "EMAIL" elif bmvalue[:1] == "+" and bmvalue[1:].isdecimal(): bmtype = "PHONE_NUMBER" elif bmvalue[:2].isalpha() and bmvalue[2:4].isdecimal(): bmtype = "IBAN" else: errmsg = "Unrecognized as email, phone or iban: " + bmvalue print("[request_inquiry] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message":\ errmsg}]}), 400 msg = { "amount_inquired": { "value": "{:.2f}".format(transfer_amount), "currency": "EUR", }, "counterparty_alias": { "type": bmtype, "name": bmvalue, "value": bmvalue }, "description": fields["request_description"], "allow_bunqme": True, } print(json.dumps(msg)) config = bunq.retrieve_config() result = bunq.post("v1/user/{}/monetary-account/{}/request-inquiry"\ .format(config["user_id"], accid), msg, config) elif transfer_amount < 0 and "skim" in fields["direction"]: paymentmsg = { "amount": { "value": "{:.2f}".format(-transfer_amount), "currency": "EUR" }, "counterparty_alias": { "type": "IBAN", "value": fields["payment_account"], "name": fields["payment_name"] }, "description": fields["payment_description"] } print(paymentmsg) paymentmsg = {"number_of_required_accepts": 1, "entries": [paymentmsg]} result = bunq.post( "v1/user/{}/monetary-account/{}/draft-payment".format( config["user_id"], accid), paymentmsg) else: errmsg = "No transfer needed, balance already ok" print("[target_balance_external] ERROR: " + errmsg) return json.dumps({"errors": [{"status": "SKIP", "message": errmsg}]})\ , 400 print(result) if "Error" in result: return json.dumps({ "errors": [{ "status": "SKIP", "message": result["Error"][0]["error_description"] }] }), 400 return json.dumps( {"data": [{ "id": str(result["Response"][0]["Id"]["id"]) }]})