diff --git a/features/account.feature b/features/account.feature new file mode 100644 index 0000000..9324a74 --- /dev/null +++ b/features/account.feature @@ -0,0 +1,10 @@ +Feature: Account + + Scenario: Calculate account balance + Given an account + When the following transactions are applied + | amount | + | $1423.32 | + | -$51.31 | + | -$100.00 | + Then the balance is $1272.01 diff --git a/features/currency.feature b/features/currency.feature index e6e8ef9..27e1411 100644 --- a/features/currency.feature +++ b/features/currency.feature @@ -6,22 +6,32 @@ Feature: Represent real currency When the currency is stringified Then it matches - Examples: USD - | denomination | value | pattern | - | USD-cents | 23 | $0.23 | - | USD-cents | 100 | $1.00 | - | USD-cents | 171 | $1.71 | - | USD-cents | 1071 | $10.71 | - | USD-cents | 10071 | $100.71 | - | USD-cents | 110071 | $1,100.71 | - | USD-cents | 500110071 | $5,001,100.71 | + Examples: Positive values USD + | denomination | value | pattern | + | USD-cents | 23 | $0.23 | + | USD-cents | 100 | $1.00 | + | USD-cents | 171 | $1.71 | + | USD-cents | 1071 | $10.71 | + | USD-cents | 10071 | $100.71 | + | USD-cents | 110071 | $1,100.71 | + | USD-cents | 500110071 | $5,001,100.71 | + + Examples: Negative values USD + | denomination | value | pattern | + | USD-cents | -500110071 | -$5,001,100.71 | + | USD-cents | -110071 | -$1,100.71 | + | USD-cents | -10071 | -$100.71 | + | USD-cents | -1071 | -$10.71 | + | USD-cents | -171 | -$1.71 | + | USD-cents | -100 | -$1.00 | + | USD-cents | -23 | -$0.23 | Scenario Outline: Parse text to currency Given currency text representation When currency text representation is parsed Then the resulting currency object has value - Examples: USD + Examples: Positive values USD | repr | value | | $1.00 | 100 | | $1.0 | 100 | @@ -33,3 +43,16 @@ Feature: Represent real currency | $100 | 10000 | | $1000 | 100000 | | $1,000 | 100000 | + + Examples: Negative values USD + | repr | value | + | -$1.00 | -100 | + | -$1.0 | -100 | + | -$1. | -100 | + | -$1 | -100 | + | -$.5 | -50 | + | -$0.5 | -50 | + | -$0.50 | -50 | + | -$100 | -10000 | + | -$1000 | -100000 | + | -$1,000 | -100000 | diff --git a/features/steps/account.py b/features/steps/account.py new file mode 100644 index 0000000..42c8eb4 --- /dev/null +++ b/features/steps/account.py @@ -0,0 +1,27 @@ +from behave import given, when, then +from ddd_sample.domain.account import Account, Transaction +from ddd_sample.domain.currency import USD, Currency + +import datetime + + +@given("an account") +def _(context): + context.account = Account(format=USD) + + +@when("the following transactions are applied") +def _(context): + for row in context.table: + value = USD.toValue(row["amount"]) + txn = Transaction( + date=datetime.date.today(), + amount=Currency(value=value, format=USD) + ) + context.account.post_transaction(txn) + + +@then("the balance is $1272.01") +def _(context): + exp = Currency(value=127201, format=USD) + assert exp == context.account.balance diff --git a/src/ddd_sample/domain/account.py b/src/ddd_sample/domain/account.py new file mode 100644 index 0000000..1c76ad1 --- /dev/null +++ b/src/ddd_sample/domain/account.py @@ -0,0 +1,19 @@ +from .currency import Currency, CurrencyFormatMixin + +from datetime import date as Date + + +class Transaction: + def __init__(self, date: Date, amount: Currency): + self.date = date + self.amount = amount + + +class Account: + def __init__(self, format: CurrencyFormatMixin): + self.transactions: list[Transaction] = [] + self.balance = Currency(value=0, format=format) + + def post_transaction(self, transaction: Transaction): + self.transactions.append(transaction) + self.balance += transaction.amount diff --git a/src/ddd_sample/domain/currency.py b/src/ddd_sample/domain/currency.py index 95e5071..19b3549 100644 --- a/src/ddd_sample/domain/currency.py +++ b/src/ddd_sample/domain/currency.py @@ -1,5 +1,6 @@ import re from abc import ABC, abstractclassmethod +from typing import Self class CurrencyFormatMixin(ABC): @@ -17,21 +18,24 @@ class USD(CurrencyFormatMixin): def toValue(cls, repr: str) -> int: repr = repr.strip() repr = repr.replace(",", "") - groups = re.search(r"^\$(\d*)\.?(\d?\d?)$", repr).groups() + groups = re.search(r"^(-?)\$(\d*)\.?(\d?\d?)$", repr).groups() - dollars_str = groups[0] + sign_modifier = -1 if groups[0] and groups[0] == "-" else 1 + + dollars_str = groups[1] dollars = int(dollars_str) if dollars_str else 0 - cents_str = groups[1] + cents_str = groups[2] if cents_str and 1 == len(cents_str): cents_str += '0' cents = int(cents_str) if cents_str else 0 - return dollars * 100 + cents + return (dollars * 100 + cents) * sign_modifier @abstractclassmethod def toString(cls, value: int) -> str: - return "${:,}.{:02d}".format(value // 100, value % 100) + sign = "-" if value < 0 else "" + return "{}${:,}.{:02d}".format(sign, abs(value) // 100, abs(value) % 100) class Currency: @@ -42,3 +46,23 @@ def __init__(self, value: int, format: CurrencyFormatMixin): def __str__(self) -> str: return self.format.toString(self.value) + + def __add__(self, other) -> Self: + if isinstance(other, Currency): + new_value = self.value + other.value + elif isinstance(other, int): + new_value = self.value + other + return Currency(value=new_value, format=self.format) + + def __iadd__(self, other): + if isinstance(other, Currency): + self.value = self.value + other.value + elif isinstance(other, int): + self.value = self.value + other + return self + + def __eq__(self, other): + if not isinstance(other, Currency): + return False + + return other.value == self.value