+23
CMakeLists.txt
+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
+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
+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
+77
README.md
···
1
+
# SoapDump
2
+
3
+
A high-performance PayPal SOAP log parser written in C++.
4
+
5
+

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>© 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
+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
+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
+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
+
}