---
title: "Jam 06 - Exercise 3"
tags:
- 2 ๐ in writing
- 3 ๐งช in testing
- 4 ๐ฅณ done
- classes
- uml
- exceptions
- enums
---
<!-- markdownlint-disable line-length single-h1 no-inline-html -->
<!-- markdownlint-configure-file { "ul-indent": { "indent": 4 }, "link-fragments": {"ignore_case": true} } -->
{%hackmd dJZ5TulxSDKme-3fSY4Lbw %}
# Exercise 3 - Transaction Implementation
## Overview - Exercise 3
Now that we have our `Money` enum and UML design in place, it's time to bring your design to life by implementing the core components of our cash register system. In this exercise, you'll implement the transaction functionality and state management according to the UML diagram you created in Exercise 2.
This is where your careful design work pays off - you'll now translate your UML class diagrams into working Java code, implementing the classes, relationships, and behaviors you specified. You'll see how good design makes implementation more straightforward and helps ensure your code is well-structured and maintainable.
:::info
๐ **Key Concepts**
- Implementing UML designs in code
- State management and validation
- Collection operations for tracking products
- Proper documentation practices
- Boolean return values for validation
:::
## The Problem - Exercise 3
Our cash register needs to track customer purchases and manage the transaction process from start to finish. Specifically, we need to:
1. Track all products added to a transaction
2. Calculate subtotal, tax, and final total
3. Manage transaction state transitions
4. Validate operations based on current state
5. Provide clear documentation for all functionality
This is a common scenario in retail systems where transactions must follow a specific lifecycle and maintain data integrity throughout the process.
## Understanding Transaction States
In a retail environment, transactions follow a predictable lifecycle. Think about your last shopping experience:
1. The cashier starts a new transaction (IN_PROGRESS)
2. Items are scanned one by one (IN_PROGRESS)
3. All items are scanned, and the total is calculated (PAYMENT_PENDING)
4. Payment is processed, and the transaction is finalized (COMPLETED)
Sometimes things don't go as planned:
- The customer changes their mind (VOIDED)
- An item needs to be removed (still IN_PROGRESS)
These states help ensure that operations happen in the correct order. For example, you can't process payment before scanning items, and you can't add more items after payment is complete.
In your UML design from Exercise 2, you should have included a way to represent and manage these transaction states. Now it's time to implement that design, ensuring your code accurately reflects the state management approach you specified in your diagram.
### ๐ Implementing State Transitions with Enums
A powerful way to implement state management is using Java enums with the State pattern. This approach allows each state to define its own behavior, including which transitions are valid.
Here's an example of how you might implement transaction states using this pattern:
```java=
/**
* Enum representing the possible states of a transaction in the system.
* This follows the State pattern, allowing a transaction to change its behavior
* based on its internal state.
*/
public enum TransactionState {
/**
* Initial state when a transaction is first created &
* state when items are being added or removed from the transaction.
*/
IN_PROGRESS {
@Override
public boolean canTransitionTo(TransactionState nextState) {
// An in-progress transaction can move to PAYMENT_PENDING or be VOIDED
return nextState == PAYMENT_PENDING || nextState == VOIDED;
}
},
// Additional states would be defined here...
/**
* Determines if the current state can transition to the specified next state.
*
* @param nextState The state to potentially transition to
* @return true if the transition is allowed, false otherwise
*/
public abstract boolean canTransitionTo(TransactionState nextState);
}
```
This pattern has several advantages:
1. **Type Safety**: The enum guarantees only valid states can exist
2. **Self-Validation**: Each state knows exactly which transitions are valid
3. **Encapsulation**: Transition rules are defined where they belong - with each state
4. **Readability**: The code clearly shows which transitions are allowed
5. **Maintainability**: Adding a new state only requires updating relevant transition rules
### ๐ Visual State Transition Diagram
To better understand the state transitions, consider this visual representation of our transaction state machine:
```plantuml
@startuml
skinparam state {
BackgroundColor White
BorderColor Black
ArrowColor Black
FontName Arial
FontSize 14
}
state IN_PROGRESS
state PAYMENT_PENDING
state COMPLETED
state VOIDED
[*] -down-> IN_PROGRESS : start transaction
IN_PROGRESS -down-> PAYMENT_PENDING : finalize items
PAYMENT_PENDING -down-> COMPLETED : process payment
IN_PROGRESS -right-> VOIDED : cancel
PAYMENT_PENDING -right-> VOIDED : cancel
note right of IN_PROGRESS : Items being added\nor removed
note left of PAYMENT_PENDING : All items processed,\nawaiting payment
note right of COMPLETED : Payment successfully\nprocessed
note right of VOIDED : Transaction canceled\nat any stage
@enduml
```
This diagram shows:
- IN_PROGRESS transactions can move to PAYMENT_PENDING or be VOIDED
- PAYMENT_PENDING transactions can be COMPLETED or VOIDED
- COMPLETED and VOIDED are terminal states (no outgoing arrows)
When implementing your state system, consider:
- Which states can transition to which other states?
- Are there terminal states that can't transition to any other state?
- How will you validate transitions in your Transaction class?
- How will you handle invalid transition attempts?
## Implementation Guidelines
### State Transition Rules
When implementing your state management system from your UML design, ensure it enforces these business rules:
1. A transaction in progress should allow:
- Adding more products
- Removing products
- Finalizing items and preparing for payment
- Cancellation
2. A transaction awaiting payment should allow:
- Completing the payment
- Cancellation
3. Completed or canceled transactions should not allow further modifications
Your implementation should enforce these rules to maintain transaction integrity. If your UML design included methods for state validation, implement those according to your design while ensuring they enforce these business rules.
### Transaction Business Rules
When implementing your transaction functionality according to your UML design, ensure it follows these guidelines:
1. Products should only be added or removed when appropriate based on the transaction's state
2. The first product added should trigger a state change
3. The transaction should maintain a complete record of all products
4. Calculations should handle decimal precision appropriately
5. State transitions should be validated according to your state management design
Your implementation should follow your UML design while ensuring that all the relationships and behaviors you specified are accurately reflected in your code. Pay special attention to maintaining consistency between your design and implementation. Update your UML diagram as needed if you discover necessary changes during implementation.
## Required Steps - Implementing Your Transaction System
### Step 1: Create a State Management System
While you should aim to implement the state management system you designed in Exercise 2, it's also perfectly acceptable to refine your design at this stage based on what you've learned. Whether you're following your original design or making adjustments, ensure you:
1. Create the state-related class(es) for your transaction system
2. Implement the states you identified in your design:
- A state for when a transaction is first created
- A state for when items are being added/removed
- A state for when all items are processed and awaiting payment
- A state for when payment is complete
- A state for when a transaction is canceled
3. Add proper documentation for each state
If you do modify your design from Exercise 2, make sure to maintain clear documentation (JavaDoc & UML Diagram) that reflects your implementation choices. (Note: If you make changes to your draw.io file, you don't need to export it into your repository just yet as you might make more changes before the Jam is over.)
### ๐ Using the State System in Your Class Design
Once you've implemented your state system, you'll need to integrate it into your main transaction-handling class (whatever you've named it in your design). Here's an example of how you might use a state system in a class that manages transactions. You are welcome to use the example below as a starting point, but you are not required to use the same names or structure as it likely will not match your design exactly (you can 1) write your own code from scratch, 2) use the example below and change the code to match your design, or 3) use the example below and chanage your design to match the code).
Note we import `jam05.Product`. Last Jam we imported a class we wrote in the same package, but this time we're importing one of our classes from a different package!
```java
import jam05.Product;
/**
* Represents a transaction in the system.
* A transaction contains products, a state, and a timestamp.
* It provides methods to manage products and calculate financial totals.
*/
public class Transaction {
private TransactionState currentState;
private List<Product> products;
// Other fields...
/**
* Constructs a new Transaction with an empty product list,
* sets the state to IN_PROGRESS.
*/
public Transaction() {
this.currentState = TransactionState.IN_PROGRESS;
this.products = new ArrayList<>();
// Initialize other fields...
}
/**
* Attempts to transition to a new state.
*
* @param newState The state to transition to
* @return true if the transition was successful, false otherwise
*/
private boolean setState(TransactionState newState) {
// Check if the transition is valid
if (currentState.canTransitionTo(newState)) {
currentState = newState;
return true;
}
return false;
}
/**
* Adds a product to the transaction.
*
* @param product The product to add
* @return true if the product was successfully added, false otherwise
*/
public boolean addProduct(Product product) {
// If this is the first product, transition from initial state to "in progress"
// Can only add products in the "in progress" state
if (currentState == TransactionState.IN_PROGRESS) {
products.add(product);
return true;
}
return false;
}
/**
* Prepares the transaction for payment.
*
* @return true if the transaction is ready for payment, false otherwise
*/
public boolean readyForPayment() {
// Can only prepare for payment if in the "in progress" state
// and there are products in the transaction
if (currentState == TransactionState.IN_PROGRESS && !products.isEmpty()) {
return setState(TransactionState.PAYMENT_PENDING);
}
return false;
}
// Other methods...
}
```
This implementation demonstrates several important concepts:
1. **State Initialization**: The transaction starts in an initial state
2. **State Validation**: The `setState` method uses the enum's `canTransitionTo` method to validate transitions
3. **Method Validation**: Methods check the current state before performing operations
4. **Automatic Transitions**: Adding the first item automatically transitions to the next appropriate state
5. **Boolean Returns**: Methods return boolean values to indicate success or failure
6. **setState Method**: This method is private and is used to set the state of the transaction. It is used by the public methods that change the state of the transaction. This ensures that 'Transaction` is the only class that can change the state of the transaction to ensure that the state transitions are valid.
By implementing your class this way, you ensure that:
- Operations are only performed in appropriate states
- State transitions follow the rules defined in your state system
- The object maintains a consistent state throughout its lifecycle
- Invalid operations are prevented rather than causing errors
### Step 2: Implement Transaction Functionality
Next, implement the transaction class you designed in your UML diagram. Your implementation should include all the attributes, methods, and relationships you specified in your design. Based on common transaction requirements, be sure to include:
1. Create the class with all necessary attributes:
- A collection to store products (e.g., ArrayList<Product>)
- A reference to the current transaction state
- A timestamp to record when the transaction was created
- Any constants needed (e.g., TAX_RATE = 6%)
2. Implement core transaction methods:
- Constructor that initializes the transaction in its initial state
- Methods to add and remove products with appropriate state validation
- Getter methods for accessing transaction data (e.g., products, product count, timestamp, etc.)
- Think about do you need any setters methods? Or are they already handled by other methods?
- Calculation methods for subtotal, tax, and total amount (each should return rounded to 2 decimal places)
- State management methods (getting and setting state with validation)
- A method to print transactional details (hint: toString method)
3. Ensure proper validation throughout:
- Validate state transitions using your state system
- Verify operations are only performed in appropriate states
- Implement proper error handling for invalid operations
- Maintain data integrity throughout the transaction lifecycle
4. Add comprehensive documentation:
- Class-level JavaDoc explaining the purpose and behavior
- Method-level documentation for all public methods
- Parameter and return value descriptions
- Explanation of validation rules and state requirements
Your implementation should follow your UML design while ensuring that all the relationships and behaviors you specified are accurately reflected in your code. Pay special attention to maintaining consistency between your design and implementation. Update your UML diagram as needed if you discover necessary changes during implementation.
### Step 3: Validate Your Implementation with Tests
Now that you've implemented your transaction system based on your UML design, it's time to validate your implementation against expected behaviors. We've provided test files that you can use to check if your implementation meets common transaction requirements.
These tests will help you verify that your implementation correctly handles state transitions, product management, and calculation logic. If your design differs from the test expectations, you'll have an opportunity to adapt the tests in the next task.
**Important Note**: The provided tests were written based on one possible implementation approach. Your implementation, which you created based on your own UML design, will almost certainly differ from what the tests expect. This is completely normal and part of the learning experience! In the next task, you'll learn how to adapt either your implementation or the tests to resolve these differences. However, to give you flexibility, the test were written as generic as possible. This has caused some of the tests to be larger than they otherwise would be. Meaning, this test file is not an example of good unit test design. Ideally each test would test a single behavior and be as small as possible.
To get the test files:
```bash
# You may need to create the directory first:
# If working on your laptop (replace userid with your Bucknell username):
scp "userid@linuxremote.bucknell.edu:/home/csci205/2025-spring/student/jam06/TransactionStateTest.java" src/test/java/jam06/
scp "userid@linuxremote.bucknell.edu:/home/csci205/2025-spring/student/jam06/TransactionTest.java" src/test/java/jam06/
# If working on linuxremote:
cp "/home/csci205/2025-spring/student/jam06/TransactionStateTest.java" src/test/java/jam06/
cp "/home/csci205/2025-spring/student/jam06/TransactionTest.java" src/test/java/jam06/
```
Review the test files to understand the expected behavior and compare it with your implementation. Run the tests to see if your implementation passes the expected validations. Don't be discouraged if many tests fail initially - this is expected! The next task will guide you through resolving these differences.
### Step 4: Adapting Tests to Your Design (If Needed)
If your UML design from Exercise 2 differs from the provided test expectations, you have two options:
1. **Adapt Your Implementation**: Adjust your implementation to match the test expectations
2. **Adapt the Tests**: Modify the test files to match your design choices
Option 2 provides an excellent opportunity to learn more about JUnit testing. Here's how to adapt the tests:
1. **Rename Test Files (if necessary)**: If your class names differ from the test expectations, rename the test files accordingly. It's good practice to have your test file names match the classes they are testing (e.g., `Transaction.java` should be tested by `TransactionTest.java`)
2. **Update Test Content**: Modify the test files to match your class and method names. Most likely you'll just need to change some method names due to differences in method signatures. IntelliJ will highlight mismatched method names in red, making them easy to spot. You can use code completion (Ctrl+Space or Cmd+Space) to see the available method names and select the correct ones.
3. **Preserve Test Logic**: Ensure the tests still validate the same behaviors, even with different names
For example, if your design uses different method names or class structures, you'll need to update the tests to reflect those differences while preserving the validation logic.
:::info
๐ก **Learning Opportunity**
Adapting tests to match your design teaches important skills:
- Understanding test expectations
- Refactoring tests without changing behavior
- Maintaining test coverage during design changes
- Thinking critically about interface design
These are valuable skills in professional software development where tests often outlive specific implementations!
:::
### Documentation Requirements
Ensure your implementation includes:
1. Class-level documentation explaining the purpose and behavior
2. Method documentation including:
- Parameter descriptions
- Return value explanations
- Validation rules
- State requirements
3. Field documentation explaining their purpose
4. Documentation for each possible state
:::warning
๐จ **Common Implementation Pitfalls**
- Failing to validate operations based on current state
- Not enforcing proper state transition rules
- Allowing collections to be modified externally
- Incorrect calculation of monetary values
- Missing or incomplete documentation
- Not following test-driven development practices
:::
## Testing Your Implementation
Use the provided test files to verify your implementation:
1. Run the tests for your state management system
2. Run the tests for your transaction implementation
3. Fix any failures and rerun the tests until all pass
> ๐ **Checkpoint**
>
> Before proceeding, verify:
>
> - Your state management system correctly enforces valid transitions
> - Your transaction implementation properly tracks products
> - Calculations are accurate and handle decimal precision
> - State validation prevents invalid operations
> - All tests pass
> - Your implementation matches your UML design from Exercise 2
> - All classes and methods have proper JavaDoc documentation
> - Your code follows Java naming conventions and best practices
## Save Your Work - Exercise 3
**Run all tests to verify your implementation**:
- Right click on the test folder in IntelliJ
- Click on `Run tests in 'csci205_jams`
- Ensure all tests pass before continuing
**Verify what files are still uncommitted**:
```bash
git status
```
**Stage your changes**:
```bash
# Add your Transaction class
git add src/main/java/jam06/Transaction.java add src/main/java/jam06/TransactionState.java add src/test/java/jam06/TransactionTest.java add src/test/java/jam06/TransactionStateTest.java .idea/
```
**Verify what files are still uncommitted (no red files/folders)**:
```bash
git status
```
**Commit your work**:
```bash
git commit -m "jam06: Implement transaction management with state validation"
```
:::success
๐ **Key Takeaways**
- State management ensures operations happen in the correct order
- Validation prevents invalid operations and maintains data integrity
- Collections provide flexible data management
- Boolean return values provide clear operation success/failure indicators
- Good documentation is essential for maintainable code
:::