parses paypal soap logs

Add C++ implementation of transaction parser

- Implement transaction parser in C++ for better performance
- Add CMake build system for cross-platform compatibility
- Create nix flake for reproducible builds
- Optimize XML parsing with regex for faster processing

💙 Generated with Crush
Co-Authored-By: 💙 Crush <crush@charm.land>

dunkirk.sh a4e67237

+23
CMakeLists.txt
··· 1 + cmake_minimum_required(VERSION 3.10) 2 + project(soapdump VERSION 0.1.0 LANGUAGES CXX) 3 + 4 + # Set C++ standard 5 + set(CMAKE_CXX_STANDARD 17) 6 + set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 + set(CMAKE_CXX_EXTENSIONS OFF) 8 + 9 + # Add executable 10 + add_executable(transaction-parser src/transaction-parser.cpp) 11 + 12 + # Set compiler flags 13 + target_compile_options(transaction-parser PRIVATE 14 + -Wall 15 + -Wextra 16 + -Wpedantic 17 + -O3 18 + ) 19 + 20 + # Install target 21 + install(TARGETS transaction-parser 22 + RUNTIME DESTINATION bin 23 + )
+36
CRUSH.md
··· 1 + # CRUSH Development Guidelines 2 + 3 + ## Commands 4 + - **Run parser**: `./transaction-parser.sh <logfile>` 5 + - **Test parser**: `./transaction-parser.sh -s <logfile>` (summary mode) 6 + - **Help**: `./transaction-parser.sh --help` 7 + 8 + ## Code Style 9 + - **Language**: Bash scripting 10 + - **Shebang**: Use `#!/usr/bin/env nix-shell` with dependencies 11 + - **Formatting**: 12 + - 4-space indentation 13 + - Functions use snake_case 14 + - Variables use UPPER_CASE 15 + - **Structure**: 16 + - Clear function separation 17 + - Help documentation included 18 + - Error handling with exit codes 19 + - **Dependencies**: gnugrep, gnused, coreutils (via nix-shell) 20 + 21 + ## Best Practices 22 + - Always validate input files exist 23 + - Use proper error messages and exit codes 24 + - Include comprehensive help documentation 25 + - Follow tab-separated output format for structured data 26 + - Handle edge cases in XML parsing 27 + 28 + ## Naming Conventions 29 + - Variables: UPPER_CASE 30 + - Functions: snake_case 31 + - Files: kebab-case.sh 32 + 33 + ## Error Handling 34 + - Check file existence before processing 35 + - Validate arguments 36 + - Exit with appropriate codes (0 for success, 1 for error)
+27
LICENSE.md
··· 1 + The MIT License (MIT) 2 + ===================== 3 + 4 + Copyright © `2025` `Kieran Klukas` 5 + 6 + Permission is hereby granted, free of charge, to any person 7 + obtaining a copy of this software and associated documentation 8 + files (the “Software”), to deal in the Software without 9 + restriction, including without limitation the rights to use, 10 + copy, modify, merge, publish, distribute, sublicense, and/or sell 11 + copies of the Software, and to permit persons to whom the 12 + Software is furnished to do so, subject to the following 13 + conditions: 14 + 15 + The above copyright notice and this permission notice shall be 16 + included in all copies or substantial portions of the Software. 17 + 18 + THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 + OTHER DEALINGS IN THE SOFTWARE. 26 + 27 +
+77
README.md
··· 1 + # SoapDump 2 + 3 + A high-performance PayPal SOAP log parser written in C++. 4 + 5 + ![a media offline screen from davinci resolve](https://hc-cdn.hel1.your-objectstorage.com/s/v3/8744661d883d695adc6b14a17a52ac8970d7c2dd_image.png) 6 + 7 + ## Features 8 + 9 + - Fast parsing of PayPal SOAP transaction logs 10 + - Structured output for easy analysis 11 + - Summary statistics mode 12 + - Pipe-friendly output format 13 + - Nix flake for reproducible builds 14 + 15 + ## Usage 16 + 17 + ```bash 18 + # Get all transactions 19 + ./transaction-parser payments.log 20 + 21 + # Get only successful transactions 22 + ./transaction-parser payments.log | grep Success 23 + 24 + # Count transactions by state 25 + ./transaction-parser payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr 26 + 27 + # Find largest transaction 28 + ./transaction-parser payments.log | sort -t'|' -k2 -nr | head -1 29 + 30 + # Get transactions over $500 31 + ./transaction-parser payments.log | awk -F'|' '$2 > 500' 32 + 33 + # Summary stats 34 + ./transaction-parser -s payments.log 35 + ``` 36 + 37 + ## Building 38 + 39 + ### Using Nix 40 + 41 + ```bash 42 + # Build the project 43 + nix build 44 + 45 + # Run the parser 46 + nix run . -- payments.log 47 + 48 + # Development shell 49 + nix develop 50 + ``` 51 + 52 + ### Using CMake 53 + 54 + ```bash 55 + # Build the project 56 + cmake -B build -S . 57 + cmake --build build --config Release 58 + 59 + # Run the parser 60 + ./build/transaction-parser payments.log 61 + ``` 62 + 63 + ## Output Format 64 + 65 + Tab-separated values with the following fields: 66 + 67 + ``` 68 + TRANS_NUM|AMOUNT|CURRENCY|FIRSTNAME|LASTNAME|STREET|CITY|STATE|ZIP|CCTYPE|CCLAST4|EXPMONTH|EXPYEAR|CVV|TRANSID|STATUS|CORRID|PROC_AMOUNT 69 + ``` 70 + 71 + <p align="center"> 72 + <i><code>&copy 2025-present <a href="https://github.com/taciturnaxolotl">Kieran Klukas</a></code></i> 73 + </p> 74 + 75 + <p align="center"> 76 + <a href="https://github.com/taciturnaxolotl/soapdump/blob/main/LICENSE.md"><img src="https://img.shields.io/static/v1.svg?style=for-the-badge&label=License&message=MIT&logoColor=d9e0ee&colorA=363a4f&colorB=b7bdf8"/></a> 77 + </p>
+61
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1757487488, 6 + "narHash": "sha256-zwE/e7CuPJUWKdvvTCB7iunV4E/+G0lKfv4kk/5Izdg=", 7 + "owner": "NixOS", 8 + "repo": "nixpkgs", 9 + "rev": "ab0f3607a6c7486ea22229b92ed2d355f1482ee0", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "NixOS", 14 + "ref": "nixos-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs", 22 + "utils": "utils" 23 + } 24 + }, 25 + "systems": { 26 + "locked": { 27 + "lastModified": 1681028828, 28 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 29 + "owner": "nix-systems", 30 + "repo": "default", 31 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 32 + "type": "github" 33 + }, 34 + "original": { 35 + "owner": "nix-systems", 36 + "repo": "default", 37 + "type": "github" 38 + } 39 + }, 40 + "utils": { 41 + "inputs": { 42 + "systems": "systems" 43 + }, 44 + "locked": { 45 + "lastModified": 1731533236, 46 + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 47 + "owner": "numtide", 48 + "repo": "flake-utils", 49 + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 50 + "type": "github" 51 + }, 52 + "original": { 53 + "owner": "numtide", 54 + "repo": "flake-utils", 55 + "type": "github" 56 + } 57 + } 58 + }, 59 + "root": "root", 60 + "version": 7 61 + }
+68
flake.nix
··· 1 + { 2 + description = "PayPal SOAP Log Parser"; 3 + 4 + inputs = { 5 + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 + utils.url = "github:numtide/flake-utils"; 7 + }; 8 + 9 + outputs = { self, nixpkgs, utils }: 10 + utils.lib.eachDefaultSystem (system: 11 + let 12 + pkgs = nixpkgs.legacyPackages.${system}; 13 + 14 + # Build dependencies 15 + buildInputs = with pkgs; [ 16 + cmake 17 + ]; 18 + 19 + # Development dependencies 20 + nativeBuildInputs = with pkgs; [ 21 + clang-tools 22 + bear 23 + ]; 24 + in 25 + { 26 + packages = rec { 27 + soapdump = pkgs.stdenv.mkDerivation { 28 + pname = "soapdump"; 29 + version = "0.1.0"; 30 + 31 + src = ./.; 32 + 33 + nativeBuildInputs = [ pkgs.clang ]; 34 + 35 + buildPhase = '' 36 + mkdir -p bin 37 + clang++ -std=c++17 -O3 -o bin/transaction-parser src/transaction-parser.cpp 38 + ''; 39 + 40 + installPhase = '' 41 + mkdir -p $out/bin 42 + cp bin/transaction-parser $out/bin/ 43 + ''; 44 + }; 45 + 46 + default = soapdump; 47 + }; 48 + 49 + devShells.default = pkgs.mkShell { 50 + inherit buildInputs nativeBuildInputs; 51 + 52 + shellHook = '' 53 + echo "SoapDump development environment loaded" 54 + ''; 55 + }; 56 + 57 + apps = rec { 58 + transaction-parser = { 59 + type = "app"; 60 + program = "${self.packages.${system}.default}/bin/transaction-parser"; 61 + drv = self.packages.${system}.default; 62 + }; 63 + 64 + default = transaction-parser; 65 + }; 66 + } 67 + ); 68 + }
+353
src/transaction-parser.cpp
··· 1 + #include <cstdio> 2 + #include <iostream> 3 + #include <fstream> 4 + #include <string> 5 + #include <vector> 6 + #include <map> 7 + #include <regex> 8 + #include <algorithm> 9 + #include <numeric> 10 + #include <iomanip> 11 + #include <unordered_map> 12 + #include <getopt.h> 13 + 14 + // Transaction data structure 15 + struct Transaction { 16 + int transNum; 17 + std::string amount; 18 + std::string currency; 19 + std::string firstName; 20 + std::string lastName; 21 + std::string street; 22 + std::string city; 23 + std::string state; 24 + std::string zip; 25 + std::string ccType; 26 + std::string ccLast4; 27 + std::string expMonth; 28 + std::string expYear; 29 + std::string cvv; 30 + std::string transId; 31 + std::string status; 32 + std::string corrId; 33 + std::string procAmount; 34 + }; 35 + 36 + // Response data structure 37 + struct Response { 38 + std::string transId; 39 + std::string status; 40 + std::string corrId; 41 + std::string procAmount; 42 + }; 43 + 44 + // Function prototypes 45 + void showHelp(const char* programName); 46 + std::string extractXmlValue(const std::string& xml, const std::string& tag); 47 + std::string extractXmlAttribute(const std::string& xml, const std::string& attribute); 48 + std::vector<std::string> extractRequests(const std::string& logContent); 49 + std::vector<std::string> extractResponses(const std::string& logContent); 50 + std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls); 51 + std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses); 52 + void outputRawData(const std::vector<Transaction>& transactions); 53 + void outputSummary(const std::vector<Transaction>& transactions); 54 + 55 + int main(int argc, char* argv[]) { 56 + // Default options 57 + bool summaryOnly = false; 58 + std::string logFile; 59 + 60 + // Parse command line options 61 + static struct option longOptions[] = { 62 + {"help", no_argument, 0, 'h'}, 63 + {"summary", no_argument, 0, 's'}, 64 + {"raw", no_argument, 0, 'r'}, 65 + {0, 0, 0, 0} 66 + }; 67 + 68 + int optionIndex = 0; 69 + int opt; 70 + while ((opt = getopt_long(argc, argv, "hsr", longOptions, &optionIndex)) != -1) { 71 + switch (opt) { 72 + case 'h': 73 + showHelp(argv[0]); 74 + return 0; 75 + case 's': 76 + summaryOnly = true; 77 + break; 78 + case 'r': 79 + summaryOnly = false; 80 + break; 81 + case '?': 82 + std::cerr << "Unknown option: " << static_cast<char>(optopt) << std::endl; 83 + showHelp(argv[0]); 84 + return 1; 85 + default: 86 + break; 87 + } 88 + } 89 + 90 + // Get logfile name 91 + if (optind < argc) { 92 + logFile = argv[optind]; 93 + } else { 94 + std::cerr << "Error: No logfile specified" << std::endl; 95 + showHelp(argv[0]); 96 + return 1; 97 + } 98 + 99 + // Check if file exists 100 + std::ifstream file(logFile); 101 + if (!file.is_open()) { 102 + std::cerr << "Error: File '" << logFile << "' not found" << std::endl; 103 + return 1; 104 + } 105 + 106 + // Read the entire file 107 + std::string logContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); 108 + file.close(); 109 + 110 + // Extract requests and responses 111 + std::vector<std::string> requestXmls = extractRequests(logContent); 112 + std::vector<std::string> responseXmls = extractResponses(logContent); 113 + 114 + // Parse responses 115 + std::vector<Response> responses = parseResponses(responseXmls); 116 + 117 + // Parse transactions 118 + std::vector<Transaction> transactions = parseTransactions(requestXmls, responses); 119 + 120 + // Output data 121 + if (summaryOnly) { 122 + outputSummary(transactions); 123 + } else { 124 + outputRawData(transactions); 125 + } 126 + 127 + return 0; 128 + } 129 + 130 + void showHelp(const char* programName) { 131 + std::cout << "PayPal SOAP Log Parser\n\n"; 132 + std::cout << "USAGE:\n"; 133 + std::cout << " " << programName << " [OPTIONS] <logfile>\n\n"; 134 + std::cout << "OPTIONS:\n"; 135 + std::cout << " -h, --help Show this help message\n"; 136 + std::cout << " -s, --summary Show summary statistics only\n"; 137 + std::cout << " -r, --raw Output raw structured data (default)\n\n"; 138 + std::cout << "OUTPUT FORMAT (tab-separated):\n"; 139 + std::cout << " TRANS_NUM|AMOUNT|CURRENCY|FIRSTNAME|LASTNAME|STREET|CITY|STATE|ZIP|CCTYPE|CCLAST4|EXPMONTH|EXPYEAR|CVV|TRANSID|STATUS|CORRID|PROC_AMOUNT\n\n"; 140 + std::cout << "FIELD DESCRIPTIONS:\n"; 141 + std::cout << " TRANS_NUM - Transaction sequence number\n"; 142 + std::cout << " AMOUNT - Order total amount\n"; 143 + std::cout << " CURRENCY - Currency code (USD, etc)\n"; 144 + std::cout << " FIRSTNAME - Customer first name\n"; 145 + std::cout << " LASTNAME - Customer last name\n"; 146 + std::cout << " STREET - Street address\n"; 147 + std::cout << " CITY - City name\n"; 148 + std::cout << " STATE - State/Province code\n"; 149 + std::cout << " ZIP - Postal code\n"; 150 + std::cout << " CCTYPE - Credit card type (Visa, MasterCard, etc)\n"; 151 + std::cout << " CCLAST4 - Last 4 digits of credit card\n"; 152 + std::cout << " EXPMONTH - Card expiration month\n"; 153 + std::cout << " EXPYEAR - Card expiration year\n"; 154 + std::cout << " CVV - CVV code\n"; 155 + std::cout << " TRANSID - PayPal transaction ID\n"; 156 + std::cout << " STATUS - Transaction status (Success/Failure)\n"; 157 + std::cout << " CORRID - Correlation ID\n"; 158 + std::cout << " PROC_AMOUNT - Actually processed amount\n\n"; 159 + std::cout << "EXAMPLES:\n"; 160 + std::cout << " # Get all transactions\n"; 161 + std::cout << " " << programName << " payments.log\n\n"; 162 + std::cout << " # Get only successful transactions\n"; 163 + std::cout << " " << programName << " payments.log | grep Success\n\n"; 164 + std::cout << " # Count transactions by state\n"; 165 + std::cout << " " << programName << " payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr\n\n"; 166 + std::cout << " # Find largest transaction\n"; 167 + std::cout << " " << programName << " payments.log | sort -t'|' -k2 -nr | head -1\n\n"; 168 + std::cout << " # Get transactions over $500\n"; 169 + std::cout << " " << programName << " payments.log | awk -F'|' '$2 > 500'\n\n"; 170 + std::cout << " # Summary stats\n"; 171 + std::cout << " " << programName << " -s payments.log\n"; 172 + } 173 + 174 + std::string extractXmlValue(const std::string& xml, const std::string& tag) { 175 + std::regex pattern("<" + tag + "(?:[^>]*)>([^<]*)</" + tag + ">"); 176 + std::smatch match; 177 + if (std::regex_search(xml, match, pattern) && match.size() > 1) { 178 + return match[1].str(); 179 + } 180 + return ""; 181 + } 182 + 183 + std::string extractXmlAttribute(const std::string& xml, const std::string& attribute) { 184 + std::regex pattern(attribute + "=\"([^\"]*)\""); 185 + std::smatch match; 186 + if (std::regex_search(xml, match, pattern) && match.size() > 1) { 187 + return match[1].str(); 188 + } 189 + return ""; 190 + } 191 + 192 + std::vector<std::string> extractRequests(const std::string& logContent) { 193 + std::vector<std::string> requests; 194 + std::regex pattern("PPAPIService: Request: (.*)"); 195 + 196 + std::string::const_iterator searchStart(logContent.cbegin()); 197 + std::smatch match; 198 + while (std::regex_search(searchStart, logContent.cend(), match, pattern)) { 199 + if (match.size() > 1) { 200 + requests.push_back(match[1].str()); 201 + } 202 + searchStart = match.suffix().first; 203 + } 204 + 205 + return requests; 206 + } 207 + 208 + std::vector<std::string> extractResponses(const std::string& logContent) { 209 + std::vector<std::string> responses; 210 + std::regex pattern("PPAPIService: Response: <\\?.*\\?>(.*)"); 211 + 212 + std::string::const_iterator searchStart(logContent.cbegin()); 213 + std::smatch match; 214 + while (std::regex_search(searchStart, logContent.cend(), match, pattern)) { 215 + if (match.size() > 1) { 216 + responses.push_back(match[1].str()); 217 + } 218 + searchStart = match.suffix().first; 219 + } 220 + 221 + return responses; 222 + } 223 + 224 + std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls) { 225 + std::vector<Response> responses; 226 + 227 + for (const auto& xml : responseXmls) { 228 + Response response; 229 + response.transId = extractXmlValue(xml, "TransactionID"); 230 + response.status = extractXmlValue(xml, "Ack"); 231 + response.corrId = extractXmlValue(xml, "CorrelationID"); 232 + response.procAmount = extractXmlValue(xml, "Amount"); 233 + 234 + responses.push_back(response); 235 + } 236 + 237 + return responses; 238 + } 239 + 240 + std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses) { 241 + std::vector<Transaction> transactions; 242 + int transNum = 1; 243 + 244 + for (size_t i = 0; i < requestXmls.size(); ++i) { 245 + const auto& xml = requestXmls[i]; 246 + 247 + Transaction transaction; 248 + transaction.transNum = transNum++; 249 + 250 + // Extract request fields 251 + transaction.amount = extractXmlValue(xml, "ebl:OrderTotal"); 252 + transaction.currency = extractXmlAttribute(xml, "currencyID"); 253 + transaction.firstName = extractXmlValue(xml, "ebl:FirstName"); 254 + transaction.lastName = extractXmlValue(xml, "ebl:LastName"); 255 + transaction.street = extractXmlValue(xml, "ebl:Street1"); 256 + transaction.city = extractXmlValue(xml, "ebl:CityName"); 257 + transaction.state = extractXmlValue(xml, "ebl:StateOrProvince"); 258 + transaction.zip = extractXmlValue(xml, "ebl:PostalCode"); 259 + transaction.ccType = extractXmlValue(xml, "ebl:CreditCardType"); 260 + transaction.ccLast4 = extractXmlValue(xml, "ebl:CreditCardLastFourDigits"); 261 + transaction.expMonth = extractXmlValue(xml, "ebl:ExpMonth"); 262 + transaction.expYear = extractXmlValue(xml, "ebl:ExpYear"); 263 + transaction.cvv = extractXmlValue(xml, "ebl:CVV2"); 264 + 265 + // Get corresponding response data 266 + if (i < responses.size()) { 267 + transaction.transId = responses[i].transId; 268 + transaction.status = responses[i].status; 269 + transaction.corrId = responses[i].corrId; 270 + transaction.procAmount = responses[i].procAmount; 271 + } 272 + 273 + transactions.push_back(transaction); 274 + } 275 + 276 + return transactions; 277 + } 278 + 279 + void outputRawData(const std::vector<Transaction>& transactions) { 280 + for (const auto& t : transactions) { 281 + std::cout << t.transNum << "|" 282 + << t.amount << "|" 283 + << t.currency << "|" 284 + << t.firstName << "|" 285 + << t.lastName << "|" 286 + << t.street << "|" 287 + << t.city << "|" 288 + << t.state << "|" 289 + << t.zip << "|" 290 + << t.ccType << "|" 291 + << t.ccLast4 << "|" 292 + << t.expMonth << "|" 293 + << t.expYear << "|" 294 + << t.cvv << "|" 295 + << t.transId << "|" 296 + << t.status << "|" 297 + << t.corrId << "|" 298 + << t.procAmount << std::endl; 299 + } 300 + } 301 + 302 + void outputSummary(const std::vector<Transaction>& transactions) { 303 + std::cout << "=== SUMMARY ===" << std::endl; 304 + 305 + // Count transactions 306 + int total = transactions.size(); 307 + int successful = std::count_if(transactions.begin(), transactions.end(), 308 + [](const Transaction& t) { return t.status == "Success"; }); 309 + 310 + std::cout << "Total Transactions: " << total << std::endl; 311 + std::cout << "Successful: " << successful << std::endl; 312 + std::cout << "Failed: " << (total - successful) << std::endl; 313 + std::cout << std::endl; 314 + 315 + // Top 5 states 316 + std::map<std::string, int> stateCounts; 317 + for (const auto& t : transactions) { 318 + stateCounts[t.state]++; 319 + } 320 + 321 + std::cout << "Top 5 States by Transaction Count:" << std::endl; 322 + std::vector<std::pair<std::string, int>> stateCountVec(stateCounts.begin(), stateCounts.end()); 323 + std::sort(stateCountVec.begin(), stateCountVec.end(), 324 + [](const auto& a, const auto& b) { return a.second > b.second; }); 325 + 326 + int count = 0; 327 + for (const auto& sc : stateCountVec) { 328 + if (count++ >= 5) break; 329 + std::cout << " " << sc.first << ": " << sc.second << std::endl; 330 + } 331 + std::cout << std::endl; 332 + 333 + // Transaction amount stats 334 + std::vector<double> amounts; 335 + for (const auto& t : transactions) { 336 + try { 337 + amounts.push_back(std::stod(t.amount)); 338 + } catch (...) { 339 + // Skip invalid amounts 340 + } 341 + } 342 + 343 + if (!amounts.empty()) { 344 + double totalAmount = std::accumulate(amounts.begin(), amounts.end(), 0.0); 345 + double largest = *std::max_element(amounts.begin(), amounts.end()); 346 + double smallest = *std::min_element(amounts.begin(), amounts.end()); 347 + 348 + std::cout << "Transaction Amount Stats:" << std::endl; 349 + std::cout << " Total: $" << std::fixed << std::setprecision(2) << totalAmount << std::endl; 350 + std::cout << " Largest: $" << std::fixed << std::setprecision(2) << largest << std::endl; 351 + std::cout << " Smallest: $" << std::fixed << std::setprecision(2) << smallest << std::endl; 352 + } 353 + }