def test_descriptionless_fails(random_transaction): value = random_transaction.value() if value > 0: with pytest.raises(TransactionError): t = Transaction(value) assert (t.is_null()) else: t = Transaction(value)
def test_negative_fails(random_transaction): value = -(random_transaction.value()) if (value < 0): with pytest.raises(TransactionError): t = Transaction(value, "this is a negative transaction") assert (t.is_null()) else: t = Transaction(value)
def random_transaction(): value = create_decimal(1000.0 * random.random()) description = "%s transaction" % value transaction = Transaction(value, description) assert (transaction.value() == value) assert (transaction.description() == description) return transaction
def test_split(random_large_value): value = random_large_value transactions = Transaction.split(value, "something") total = 0 for transaction in transactions: total += transaction.value() total = Transaction.round(total) assert (total == Transaction.round(value))
def random_transaction(account1, account2): value = create_decimal(1000.0 * random.random()) description = "%s transaction" % value transaction = Transaction(value, description) assert (transaction.value() == value) assert (transaction.description() == description) assert (account1.get_overdraft_limit() == account1_overdraft_limit) assert (account2.get_overdraft_limit() == account2_overdraft_limit) if random.randint(0, 1): return (transaction, account1, account2) else: return (transaction, account2, account1)
def from_data(data): """Return a DebitNote that has been extracted from the passed json-decoded dictionary Args: data (dict): Dictionary from which to create object Returns: DebitNote: Created from dictionary """ d = DebitNote() if (data and len(data) > 0): from Acquire.Accounting import Transaction as _Transaction from Acquire.Identity import Authorisation as _Authorisation from Acquire.ObjectStore import string_to_datetime \ as _string_to_datetime d._transaction = _Transaction.from_data(data["transaction"]) d._account_uid = data["account_uid"] d._authorisation = _Authorisation.from_data(data["authorisation"]) d._is_provisional = data["is_provisional"] d._datetime = _string_to_datetime(data["datetime"]) d._uid = data["uid"] if d._is_provisional: d._receipt_by = _string_to_datetime(data["receipt_by"]) return d
def test_transaction_saving(random_transaction): t = random_transaction data = t.to_data() t2 = Transaction.from_data(data) assert (t.value() == t2.value()) assert (t.description() == t2.description()) assert (t == t2)
def test_comparison(value1, value2): t1 = Transaction(value1, "lower") t2 = Transaction(value2, "higher") assert (t1 < t2) assert (t2 > t1) assert (t1 < value2) assert (t2 > value1) assert (t1 >= value1) assert (t1 <= value1) assert (t2 >= value2) assert (t2 <= value2) assert (t1 == value1) assert (t2 == value2) assert (t1 == t1) assert (t2 == t2) assert (t1 != t2) assert (t2 != t1) assert (t1 != value2) assert (t2 != value1)
def perform_transaction(key, result): delta1 = zero delta2 = zero auth = Authorisation() # need to work with thread-local copies of the accounts my_account1 = Account(uid=account1.uid()) my_account2 = Account(uid=account2.uid()) for i in range(0, 5): transaction = Transaction(value=create_decimal(random.random()), description="Transaction %d" % i) if random.randint(0, 1): Ledger.perform(transaction, my_account1, my_account2, auth) delta1 -= transaction.value() delta2 += transaction.value() else: Ledger.perform(transaction, my_account2, my_account1, auth) delta1 += transaction.value() delta2 -= transaction.value() with rlock: result[key] = (delta1, delta2)
def perform_transaction(key, result): delta1 = zero delta2 = zero # need to work with thread-local copies of the accounts my_account1 = Account(uid=account1.uid()) my_account2 = Account(uid=account2.uid()) for i in range(0, 10): transaction = Transaction(value=create_decimal(random.random()), description="Transaction %d" % i) if random.randint(0, 1): auth = Authorisation( resource=transaction.fingerprint(), testing_key=testing_key, testing_user_guid=my_account1.group_name()) Ledger.perform(transaction=transaction, debit_account=my_account1, credit_account=my_account2, authorisation=auth) delta1 -= transaction.value() delta2 += transaction.value() else: auth = Authorisation( resource=transaction.fingerprint(), testing_key=testing_key, testing_user_guid=my_account2.group_name()) Ledger.perform(transaction=transaction, debit_account=my_account2, credit_account=my_account1, authorisation=auth) delta1 += transaction.value() delta2 -= transaction.value() with rlock: result[key] = (delta1, delta2)
def test_transaction_is_null(): t = Transaction() assert (t.is_null())
def run(args): """This function is called to handle request to cash cheques. This will verify that the cheque is valid and will then create the debit/credit note pair for the transation. It will return the CreditNote to the caller so they can see that the funds have been reserved, and can receipt the transaction once goods/services have been delivered. Args: args (dict): information for payment for service Returns: dict: contains status, status message and credit note if valid """ credit_notes = [] try: cheque = args["cheque"] except: raise ValueError("You must supply a cheque to be cashed!") try: cheque = Cheque.from_data(cheque) except Exception as e: from Acquire.Service import exception_to_string raise TypeError("Unable to interpret the cheque.\n\nCAUSE: %s" % exception_to_string(e)) try: spend = args["spend"] except: spend = None if spend is not None: try: spend = string_to_decimal(spend) except Exception as e: from Acquire.Service import exception_to_string raise TypeError("Unable to interpret the spend.\n\nCause: %s" % exception_to_string(e)) try: resource = str(args["resource"]) except: raise ValueError( "You must supply a string representing the resource that will " "be paid for using this cheque") try: account_uid = str(args["account_uid"]) except: raise ValueError("You must supply the UID of the account to which the " "cheque will be cashed") try: receipt_by = args["receipt_by"] except: raise ValueError( "You must supply the datetime by which you promise to " "receipt this transaction") try: receipt_by = string_to_datetime(receipt_by) except Exception as e: from Acquire.Service import exception_to_string raise TypeError( "Unable to interpret the receipt_by date.\n\nCAUSE: %s" % exception_to_string(e)) # now read the cheque - this will only succeed if the cheque # is valid, has been signed, has been sent from the right # service, and was authorised by the user, the cheque # has not expired and we are the # service which holds the account from which funds are drawn info = cheque.read(resource=resource, spend=spend, receipt_by=receipt_by) try: description = str(args["description"]) except: description = info["resource"] authorisation = info["authorisation"] auth_resource = info["auth_resource"] user_guid = authorisation.user_guid() # the cheque is valid bucket = get_service_account_bucket() try: debit_account = Account(uid=info["account_uid"], bucket=bucket) except Exception as e: from Acquire.Service import exception_to_string raise PaymentError("Cannot find the account associated with the cheque" "\n\nCAUSE: %s" % exception_to_string(e)) try: credit_account = Account(uid=account_uid, bucket=bucket) except Exception as e: from Acquire.Service import exception_to_string raise PaymentError( "Cannot find the account to which funds will be creditted:" "\n\nCAUSE: %s" % exception_to_string(e)) # validate that this account is in a group that can be authorised # by the user (this should eventually go as the ACLs now allow users # to authorised payments from many accounts) accounts = Accounts(user_guid=user_guid) if not accounts.contains(account=debit_account, bucket=bucket): raise PermissionError( "The user with UID '%s' cannot authorise transactions from " "the account '%s' as they do not own this account." % (user_guid, str(debit_account))) transaction = Transaction(value=info["spend"], description=description) # we have enough information to perform the transaction # - this is provisional as the service must receipt everything transaction_records = Ledger.perform(transactions=transaction, debit_account=debit_account, credit_account=credit_account, authorisation=authorisation, authorisation_resource=auth_resource, is_provisional=True, receipt_by=receipt_by, bucket=bucket) # extract all of the credit notes to return to the user, # and also to record so that we can check if they have not # been receipted in time... credit_notes = [] for record in transaction_records: credit_notes.append(record.credit_note()) credit_notes = list_to_string(credit_notes) receipt_key = "accounting/cashed_cheque/%s" % info["uid"] mutex = Mutex(receipt_key, bucket=bucket) try: receipted = ObjectStore.get_object_from_json(bucket, receipt_key) except: receipted = None if receipted is not None: # we have tried to cash this cheque twice! mutex.unlock() Ledger.refund(transaction_records, bucket=bucket) else: info = {"status": "needs_receipt", "creditnotes": credit_notes} ObjectStore.set_object_from_json(bucket, receipt_key, info) mutex.unlock() return {"credit_notes": credit_notes}
def test_temporal_transactions(account1, account2, bucket): if not have_freezetime: return zero = create_decimal(0) balance1 = zero balance2 = zero final_balance1 = zero final_balance2 = zero liability1 = zero liability2 = zero receivable1 = zero receivable2 = zero # generate some random times for the transactions random_dates = [] now = get_datetime_now() for i in range(0, 50): if i == 0: # this is an evil edge-case datetime s = "2019-01-20 20:59:59.092627+00:00" r = datetime.datetime.fromisoformat(s) else: r = start_time + random.random() * (now - start_time) while (r.minute == 59 and r.second >= 58) or \ (r.minute == 0 and r.second == 0 and r.microsecond < 10): r = r + datetime.timedelta(seconds=1) random_dates.append(r) random_dates.sort() # (which must be applied in time order!) random_dates.sort() provisionals = [] for (i, transaction_time) in enumerate(random_dates): with freeze_time(transaction_time) as _frozen_datetime: now = get_datetime_now() assert (transaction_time == now) is_provisional = (random.randint(0, 3) <= 2) # check search for transaction is not O(n^2) lookup scanning # through the keys... transaction = Transaction(25 * random.random(), "test transaction %d" % i) if random.randint(0, 1): debit_account = account1 credit_account = account2 if is_provisional: liability1 += transaction.value() receivable2 += transaction.value() else: balance1 -= transaction.value() balance2 += transaction.value() final_balance1 -= transaction.value() final_balance2 += transaction.value() else: debit_account = account2 credit_account = account1 if is_provisional: receivable1 += transaction.value() liability2 += transaction.value() else: balance1 += transaction.value() balance2 -= transaction.value() final_balance1 += transaction.value() final_balance2 -= transaction.value() auth = Authorisation(resource=transaction.fingerprint(), testing_key=testing_key, testing_user_guid=debit_account.group_name()) records = Ledger.perform(transaction=transaction, debit_account=debit_account, credit_account=credit_account, authorisation=auth, is_provisional=is_provisional, bucket=bucket) for record in records: assert (record.datetime() == now) if is_provisional: for record in records: provisionals.append((credit_account, record)) elif (random.randint(0, 3) <= 2): # receipt pending transactions balance1 = Balance(balance=balance1, liability=liability1, receivable=receivable1) balance2 = Balance(balance=balance2, liability=liability2, receivable=receivable2) assert (account1.balance() == balance1) assert (account2.balance() == balance2) for (credit_account, record) in provisionals: credit_note = record.credit_note() auth = Authorisation( resource=credit_note.fingerprint(), testing_key=testing_key, testing_user_guid=credit_account.group_name()) receipted_value = create_decimal( random.random() * float(credit_note.value())) delta_value = credit_note.value() - receipted_value Ledger.receipt(Receipt(credit_note=credit_note, receipted_value=receipted_value, authorisation=auth), bucket=bucket) if credit_note.debit_account_uid() == account1.uid(): final_balance1 += delta_value final_balance2 -= delta_value else: final_balance2 += delta_value final_balance1 -= delta_value assert (account1.balance() == Balance(balance=final_balance1)) assert (account2.balance() == Balance(balance=final_balance2)) provisionals = [] balance1 = final_balance1 balance2 = final_balance2 liability1 = zero liability2 = zero receivable1 = zero receivable2 = zero balance1 = Balance(balance=balance1, liability=liability1, receivable=receivable1) balance2 = Balance(balance=balance2, liability=liability2, receivable=receivable2) assert (account1.balance() == balance1) assert (account2.balance() == balance2) for (credit_account, record) in provisionals: credit_note = record.credit_note() auth = Authorisation(resource=record.credit_note().fingerprint(), testing_key=testing_key, testing_user_guid=credit_account.group_name()) receipted_value = create_decimal(random.random() * float(credit_note.value())) delta_value = credit_note.value() - receipted_value Ledger.receipt(Receipt(credit_note=credit_note, authorisation=auth, receipted_value=receipted_value), bucket=bucket) if credit_note.debit_account_uid() == account1.uid(): final_balance1 += delta_value final_balance2 -= delta_value else: final_balance2 += delta_value final_balance1 -= delta_value assert (account1.balance() == Balance(balance=final_balance1)) assert (account2.balance() == Balance(balance=final_balance2))
def run(args): """This function is called to handle requests from a user to deposit more funds into their account. This will add this deposit as a debt for the user. Once the debt exceeds a certain value, then the backend-payment system will charge the user's real account to recover the funds Args: args (dict): data for deposit of funds into the account Returns: dict: contains status, status message and details regarding the deposit into the account and invoice data """ transaction_records = None invoice_value = None invoice_user = None try: authorisation = Authorisation.from_data(args["authorisation"]) except: authorisation = None transaction = Transaction.from_data(args["transaction"]) if authorisation is None: raise PermissionError("You must supply a valid authorisation " "to deposit funds into your account") if transaction is None or transaction.is_null(): raise ValueError("You must supply a valid transaction that " "represents the deposit") try: account_name = str(args["account_name"]) except: account_name = "deposits" if transaction.value() > 0: user_guid = authorisation.user_guid() # load the account from which the transaction will be performed bucket = get_service_account_bucket() accounts = Accounts(user_guid=user_guid) # deposits are made by transferring funds from the user's # 'billing' account to their named account (or 'deposits' account) deposit_account = accounts.create_account(account_name, "Deposit account", bucket=bucket) billing_account = accounts.create_account("billing", "Billing account", overdraft_limit=150, bucket=bucket) billing_balance = billing_account.balance() - transaction.value() if billing_balance.balance() < -50.0: # there are sufficient funds that need to be transferred that # it is worth really charging the user invoice_user = user_guid invoice_value = billing_balance # we have enough information to perform the transaction transaction_records = Ledger.perform(transactions=transaction, debit_account=billing_account, credit_account=deposit_account, authorisation=authorisation, is_provisional=False, bucket=bucket) return_value = {} if transaction_records: try: transaction_records[0] except: transaction_records = [transaction_records] for i in range(0, len(transaction_records)): transaction_records[i] = transaction_records[i].to_data() return_value["transaction_records"] = transaction_records if invoice_user: return_value["invoice_user"] = invoice_user return_value["invoice_value"] = str(invoice_value) return return_value
def test_temporal_transactions(account1, account2, bucket): if not have_freezetime: return zero = create_decimal(0) balance1 = zero balance2 = zero final_balance1 = zero final_balance2 = zero liability1 = zero liability2 = zero receivable1 = zero receivable2 = zero # generate some random times for the transactions random_dates = [] now = datetime.datetime.now() for i in range(0, 100): random_dates.append(start_time + random.random() * (now - start_time)) # (which must be applied in time order!) random_dates.sort() records = [] for (i, transaction_time) in enumerate(random_dates): with freeze_time(transaction_time) as frozen_datetime: now = datetime.datetime.now() assert (frozen_datetime() == now) is_provisional = random.randint(0, 5) transaction = Transaction(25 * random.random(), "test transaction %d" % i) auth = Authorisation() if random.randint(0, 10): record = Ledger.perform(transaction, account1, account2, auth, is_provisional, bucket=bucket) if is_provisional: liability1 += transaction.value() receivable2 += transaction.value() else: balance1 -= transaction.value() balance2 += transaction.value() final_balance1 -= transaction.value() final_balance2 += transaction.value() else: record = Ledger.perform(transaction, account2, account1, auth, is_provisional, bucket=bucket) if is_provisional: receivable1 += transaction.value() liability2 += transaction.value() else: balance1 += transaction.value() balance2 -= transaction.value() final_balance1 += transaction.value() final_balance2 -= transaction.value() if is_provisional: records.append(record) assert (record.timestamp() == now.timestamp()) assert (account1.balance() == balance1) assert (account2.balance() == balance2) assert (account1.liability() == liability1) assert (account1.receivable() == receivable1) assert (account2.liability() == liability2) assert (account2.receivable() == receivable2) for record in records: Ledger.receipt(Receipt(record.credit_note(), Authorisation()), bucket=bucket) assert (account1.balance() == final_balance1) assert (account2.balance() == final_balance2) assert (account1.liability() == zero) assert (account1.receivable() == zero) assert (account2.liability() == zero) assert (account2.receivable() == zero)
def run(args): """This function is called to handle requests to perform transactions between accounts Args: args (dict): data for account transfers Returns: dict: contains status, status message and transaction records if any are available """ transaction_records = None try: debit_account_uid = str(args["debit_account_uid"]) except: debit_account_uid = None try: credit_account_uid = str(args["credit_account_uid"]) except: credit_account_uid = None try: authorisation = Authorisation.from_data(args["authorisation"]) except: authorisation = None try: transaction = Transaction.from_data(args["transaction"]) except: transaction = None try: is_provisional = bool(args["is_provisional"]) except: is_provisional = None if debit_account_uid is None: raise TransactionError("You must supply the account UID " "for the debit account") if credit_account_uid is None: raise TransactionError("You must supply the account UID " "for the credit account") if debit_account_uid == credit_account_uid: raise TransactionError( "You cannot perform a transaction where the debit and credit " "accounts are the same!") if transaction is None or transaction.is_null(): raise TransactionError("You must supply a valid transaction to " "perform!") if is_provisional is None: raise TransactionError("You must say whether or not the " "transaction is provisional using " "is_provisional") if authorisation is None: raise PermissionError("You must supply a valid authorisation " "to perform transactions between accounts") authorisation.assert_once() user_guid = authorisation.user_guid() # load the account from which the transaction will be performed bucket = get_service_account_bucket() debit_account = Account(uid=debit_account_uid, bucket=bucket) # validate that this account is in a group that can be authorised # by the user - This should eventually go as this is all # handled by the ACLs if not Accounts(user_guid).contains(account=debit_account, bucket=bucket): raise PermissionError( "The user with GUID '%s' cannot authorise transactions from " "the account '%s' as they do not own this account." % (user_guid, str(debit_account))) # now load the two accounts involved in the transaction credit_account = Account(uid=credit_account_uid, bucket=bucket) # we have enough information to perform the transaction transaction_records = Ledger.perform(transactions=transaction, debit_account=debit_account, credit_account=credit_account, authorisation=authorisation, is_provisional=is_provisional, bucket=bucket) return_value = {} if transaction_records: try: transaction_records[0] except: transaction_records = [transaction_records] for i in range(0, len(transaction_records)): transaction_records[i] = transaction_records[i].to_data() return_value["transaction_records"] = transaction_records return return_value