parses paypal soap logs

feat: rename binary to soapdump

- Rename transaction-parser to soapdump in all files
- Update CMakeLists.txt to build soapdump
- Update flake.nix to build soapdump
- Update README.md and CRUSH.md with new name
- Rename source file to soapdump.cpp

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

dunkirk.sh 5f67976e cc0f6ec9

verified
+1
.gitignore
··· 1 1 # Build artifacts 2 2 /build/ 3 3 /result 4 + /soapdump 4 5 /transaction-parser 5 6 6 7 # Nix
+3 -3
CMakeLists.txt
··· 7 7 set(CMAKE_CXX_EXTENSIONS OFF) 8 8 9 9 # Add executable 10 - add_executable(transaction-parser src/transaction-parser.cpp) 10 + add_executable(soapdump src/soapdump.cpp) 11 11 12 12 # Set compiler flags 13 - target_compile_options(transaction-parser PRIVATE 13 + target_compile_options(soapdump PRIVATE 14 14 -Wall 15 15 -Wextra 16 16 -Wpedantic ··· 18 18 ) 19 19 20 20 # Install target 21 - install(TARGETS transaction-parser 21 + install(TARGETS soapdump 22 22 RUNTIME DESTINATION bin 23 23 )
+4 -4
CRUSH.md
··· 2 2 3 3 ## Commands 4 4 - **Build with nix**: `nix build` 5 - - **Run parser**: `./result/bin/transaction-parser <logfile>` 6 - - **Test parser**: `./result/bin/transaction-parser -s <logfile>` (summary mode) 7 - - **Help**: `./result/bin/transaction-parser --help` 8 - - **Build with clang**: `clang++ -std=c++17 -O3 -o transaction-parser src/transaction-parser.cpp` 5 + - **Run parser**: `./result/bin/soapdump <logfile>` 6 + - **Test parser**: `./result/bin/soapdump -s <logfile>` (summary mode) 7 + - **Help**: `./result/bin/soapdump --help` 8 + - **Build with clang**: `clang++ -std=c++17 -O3 -o soapdump src/soapdump.cpp` 9 9 10 10 ## Code Style 11 11 - **Language**: C++17
+7 -28
README.md
··· 2 2 3 3 A high-performance PayPal SOAP log parser written in C++. 4 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 5 ## Usage 16 6 17 7 ```bash 18 8 # Get all transactions 19 - ./transaction-parser payments.log 9 + soapdump payments.log 20 10 21 11 # Get only successful transactions 22 - ./transaction-parser payments.log | grep Success 12 + soapdump payments.log | grep Success 23 13 24 14 # Count transactions by state 25 - ./transaction-parser payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr 15 + soapdump payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr 26 16 27 17 # Find largest transaction 28 - ./transaction-parser payments.log | sort -t'|' -k2 -nr | head -1 18 + soapdump payments.log | sort -t'|' -k2 -nr | head -1 29 19 30 20 # Get transactions over $500 31 - ./transaction-parser payments.log | awk -F'|' '$2 > 500' 21 + soapdump payments.log | awk -F'|' '$2 > 500' 32 22 33 23 # Summary stats 34 - ./transaction-parser -s payments.log 24 + soapdump -s payments.log 35 25 ``` 36 26 37 27 ## Building ··· 49 39 nix develop 50 40 ``` 51 41 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 42 ## Output Format 64 43 65 44 Tab-separated values with the following fields: ··· 74 53 75 54 <p align="center"> 76 55 <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> 56 + </p>
+5 -5
flake.nix
··· 34 34 35 35 buildPhase = '' 36 36 mkdir -p bin 37 - clang++ -std=c++17 -O3 -o bin/transaction-parser src/transaction-parser.cpp 37 + clang++ -std=c++17 -O3 -o bin/soapdump src/soapdump.cpp 38 38 ''; 39 39 40 40 installPhase = '' 41 41 mkdir -p $out/bin 42 - cp bin/transaction-parser $out/bin/ 42 + cp bin/soapdump $out/bin/ 43 43 ''; 44 44 }; 45 45 ··· 55 55 }; 56 56 57 57 apps = rec { 58 - transaction-parser = { 58 + soapdump = { 59 59 type = "app"; 60 - program = "${self.packages.${system}.default}/bin/transaction-parser"; 60 + program = "${self.packages.${system}.default}/bin/soapdump"; 61 61 drv = self.packages.${system}.default; 62 62 }; 63 63 64 - default = transaction-parser; 64 + default = soapdump; 65 65 }; 66 66 } 67 67 );
+352
src/soapdump.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 <getopt.h> 12 + 13 + // Transaction data structure 14 + struct Transaction { 15 + int transNum; 16 + std::string amount; 17 + std::string currency; 18 + std::string firstName; 19 + std::string lastName; 20 + std::string street; 21 + std::string city; 22 + std::string state; 23 + std::string zip; 24 + std::string ccType; 25 + std::string ccLast4; 26 + std::string expMonth; 27 + std::string expYear; 28 + std::string cvv; 29 + std::string transId; 30 + std::string status; 31 + std::string corrId; 32 + std::string procAmount; 33 + }; 34 + 35 + // Response data structure 36 + struct Response { 37 + std::string transId; 38 + std::string status; 39 + std::string corrId; 40 + std::string procAmount; 41 + }; 42 + 43 + // Function prototypes 44 + void showHelp(const char* programName); 45 + std::string extractXmlValue(const std::string& xml, const std::string& tag); 46 + std::string extractXmlAttribute(const std::string& xml, const std::string& attribute); 47 + std::vector<std::string> extractRequests(const std::string& logContent); 48 + std::vector<std::string> extractResponses(const std::string& logContent); 49 + std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls); 50 + std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses); 51 + void outputRawData(const std::vector<Transaction>& transactions); 52 + void outputSummary(const std::vector<Transaction>& transactions); 53 + 54 + int main(int argc, char* argv[]) { 55 + // Default options 56 + bool summaryOnly = false; 57 + std::string logFile; 58 + 59 + // Parse command line options 60 + static struct option longOptions[] = { 61 + {"help", no_argument, 0, 'h'}, 62 + {"summary", no_argument, 0, 's'}, 63 + {"raw", no_argument, 0, 'r'}, 64 + {0, 0, 0, 0} 65 + }; 66 + 67 + int optionIndex = 0; 68 + int opt; 69 + while ((opt = getopt_long(argc, argv, "hsr", longOptions, &optionIndex)) != -1) { 70 + switch (opt) { 71 + case 'h': 72 + showHelp(argv[0]); 73 + return 0; 74 + case 's': 75 + summaryOnly = true; 76 + break; 77 + case 'r': 78 + summaryOnly = false; 79 + break; 80 + case '?': 81 + std::cerr << "Unknown option: " << static_cast<char>(optopt) << std::endl; 82 + showHelp(argv[0]); 83 + return 1; 84 + default: 85 + break; 86 + } 87 + } 88 + 89 + // Get logfile name 90 + if (optind < argc) { 91 + logFile = argv[optind]; 92 + } else { 93 + std::cerr << "Error: No logfile specified" << std::endl; 94 + showHelp(argv[0]); 95 + return 1; 96 + } 97 + 98 + // Check if file exists 99 + std::ifstream file(logFile); 100 + if (!file.is_open()) { 101 + std::cerr << "Error: File '" << logFile << "' not found" << std::endl; 102 + return 1; 103 + } 104 + 105 + // Read the entire file 106 + std::string logContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); 107 + file.close(); 108 + 109 + // Extract requests and responses 110 + std::vector<std::string> requestXmls = extractRequests(logContent); 111 + std::vector<std::string> responseXmls = extractResponses(logContent); 112 + 113 + // Parse responses 114 + std::vector<Response> responses = parseResponses(responseXmls); 115 + 116 + // Parse transactions 117 + std::vector<Transaction> transactions = parseTransactions(requestXmls, responses); 118 + 119 + // Output data 120 + if (summaryOnly) { 121 + outputSummary(transactions); 122 + } else { 123 + outputRawData(transactions); 124 + } 125 + 126 + return 0; 127 + } 128 + 129 + void showHelp(const char* programName) { 130 + std::cout << "PayPal SOAP Log Parser\n\n"; 131 + std::cout << "USAGE:\n"; 132 + std::cout << " " << programName << " [OPTIONS] <logfile>\n\n"; 133 + std::cout << "OPTIONS:\n"; 134 + std::cout << " -h, --help Show this help message\n"; 135 + std::cout << " -s, --summary Show summary statistics only\n"; 136 + std::cout << " -r, --raw Output raw structured data (default)\n\n"; 137 + std::cout << "OUTPUT FORMAT:\n"; 138 + std::cout << " TRANS_NUM|AMOUNT|CURRENCY|FIRSTNAME|LASTNAME|STREET|CITY|STATE|ZIP|CCTYPE|CCLAST4|EXPMONTH|EXPYEAR|CVV|TRANSID|STATUS|CORRID|PROC_AMOUNT\n\n"; 139 + std::cout << "FIELD DESCRIPTIONS:\n"; 140 + std::cout << " TRANS_NUM - Transaction sequence number\n"; 141 + std::cout << " AMOUNT - Order total amount\n"; 142 + std::cout << " CURRENCY - Currency code (USD, etc)\n"; 143 + std::cout << " FIRSTNAME - Customer first name\n"; 144 + std::cout << " LASTNAME - Customer last name\n"; 145 + std::cout << " STREET - Street address\n"; 146 + std::cout << " CITY - City name\n"; 147 + std::cout << " STATE - State/Province code\n"; 148 + std::cout << " ZIP - Postal code\n"; 149 + std::cout << " CCTYPE - Credit card type (Visa, MasterCard, etc)\n"; 150 + std::cout << " CCLAST4 - Last 4 digits of credit card\n"; 151 + std::cout << " EXPMONTH - Card expiration month\n"; 152 + std::cout << " EXPYEAR - Card expiration year\n"; 153 + std::cout << " CVV - CVV code\n"; 154 + std::cout << " TRANSID - PayPal transaction ID\n"; 155 + std::cout << " STATUS - Transaction status (Success/Failure)\n"; 156 + std::cout << " CORRID - Correlation ID\n"; 157 + std::cout << " PROC_AMOUNT - Actually processed amount\n\n"; 158 + std::cout << "EXAMPLES:\n"; 159 + std::cout << " # Get all transactions\n"; 160 + std::cout << " " << programName << " payments.log\n\n"; 161 + std::cout << " # Get only successful transactions\n"; 162 + std::cout << " " << programName << " payments.log | grep Success\n\n"; 163 + std::cout << " # Count transactions by state\n"; 164 + std::cout << " " << programName << " payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr\n\n"; 165 + std::cout << " # Find largest transaction\n"; 166 + std::cout << " " << programName << " payments.log | sort -t'|' -k2 -nr | head -1\n\n"; 167 + std::cout << " # Get transactions over $500\n"; 168 + std::cout << " " << programName << " payments.log | awk -F'|' '$2 > 500'\n\n"; 169 + std::cout << " # Summary stats\n"; 170 + std::cout << " " << programName << " -s payments.log\n"; 171 + } 172 + 173 + std::string extractXmlValue(const std::string& xml, const std::string& tag) { 174 + std::regex pattern("<" + tag + "(?:[^>]*)>([^<]*)</" + tag + ">"); 175 + std::smatch match; 176 + if (std::regex_search(xml, match, pattern) && match.size() > 1) { 177 + return match[1].str(); 178 + } 179 + return ""; 180 + } 181 + 182 + std::string extractXmlAttribute(const std::string& xml, const std::string& attribute) { 183 + std::regex pattern(attribute + "=\"([^\"]*)\""); 184 + std::smatch match; 185 + if (std::regex_search(xml, match, pattern) && match.size() > 1) { 186 + return match[1].str(); 187 + } 188 + return ""; 189 + } 190 + 191 + std::vector<std::string> extractRequests(const std::string& logContent) { 192 + std::vector<std::string> requests; 193 + std::regex pattern("PPAPIService: Request: (.*)"); 194 + 195 + std::string::const_iterator searchStart(logContent.cbegin()); 196 + std::smatch match; 197 + while (std::regex_search(searchStart, logContent.cend(), match, pattern)) { 198 + if (match.size() > 1) { 199 + requests.push_back(match[1].str()); 200 + } 201 + searchStart = match.suffix().first; 202 + } 203 + 204 + return requests; 205 + } 206 + 207 + std::vector<std::string> extractResponses(const std::string& logContent) { 208 + std::vector<std::string> responses; 209 + std::regex pattern("PPAPIService: Response: <\\?.*\\?>(.*)"); 210 + 211 + std::string::const_iterator searchStart(logContent.cbegin()); 212 + std::smatch match; 213 + while (std::regex_search(searchStart, logContent.cend(), match, pattern)) { 214 + if (match.size() > 1) { 215 + responses.push_back(match[1].str()); 216 + } 217 + searchStart = match.suffix().first; 218 + } 219 + 220 + return responses; 221 + } 222 + 223 + std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls) { 224 + std::vector<Response> responses; 225 + 226 + for (const auto& xml : responseXmls) { 227 + Response response; 228 + response.transId = extractXmlValue(xml, "TransactionID"); 229 + response.status = extractXmlValue(xml, "Ack"); 230 + response.corrId = extractXmlValue(xml, "CorrelationID"); 231 + response.procAmount = extractXmlValue(xml, "Amount"); 232 + 233 + responses.push_back(response); 234 + } 235 + 236 + return responses; 237 + } 238 + 239 + std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses) { 240 + std::vector<Transaction> transactions; 241 + int transNum = 1; 242 + 243 + for (size_t i = 0; i < requestXmls.size(); ++i) { 244 + const auto& xml = requestXmls[i]; 245 + 246 + Transaction transaction; 247 + transaction.transNum = transNum++; 248 + 249 + // Extract request fields 250 + transaction.amount = extractXmlValue(xml, "ebl:OrderTotal"); 251 + transaction.currency = extractXmlAttribute(xml, "currencyID"); 252 + transaction.firstName = extractXmlValue(xml, "ebl:FirstName"); 253 + transaction.lastName = extractXmlValue(xml, "ebl:LastName"); 254 + transaction.street = extractXmlValue(xml, "ebl:Street1"); 255 + transaction.city = extractXmlValue(xml, "ebl:CityName"); 256 + transaction.state = extractXmlValue(xml, "ebl:StateOrProvince"); 257 + transaction.zip = extractXmlValue(xml, "ebl:PostalCode"); 258 + transaction.ccType = extractXmlValue(xml, "ebl:CreditCardType"); 259 + transaction.ccLast4 = extractXmlValue(xml, "ebl:CreditCardLastFourDigits"); 260 + transaction.expMonth = extractXmlValue(xml, "ebl:ExpMonth"); 261 + transaction.expYear = extractXmlValue(xml, "ebl:ExpYear"); 262 + transaction.cvv = extractXmlValue(xml, "ebl:CVV2"); 263 + 264 + // Get corresponding response data 265 + if (i < responses.size()) { 266 + transaction.transId = responses[i].transId; 267 + transaction.status = responses[i].status; 268 + transaction.corrId = responses[i].corrId; 269 + transaction.procAmount = responses[i].procAmount; 270 + } 271 + 272 + transactions.push_back(transaction); 273 + } 274 + 275 + return transactions; 276 + } 277 + 278 + void outputRawData(const std::vector<Transaction>& transactions) { 279 + for (const auto& t : transactions) { 280 + std::cout << t.transNum << "|" 281 + << t.amount << "|" 282 + << t.currency << "|" 283 + << t.firstName << "|" 284 + << t.lastName << "|" 285 + << t.street << "|" 286 + << t.city << "|" 287 + << t.state << "|" 288 + << t.zip << "|" 289 + << t.ccType << "|" 290 + << t.ccLast4 << "|" 291 + << t.expMonth << "|" 292 + << t.expYear << "|" 293 + << t.cvv << "|" 294 + << t.transId << "|" 295 + << t.status << "|" 296 + << t.corrId << "|" 297 + << t.procAmount << std::endl; 298 + } 299 + } 300 + 301 + void outputSummary(const std::vector<Transaction>& transactions) { 302 + std::cout << "=== SUMMARY ===" << std::endl; 303 + 304 + // Count transactions 305 + int total = transactions.size(); 306 + int successful = std::count_if(transactions.begin(), transactions.end(), 307 + [](const Transaction& t) { return t.status == "Success"; }); 308 + 309 + std::cout << "Total Transactions: " << total << std::endl; 310 + std::cout << "Successful: " << successful << std::endl; 311 + std::cout << "Failed: " << (total - successful) << std::endl; 312 + std::cout << std::endl; 313 + 314 + // Top 5 states 315 + std::map<std::string, int> stateCounts; 316 + for (const auto& t : transactions) { 317 + stateCounts[t.state]++; 318 + } 319 + 320 + std::cout << "Top 5 States by Transaction Count:" << std::endl; 321 + std::vector<std::pair<std::string, int>> stateCountVec(stateCounts.begin(), stateCounts.end()); 322 + std::sort(stateCountVec.begin(), stateCountVec.end(), 323 + [](const auto& a, const auto& b) { return a.second > b.second; }); 324 + 325 + int count = 0; 326 + for (const auto& sc : stateCountVec) { 327 + if (count++ >= 5) break; 328 + std::cout << " " << sc.first << ": " << sc.second << std::endl; 329 + } 330 + std::cout << std::endl; 331 + 332 + // Transaction amount stats 333 + std::vector<double> amounts; 334 + for (const auto& t : transactions) { 335 + try { 336 + amounts.push_back(std::stod(t.amount)); 337 + } catch (...) { 338 + // Skip invalid amounts 339 + } 340 + } 341 + 342 + if (!amounts.empty()) { 343 + double totalAmount = std::accumulate(amounts.begin(), amounts.end(), 0.0); 344 + double largest = *std::max_element(amounts.begin(), amounts.end()); 345 + double smallest = *std::min_element(amounts.begin(), amounts.end()); 346 + 347 + std::cout << "Transaction Amount Stats:" << std::endl; 348 + std::cout << " Total: $" << std::fixed << std::setprecision(2) << totalAmount << std::endl; 349 + std::cout << " Largest: $" << std::fixed << std::setprecision(2) << largest << std::endl; 350 + std::cout << " Smallest: $" << std::fixed << std::setprecision(2) << smallest << std::endl; 351 + } 352 + }