parses paypal soap logs

Compare changes

Choose any two refs to compare.

+70
.github/workflows/release.yml
···
+
name: Build and Release
+
+
on:
+
push:
+
tags:
+
- 'v*'
+
+
jobs:
+
build:
+
name: Build on ${{ matrix.os }}
+
runs-on: ${{ matrix.os }}
+
permissions:
+
contents: read
+
strategy:
+
fail-fast: false
+
matrix:
+
include:
+
- os: ubuntu-latest
+
platform: x86_64-linux
+
- os: macos-latest
+
platform: x86_64-darwin
+
- os: macos-latest
+
platform: aarch64-darwin
+
extra_args: --option system aarch64-darwin
+
+
steps:
+
- name: Checkout code
+
uses: actions/checkout@v4
+
+
- name: Install Nix
+
uses: DeterminateSystems/determinate-nix-action@v3.11.2
+
+
- name: Build binary
+
run: |
+
if [[ "${{ matrix.platform }}" == *"-linux" ]]; then
+
# For Linux builds
+
nix build .#packages.${{ matrix.platform }}.default --option system ${{ matrix.platform }} -o result-${{ matrix.platform }}
+
else
+
# For macOS builds
+
nix build .#packages.${{ matrix.platform }}.default ${{ matrix.extra_args }} -o result-${{ matrix.platform }}
+
fi
+
mkdir -p dist
+
cp -r result-${{ matrix.platform }}/bin/* dist/
+
cd dist && tar -czf ../soapdump-${{ matrix.platform }}.tar.gz *
+
+
- name: Upload artifact
+
uses: actions/upload-artifact@v4
+
with:
+
name: soapdump-${{ matrix.platform }}
+
path: soapdump-${{ matrix.platform }}.tar.gz
+
+
release:
+
name: Create Release
+
needs: build
+
runs-on: ubuntu-latest
+
permissions:
+
contents: write
+
steps:
+
- name: Download all artifacts
+
uses: actions/download-artifact@v4
+
+
- name: Create Release
+
id: create_release
+
uses: softprops/action-gh-release@v1
+
with:
+
files: |
+
soapdump-x86_64-linux/soapdump-x86_64-linux.tar.gz
+
soapdump-x86_64-darwin/soapdump-x86_64-darwin.tar.gz
+
soapdump-aarch64-darwin/soapdump-aarch64-darwin.tar.gz
+
generate_release_notes: true
+2 -2
.gitignore
···
# Build artifacts
/build/
/result
-
/transaction-parser
+
/soapdump
# Nix
.direnv/
···
# OS files
.DS_Store
-
Thumbs.db
+
Thumbs.db
+3 -3
CMakeLists.txt
···
set(CMAKE_CXX_EXTENSIONS OFF)
# Add executable
-
add_executable(transaction-parser src/transaction-parser.cpp)
+
add_executable(soapdump src/soapdump.cpp)
# Set compiler flags
-
target_compile_options(transaction-parser PRIVATE
+
target_compile_options(soapdump PRIVATE
-Wall
-Wextra
-Wpedantic
···
)
# Install target
-
install(TARGETS transaction-parser
+
install(TARGETS soapdump
RUNTIME DESTINATION bin
)
+26 -16
CRUSH.md
···
# CRUSH Development Guidelines
## Commands
-
- **Run parser**: `./transaction-parser.sh <logfile>`
-
- **Test parser**: `./transaction-parser.sh -s <logfile>` (summary mode)
-
- **Help**: `./transaction-parser.sh --help`
+
- **Build with nix**: `nix build`
+
- **Run parser**: `./result/bin/soapdump <logfile>`
+
- **Test parser**: `./result/bin/soapdump -s <logfile>` (summary mode)
+
- **Help**: `./result/bin/soapdump --help`
+
- **Build with clang**: `clang++ -std=c++17 -O3 -o soapdump src/soapdump.cpp`
## Code Style
-
- **Language**: Bash scripting
-
- **Shebang**: Use `#!/usr/bin/env nix-shell` with dependencies
+
- **Language**: C++17
- **Formatting**:
- 4-space indentation
-
- Functions use snake_case
-
- Variables use UPPER_CASE
+
- Functions use camelCase
+
- Classes use PascalCase
+
- Variables use camelCase
- **Structure**:
- Clear function separation
- Help documentation included
-
- Error handling with exit codes
-
- **Dependencies**: gnugrep, gnused, coreutils (via nix-shell)
+
- Error handling with return codes
+
- **Dependencies**: Standard C++ libraries only
+
+
## Build System
+
- **Nix Flake**: For reproducible builds and development environment
+
- **CMake**: For cross-platform build configuration
+
- **Compiler**: Clang++ with C++17 standard
## Best Practices
- Always validate input files exist
-
- Use proper error messages and exit codes
+
- Use proper error messages and return codes
- Include comprehensive help documentation
-
- Follow tab-separated output format for structured data
-
- Handle edge cases in XML parsing
+
- Follow pipe-separated output format for structured data
+
- Handle edge cases in XML parsing with regex
+
- Use STL containers and algorithms for performance
## Naming Conventions
-
- Variables: UPPER_CASE
-
- Functions: snake_case
-
- Files: kebab-case.sh
+
- Variables: camelCase
+
- Functions: camelCase
+
- Classes: PascalCase
+
- Files: kebab-case.cpp
## Error Handling
- Check file existence before processing
- Validate arguments
-
- Exit with appropriate codes (0 for success, 1 for error)
+
- Return appropriate codes (0 for success, 1 for error)
+
- Use try/catch blocks for exception handling
+35 -18
README.md
···
A high-performance PayPal SOAP log parser written in C++.
-
![a media offline screen from davinci resolve](https://hc-cdn.hel1.your-objectstorage.com/s/v3/8744661d883d695adc6b14a17a52ac8970d7c2dd_image.png)
-
-
## Features
-
-
- Fast parsing of PayPal SOAP transaction logs
-
- Structured output for easy analysis
-
- Summary statistics mode
-
- Pipe-friendly output format
-
- Nix flake for reproducible builds
+
![gif of the program in action](https://github.com/taciturnaxolotl/soapdump/blob/main/docs/vhs.gif)
## Usage
```bash
# Get all transactions
-
./transaction-parser payments.log
+
soapdump payments.log
# Get only successful transactions
-
./transaction-parser payments.log | grep Success
+
soapdump payments.log | grep Success
# Count transactions by state
-
./transaction-parser payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr
+
soapdump payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr
# Find largest transaction
-
./transaction-parser payments.log | sort -t'|' -k2 -nr | head -1
+
soapdump payments.log | sort -t'|' -k2 -nr | head -1
# Get transactions over $500
-
./transaction-parser payments.log | awk -F'|' '$2 > 500'
+
soapdump payments.log | awk -F'|' '$2 > 500'
# Summary stats
-
./transaction-parser -s payments.log
+
soapdump -s payments.log
```
## Building
···
cmake --build build --config Release
# Run the parser
-
./build/transaction-parser payments.log
+
./build/soapdump payments.log
+
```
+
+
### Installation
+
+
```bash
+
nix profile install github:taciturnaxolotl/soapdump
+
```
+
+
or in the flake:
+
+
```nix
+
{
+
inputs.soapdump.url = "github:taciturnaxolotl/soapdump";
+
+
outputs = { self, nixpkgs, soapdump, ... }: {
+
# Access the package as:
+
# soapdump.packages.${system}.default
+
};
+
}
+
```
+
+
Or run without installing:
+
+
```bash
+
nix run github:taciturnaxolotl/soapdump -- payments.log
```
## Output Format
-
Tab-separated values with the following fields:
+
Pipe-separated values with the following fields:
```
TRANS_NUM|AMOUNT|CURRENCY|FIRSTNAME|LASTNAME|STREET|CITY|STATE|ZIP|CCTYPE|CCLAST4|EXPMONTH|EXPYEAR|CVV|TRANSID|STATUS|CORRID|PROC_AMOUNT
···
<p align="center">
<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>
-
</p>
+
</p>
+28
cassette.tape
···
+
Output docs/vhs.gif
+
Set Shell zsh
+
Sleep 1s
+
Type "soapdump -h"
+
Enter
+
Sleep 1s
+
Type "man soapdump"
+
Enter
+
Sleep 1s
+
Enter
+
Down 2
+
Sleep 500ms
+
Down 2
+
Sleep 500ms
+
Down 2
+
Sleep 500ms
+
Down 2
+
Sleep 500ms
+
Down 2
+
Sleep 500ms
+
Down 2
+
Sleep 1200ms
+
Type "q"
+
Sleep 500ms
+
Sleep 1s
+
Type "soapdump payments.log"
+
Enter
+
Sleep 3.5s
docs/vhs.gif

This is a binary file and will not be displayed.

+1 -35
flake.lock
···
},
"root": {
"inputs": {
-
"nixpkgs": "nixpkgs",
-
"utils": "utils"
-
}
-
},
-
"systems": {
-
"locked": {
-
"lastModified": 1681028828,
-
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-
"owner": "nix-systems",
-
"repo": "default",
-
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-
"type": "github"
-
},
-
"original": {
-
"owner": "nix-systems",
-
"repo": "default",
-
"type": "github"
-
}
-
},
-
"utils": {
-
"inputs": {
-
"systems": "systems"
-
},
-
"locked": {
-
"lastModified": 1731533236,
-
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
-
"owner": "numtide",
-
"repo": "flake-utils",
-
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
-
"type": "github"
-
},
-
"original": {
-
"owner": "numtide",
-
"repo": "flake-utils",
-
"type": "github"
+
"nixpkgs": "nixpkgs"
}
}
},
+95 -51
flake.nix
···
{
-
description = "PayPal SOAP Log Parser";
+
description = "SoapDump - A high-performance PayPal SOAP log parser";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
-
utils.url = "github:numtide/flake-utils";
};
-
outputs = { self, nixpkgs, utils }:
-
utils.lib.eachDefaultSystem (system:
-
let
-
pkgs = nixpkgs.legacyPackages.${system};
-
-
# Build dependencies
-
buildInputs = with pkgs; [
-
cmake
-
];
-
-
# Development dependencies
-
nativeBuildInputs = with pkgs; [
-
clang-tools
-
bear
-
];
-
in
-
{
-
packages = rec {
+
outputs =
+
{ self, nixpkgs, ... }:
+
let
+
allSystems = [
+
"x86_64-linux" # 64-bit Intel/AMD Linux
+
"aarch64-linux" # 64-bit ARM Linux
+
"x86_64-darwin" # 64-bit Intel macOS
+
"aarch64-darwin" # 64-bit ARM macOS
+
];
+
forAllSystems =
+
f:
+
nixpkgs.lib.genAttrs allSystems (
+
system:
+
f {
+
pkgs = import nixpkgs { inherit system; };
+
}
+
);
+
in
+
{
+
packages = forAllSystems (
+
{ pkgs }:
+
let
+
version = if self ? rev then self.rev else "0.1.0";
soapdump = pkgs.stdenv.mkDerivation {
pname = "soapdump";
-
version = "0.1.0";
-
-
src = ./.;
-
-
nativeBuildInputs = [ pkgs.clang ];
-
+
inherit version;
+
src = self;
+
+
nativeBuildInputs = with pkgs; [
+
clang
+
installShellFiles
+
];
+
+
dontUseCmakeConfigure = true;
+
buildPhase = ''
-
mkdir -p bin
-
clang++ -std=c++17 -O3 -o bin/transaction-parser src/transaction-parser.cpp
+
# Direct compilation instead of using CMake
+
mkdir -p build
+
$CXX -std=c++17 -O3 -o build/soapdump $src/src/soapdump.cpp
'';
-
+
installPhase = ''
mkdir -p $out/bin
-
cp bin/transaction-parser $out/bin/
+
cp build/soapdump $out/bin/
+
+
# Generate and install shell completions
+
mkdir -p completions
+
$out/bin/soapdump --generate-bash-completion > completions/soapdump.bash
+
$out/bin/soapdump --generate-zsh-completion > completions/soapdump.zsh
+
$out/bin/soapdump --generate-fish-completion > completions/soapdump.fish
+
+
installShellCompletion --cmd soapdump \
+
--bash completions/soapdump.bash \
+
--fish completions/soapdump.fish \
+
--zsh completions/soapdump.zsh
+
+
# Generate and install man page
+
mkdir -p $out/share/man/man1
+
$out/bin/soapdump --man > $out/share/man/man1/soapdump.1
'';
+
+
meta = with pkgs.lib; {
+
description = "A high-performance PayPal SOAP log parser";
+
homepage = "https://github.com/taciturnaxolotl/soapdump";
+
license = licenses.mit;
+
maintainers = with maintainers; [ taciturnaxolotl ];
+
platforms = platforms.linux ++ platforms.darwin;
+
};
};
-
+
in
+
{
default = soapdump;
-
};
-
-
devShells.default = pkgs.mkShell {
-
inherit buildInputs nativeBuildInputs;
-
-
shellHook = ''
-
echo "SoapDump development environment loaded"
-
'';
-
};
-
-
apps = rec {
-
transaction-parser = {
+
}
+
);
+
+
apps = forAllSystems (
+
{ pkgs }:
+
{
+
default = {
type = "app";
-
program = "${self.packages.${system}.default}/bin/transaction-parser";
-
drv = self.packages.${system}.default;
+
program = "${self.packages.${pkgs.system}.default}/bin/soapdump";
};
-
-
default = transaction-parser;
-
};
-
}
-
);
-
}
+
}
+
);
+
+
devShells = forAllSystems (
+
{ pkgs }:
+
{
+
default = pkgs.mkShell {
+
buildInputs = with pkgs; [
+
cmake
+
clang
+
self.packages.${pkgs.system}.default
+
];
+
+
shellHook = ''
+
echo "SoapDump development environment loaded"
+
'';
+
};
+
}
+
);
+
+
formatter = forAllSystems ({ pkgs }: pkgs.nixfmt-tree);
+
};
+
}
+497
src/soapdump.cpp
···
+
#include <cstdio>
+
#include <iostream>
+
#include <fstream>
+
#include <string>
+
#include <vector>
+
#include <map>
+
#include <regex>
+
#include <algorithm>
+
#include <numeric>
+
#include <iomanip>
+
#include <getopt.h>
+
#include <cstring>
+
+
// Transaction data structure
+
struct Transaction {
+
int transNum;
+
std::string amount;
+
std::string currency;
+
std::string firstName;
+
std::string lastName;
+
std::string street;
+
std::string city;
+
std::string state;
+
std::string zip;
+
std::string ccType;
+
std::string ccLast4;
+
std::string expMonth;
+
std::string expYear;
+
std::string cvv;
+
std::string transId;
+
std::string status;
+
std::string corrId;
+
std::string procAmount;
+
};
+
+
// Response data structure
+
struct Response {
+
std::string transId;
+
std::string status;
+
std::string corrId;
+
std::string procAmount;
+
};
+
+
// Function prototypes
+
void showHelp(const char* programName);
+
void generateBashCompletion();
+
void generateZshCompletion();
+
void generateFishCompletion();
+
void generateManPage();
+
std::string extractXmlValue(const std::string& xml, const std::string& tag);
+
std::string extractXmlAttribute(const std::string& xml, const std::string& attribute);
+
std::vector<std::string> extractRequests(const std::string& logContent);
+
std::vector<std::string> extractResponses(const std::string& logContent);
+
std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls);
+
std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses);
+
void outputRawData(const std::vector<Transaction>& transactions);
+
void outputSummary(const std::vector<Transaction>& transactions);
+
+
int main(int argc, char* argv[]) {
+
// Default options
+
bool summaryOnly = false;
+
std::string logFile;
+
+
// Parse command line options
+
static struct option longOptions[] = {
+
{"help", no_argument, 0, 'h'},
+
{"summary", no_argument, 0, 's'},
+
{"raw", no_argument, 0, 'r'},
+
{"generate-bash-completion", no_argument, 0, 0},
+
{"generate-zsh-completion", no_argument, 0, 0},
+
{"generate-fish-completion", no_argument, 0, 0},
+
{"man", no_argument, 0, 0},
+
{0, 0, 0, 0}
+
};
+
+
int optionIndex = 0;
+
int opt;
+
while ((opt = getopt_long(argc, argv, "hsr", longOptions, &optionIndex)) != -1) {
+
switch (opt) {
+
case 0:
+
// Long options without short equivalents
+
if (strcmp(longOptions[optionIndex].name, "generate-bash-completion") == 0) {
+
generateBashCompletion();
+
return 0;
+
} else if (strcmp(longOptions[optionIndex].name, "generate-zsh-completion") == 0) {
+
generateZshCompletion();
+
return 0;
+
} else if (strcmp(longOptions[optionIndex].name, "generate-fish-completion") == 0) {
+
generateFishCompletion();
+
return 0;
+
} else if (strcmp(longOptions[optionIndex].name, "man") == 0) {
+
generateManPage();
+
return 0;
+
}
+
break;
+
case 'h':
+
showHelp(argv[0]);
+
return 0;
+
case 's':
+
summaryOnly = true;
+
break;
+
case 'r':
+
summaryOnly = false;
+
break;
+
case '?':
+
std::cerr << "Unknown option: " << static_cast<char>(optopt) << std::endl;
+
showHelp(argv[0]);
+
return 1;
+
default:
+
break;
+
}
+
}
+
+
// Get logfile name
+
if (optind < argc) {
+
logFile = argv[optind];
+
} else {
+
std::cerr << "Error: No logfile specified" << std::endl;
+
showHelp(argv[0]);
+
return 1;
+
}
+
+
// Check if file exists
+
std::ifstream file(logFile);
+
if (!file.is_open()) {
+
std::cerr << "Error: File '" << logFile << "' not found" << std::endl;
+
return 1;
+
}
+
+
// Read the entire file
+
std::string logContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
+
file.close();
+
+
// Extract requests and responses
+
std::vector<std::string> requestXmls = extractRequests(logContent);
+
std::vector<std::string> responseXmls = extractResponses(logContent);
+
+
// Parse responses
+
std::vector<Response> responses = parseResponses(responseXmls);
+
+
// Parse transactions
+
std::vector<Transaction> transactions = parseTransactions(requestXmls, responses);
+
+
// Output data
+
if (summaryOnly) {
+
outputSummary(transactions);
+
} else {
+
outputRawData(transactions);
+
}
+
+
return 0;
+
}
+
+
void showHelp(const char* programName) {
+
std::cout << "PayPal SOAP Log Parser\n\n";
+
std::cout << "USAGE:\n";
+
std::cout << " " << programName << " [OPTIONS] <logfile>\n\n";
+
std::cout << "OPTIONS:\n";
+
std::cout << " -h, --help Show this help message\n";
+
std::cout << " -s, --summary Show summary statistics only\n";
+
std::cout << " -r, --raw Output raw structured data (default)\n";
+
std::cout << " --generate-bash-completion Generate Bash completion script\n";
+
std::cout << " --generate-zsh-completion Generate Zsh completion script\n";
+
std::cout << " --generate-fish-completion Generate Fish completion script\n";
+
std::cout << " --man Generate man page\n\n";
+
std::cout << "For detailed information, field descriptions, and examples, run:\n";
+
std::cout << " man " << programName << " \n";
+
}
+
+
void generateBashCompletion() {
+
std::cout << R"(
+
_soapdump_completions()
+
{
+
local cur prev opts
+
COMPREPLY=()
+
cur="${COMP_WORDS[COMP_CWORD]}"
+
prev="${COMP_WORDS[COMP_CWORD-1]}"
+
opts="--help --summary --raw --generate-bash-completion --generate-zsh-completion --generate-fish-completion --man"
+
+
if [[ ${cur} == -* ]] ; then
+
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+
return 0
+
fi
+
+
# Complete with log files if not an option
+
if [[ ${cur} != -* ]]; then
+
COMPREPLY=( $(compgen -f -X '!*.log' -- ${cur}) )
+
return 0
+
fi
+
}
+
+
complete -F _soapdump_completions soapdump
+
)" << std::endl;
+
}
+
+
void generateZshCompletion() {
+
std::cout << R"(
+
#compdef soapdump
+
+
_arguments -s -S \
+
'(-h --help)'{-h,--help}'[Show help message]' \
+
'(-s --summary)'{-s,--summary}'[Show summary statistics only]' \
+
'(-r --raw)'{-r,--raw}'[Output raw structured data (default)]' \
+
'--generate-bash-completion[Generate Bash completion script]' \
+
'--generate-zsh-completion[Generate Zsh completion script]' \
+
'--generate-fish-completion[Generate Fish completion script]' \
+
'--man[Generate man page]' \
+
'*:log file:_files -g "*.log"'
+
)" << std::endl;
+
}
+
+
void generateFishCompletion() {
+
std::cout << R"(
+
function __fish_soapdump_no_subcommand
+
set cmd (commandline -opc)
+
if [ (count $cmd) -eq 1 ]
+
return 0
+
end
+
return 1
+
end
+
+
complete -c soapdump -s h -l help -d "Show help message"
+
complete -c soapdump -s s -l summary -d "Show summary statistics only"
+
complete -c soapdump -s r -l raw -d "Output raw structured data"
+
complete -c soapdump -l generate-bash-completion -d "Generate Bash completion script"
+
complete -c soapdump -l generate-zsh-completion -d "Generate Zsh completion script"
+
complete -c soapdump -l generate-fish-completion -d "Generate Fish completion script"
+
complete -c soapdump -l man -d "Generate man page"
+
complete -c soapdump -n "__fish_soapdump_no_subcommand" -a "*.log" -d "Log file"
+
)" << std::endl;
+
}
+
+
void generateManPage() {
+
std::cout << R"(.TH SOAPDUMP 1 "December 2024" "soapdump 1.0" "User Commands"
+
.SH NAME
+
soapdump \- parse PayPal SOAP transaction logs
+
.SH SYNOPSIS
+
.B soapdump
+
[\fIOPTIONS\fR] \fIlogfile\fR
+
.SH DESCRIPTION
+
.B soapdump
+
parses PayPal SOAP log files to extract transaction data. It reads log entries containing XML request/response pairs and outputs structured transaction information in pipe-delimited format suitable for further processing with standard Unix tools.
+
+
The program matches SOAP requests with their corresponding responses to provide complete transaction records including customer information, payment details, and processing results.
+
.SH OPTIONS
+
.TP
+
.BR \-h ", " \-\-help
+
Display help information and exit
+
.TP
+
.BR \-s ", " \-\-summary
+
Display summary statistics instead of raw transaction data
+
.TP
+
.BR \-r ", " \-\-raw
+
Output raw transaction data in pipe-delimited format (default behavior)
+
.TP
+
.BR \-\-generate-bash-completion
+
Output bash shell completion script
+
.TP
+
.BR \-\-generate-zsh-completion
+
Output zsh shell completion script
+
.TP
+
.BR \-\-generate-fish-completion
+
Output fish shell completion script
+
.TP
+
.BR \-\-man
+
Output this manual page in troff format
+
.SH OUTPUT FORMAT
+
By default, transactions are output one per line with pipe-separated fields:
+
+
TRANS_NUM|AMOUNT|CURRENCY|FIRSTNAME|LASTNAME|STREET|CITY|STATE|ZIP|CCTYPE|CCLAST4|EXPMONTH|EXPYEAR|CVV|TRANSID|STATUS|CORRID|PROC_AMOUNT
+
+
Fields may be empty if not present in the source data.
+
.SH EXAMPLES
+
Parse a log file and display all transactions:
+
.RS
+
.B soapdump paypal.log
+
.RE
+
+
Show only successful transactions:
+
.RS
+
.B soapdump paypal.log | grep '|Success|'
+
.RE
+
+
Count transactions by state:
+
.RS
+
.B soapdump paypal.log | cut -d'|' -f8 | sort | uniq -c | sort -rn
+
.RE
+
+
Find the largest transaction amount:
+
.RS
+
.B soapdump paypal.log | sort -t'|' -k2 -rn | head -1
+
.RE
+
+
Show transactions over $500:
+
.RS
+
.B soapdump paypal.log | awk -F'|' '$2 > 500'
+
.RE
+
+
Display summary statistics:
+
.RS
+
.B soapdump --summary paypal.log
+
.RE
+
.SH FILES
+
The input file should contain PayPal SOAP API log entries with request and response XML data.
+
.SH AUTHOR
+
Written by Kieran Klukas.
+
.SH REPORTING BUGS
+
Report bugs to <me@dunkirk.sh>
+
.SH COPYRIGHT
+
Copyright \(co 2024 Kieran Klukas.
+
License MIT: <https://opensource.org/licenses/MIT>
+
.br
+
This is free software: you are free to change and redistribute it.
+
There is NO WARRANTY, to the extent permitted by law.
+
)" << std::endl;
+
}
+
+
std::string extractXmlValue(const std::string& xml, const std::string& tag) {
+
std::regex pattern("<" + tag + "(?:[^>]*)>([^<]*)</" + tag + ">");
+
std::smatch match;
+
if (std::regex_search(xml, match, pattern) && match.size() > 1) {
+
return match[1].str();
+
}
+
return "";
+
}
+
+
std::string extractXmlAttribute(const std::string& xml, const std::string& attribute) {
+
std::regex pattern(attribute + "=\"([^\"]*)\"");
+
std::smatch match;
+
if (std::regex_search(xml, match, pattern) && match.size() > 1) {
+
return match[1].str();
+
}
+
return "";
+
}
+
+
std::vector<std::string> extractRequests(const std::string& logContent) {
+
std::vector<std::string> requests;
+
std::regex pattern("PPAPIService: Request: (.*)");
+
+
std::string::const_iterator searchStart(logContent.cbegin());
+
std::smatch match;
+
while (std::regex_search(searchStart, logContent.cend(), match, pattern)) {
+
if (match.size() > 1) {
+
requests.push_back(match[1].str());
+
}
+
searchStart = match.suffix().first;
+
}
+
+
return requests;
+
}
+
+
std::vector<std::string> extractResponses(const std::string& logContent) {
+
std::vector<std::string> responses;
+
std::regex pattern("PPAPIService: Response: <\\?.*\\?>(.*)");
+
+
std::string::const_iterator searchStart(logContent.cbegin());
+
std::smatch match;
+
while (std::regex_search(searchStart, logContent.cend(), match, pattern)) {
+
if (match.size() > 1) {
+
responses.push_back(match[1].str());
+
}
+
searchStart = match.suffix().first;
+
}
+
+
return responses;
+
}
+
+
std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls) {
+
std::vector<Response> responses;
+
+
for (const auto& xml : responseXmls) {
+
Response response;
+
response.transId = extractXmlValue(xml, "TransactionID");
+
response.status = extractXmlValue(xml, "Ack");
+
response.corrId = extractXmlValue(xml, "CorrelationID");
+
response.procAmount = extractXmlValue(xml, "Amount");
+
+
responses.push_back(response);
+
}
+
+
return responses;
+
}
+
+
std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses) {
+
std::vector<Transaction> transactions;
+
int transNum = 1;
+
+
for (size_t i = 0; i < requestXmls.size(); ++i) {
+
const auto& xml = requestXmls[i];
+
+
Transaction transaction;
+
transaction.transNum = transNum++;
+
+
// Extract request fields
+
transaction.amount = extractXmlValue(xml, "ebl:OrderTotal");
+
transaction.currency = extractXmlAttribute(xml, "currencyID");
+
transaction.firstName = extractXmlValue(xml, "ebl:FirstName");
+
transaction.lastName = extractXmlValue(xml, "ebl:LastName");
+
transaction.street = extractXmlValue(xml, "ebl:Street1");
+
transaction.city = extractXmlValue(xml, "ebl:CityName");
+
transaction.state = extractXmlValue(xml, "ebl:StateOrProvince");
+
transaction.zip = extractXmlValue(xml, "ebl:PostalCode");
+
transaction.ccType = extractXmlValue(xml, "ebl:CreditCardType");
+
transaction.ccLast4 = extractXmlValue(xml, "ebl:CreditCardLastFourDigits");
+
transaction.expMonth = extractXmlValue(xml, "ebl:ExpMonth");
+
transaction.expYear = extractXmlValue(xml, "ebl:ExpYear");
+
transaction.cvv = extractXmlValue(xml, "ebl:CVV2");
+
+
// Get corresponding response data
+
if (i < responses.size()) {
+
transaction.transId = responses[i].transId;
+
transaction.status = responses[i].status;
+
transaction.corrId = responses[i].corrId;
+
transaction.procAmount = responses[i].procAmount;
+
}
+
+
transactions.push_back(transaction);
+
}
+
+
return transactions;
+
}
+
+
void outputRawData(const std::vector<Transaction>& transactions) {
+
for (const auto& t : transactions) {
+
std::cout << t.transNum << "|"
+
<< t.amount << "|"
+
<< t.currency << "|"
+
<< t.firstName << "|"
+
<< t.lastName << "|"
+
<< t.street << "|"
+
<< t.city << "|"
+
<< t.state << "|"
+
<< t.zip << "|"
+
<< t.ccType << "|"
+
<< t.ccLast4 << "|"
+
<< t.expMonth << "|"
+
<< t.expYear << "|"
+
<< t.cvv << "|"
+
<< t.transId << "|"
+
<< t.status << "|"
+
<< t.corrId << "|"
+
<< t.procAmount << std::endl;
+
}
+
}
+
+
void outputSummary(const std::vector<Transaction>& transactions) {
+
std::cout << "=== SUMMARY ===" << std::endl;
+
+
// Count transactions
+
int total = transactions.size();
+
int successful = std::count_if(transactions.begin(), transactions.end(),
+
[](const Transaction& t) { return t.status == "Success"; });
+
+
std::cout << "Total Transactions: " << total << std::endl;
+
std::cout << "Successful: " << successful << std::endl;
+
std::cout << "Failed: " << (total - successful) << std::endl;
+
std::cout << std::endl;
+
+
// Top 5 states
+
std::map<std::string, int> stateCounts;
+
for (const auto& t : transactions) {
+
stateCounts[t.state]++;
+
}
+
+
std::cout << "Top 5 States by Transaction Count:" << std::endl;
+
std::vector<std::pair<std::string, int>> stateCountVec(stateCounts.begin(), stateCounts.end());
+
std::sort(stateCountVec.begin(), stateCountVec.end(),
+
[](const auto& a, const auto& b) { return a.second > b.second; });
+
+
int count = 0;
+
for (const auto& sc : stateCountVec) {
+
if (count++ >= 5) break;
+
std::cout << " " << sc.first << ": " << sc.second << std::endl;
+
}
+
std::cout << std::endl;
+
+
// Transaction amount stats
+
std::vector<double> amounts;
+
for (const auto& t : transactions) {
+
try {
+
amounts.push_back(std::stod(t.amount));
+
} catch (...) {
+
// Skip invalid amounts
+
}
+
}
+
+
if (!amounts.empty()) {
+
double totalAmount = std::accumulate(amounts.begin(), amounts.end(), 0.0);
+
double largest = *std::max_element(amounts.begin(), amounts.end());
+
double smallest = *std::min_element(amounts.begin(), amounts.end());
+
+
std::cout << "Transaction Amount Stats:" << std::endl;
+
std::cout << " Total: $" << std::fixed << std::setprecision(2) << totalAmount << std::endl;
+
std::cout << " Largest: $" << std::fixed << std::setprecision(2) << largest << std::endl;
+
std::cout << " Smallest: $" << std::fixed << std::setprecision(2) << smallest << std::endl;
+
}
+
}
-352
src/transaction-parser.cpp
···
-
#include <cstdio>
-
#include <iostream>
-
#include <fstream>
-
#include <string>
-
#include <vector>
-
#include <map>
-
#include <regex>
-
#include <algorithm>
-
#include <numeric>
-
#include <iomanip>
-
#include <getopt.h>
-
-
// Transaction data structure
-
struct Transaction {
-
int transNum;
-
std::string amount;
-
std::string currency;
-
std::string firstName;
-
std::string lastName;
-
std::string street;
-
std::string city;
-
std::string state;
-
std::string zip;
-
std::string ccType;
-
std::string ccLast4;
-
std::string expMonth;
-
std::string expYear;
-
std::string cvv;
-
std::string transId;
-
std::string status;
-
std::string corrId;
-
std::string procAmount;
-
};
-
-
// Response data structure
-
struct Response {
-
std::string transId;
-
std::string status;
-
std::string corrId;
-
std::string procAmount;
-
};
-
-
// Function prototypes
-
void showHelp(const char* programName);
-
std::string extractXmlValue(const std::string& xml, const std::string& tag);
-
std::string extractXmlAttribute(const std::string& xml, const std::string& attribute);
-
std::vector<std::string> extractRequests(const std::string& logContent);
-
std::vector<std::string> extractResponses(const std::string& logContent);
-
std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls);
-
std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses);
-
void outputRawData(const std::vector<Transaction>& transactions);
-
void outputSummary(const std::vector<Transaction>& transactions);
-
-
int main(int argc, char* argv[]) {
-
// Default options
-
bool summaryOnly = false;
-
std::string logFile;
-
-
// Parse command line options
-
static struct option longOptions[] = {
-
{"help", no_argument, 0, 'h'},
-
{"summary", no_argument, 0, 's'},
-
{"raw", no_argument, 0, 'r'},
-
{0, 0, 0, 0}
-
};
-
-
int optionIndex = 0;
-
int opt;
-
while ((opt = getopt_long(argc, argv, "hsr", longOptions, &optionIndex)) != -1) {
-
switch (opt) {
-
case 'h':
-
showHelp(argv[0]);
-
return 0;
-
case 's':
-
summaryOnly = true;
-
break;
-
case 'r':
-
summaryOnly = false;
-
break;
-
case '?':
-
std::cerr << "Unknown option: " << static_cast<char>(optopt) << std::endl;
-
showHelp(argv[0]);
-
return 1;
-
default:
-
break;
-
}
-
}
-
-
// Get logfile name
-
if (optind < argc) {
-
logFile = argv[optind];
-
} else {
-
std::cerr << "Error: No logfile specified" << std::endl;
-
showHelp(argv[0]);
-
return 1;
-
}
-
-
// Check if file exists
-
std::ifstream file(logFile);
-
if (!file.is_open()) {
-
std::cerr << "Error: File '" << logFile << "' not found" << std::endl;
-
return 1;
-
}
-
-
// Read the entire file
-
std::string logContent((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
-
file.close();
-
-
// Extract requests and responses
-
std::vector<std::string> requestXmls = extractRequests(logContent);
-
std::vector<std::string> responseXmls = extractResponses(logContent);
-
-
// Parse responses
-
std::vector<Response> responses = parseResponses(responseXmls);
-
-
// Parse transactions
-
std::vector<Transaction> transactions = parseTransactions(requestXmls, responses);
-
-
// Output data
-
if (summaryOnly) {
-
outputSummary(transactions);
-
} else {
-
outputRawData(transactions);
-
}
-
-
return 0;
-
}
-
-
void showHelp(const char* programName) {
-
std::cout << "PayPal SOAP Log Parser\n\n";
-
std::cout << "USAGE:\n";
-
std::cout << " " << programName << " [OPTIONS] <logfile>\n\n";
-
std::cout << "OPTIONS:\n";
-
std::cout << " -h, --help Show this help message\n";
-
std::cout << " -s, --summary Show summary statistics only\n";
-
std::cout << " -r, --raw Output raw structured data (default)\n\n";
-
std::cout << "OUTPUT FORMAT:\n";
-
std::cout << " TRANS_NUM|AMOUNT|CURRENCY|FIRSTNAME|LASTNAME|STREET|CITY|STATE|ZIP|CCTYPE|CCLAST4|EXPMONTH|EXPYEAR|CVV|TRANSID|STATUS|CORRID|PROC_AMOUNT\n\n";
-
std::cout << "FIELD DESCRIPTIONS:\n";
-
std::cout << " TRANS_NUM - Transaction sequence number\n";
-
std::cout << " AMOUNT - Order total amount\n";
-
std::cout << " CURRENCY - Currency code (USD, etc)\n";
-
std::cout << " FIRSTNAME - Customer first name\n";
-
std::cout << " LASTNAME - Customer last name\n";
-
std::cout << " STREET - Street address\n";
-
std::cout << " CITY - City name\n";
-
std::cout << " STATE - State/Province code\n";
-
std::cout << " ZIP - Postal code\n";
-
std::cout << " CCTYPE - Credit card type (Visa, MasterCard, etc)\n";
-
std::cout << " CCLAST4 - Last 4 digits of credit card\n";
-
std::cout << " EXPMONTH - Card expiration month\n";
-
std::cout << " EXPYEAR - Card expiration year\n";
-
std::cout << " CVV - CVV code\n";
-
std::cout << " TRANSID - PayPal transaction ID\n";
-
std::cout << " STATUS - Transaction status (Success/Failure)\n";
-
std::cout << " CORRID - Correlation ID\n";
-
std::cout << " PROC_AMOUNT - Actually processed amount\n\n";
-
std::cout << "EXAMPLES:\n";
-
std::cout << " # Get all transactions\n";
-
std::cout << " " << programName << " payments.log\n\n";
-
std::cout << " # Get only successful transactions\n";
-
std::cout << " " << programName << " payments.log | grep Success\n\n";
-
std::cout << " # Count transactions by state\n";
-
std::cout << " " << programName << " payments.log | cut -d'|' -f8 | sort | uniq -c | sort -nr\n\n";
-
std::cout << " # Find largest transaction\n";
-
std::cout << " " << programName << " payments.log | sort -t'|' -k2 -nr | head -1\n\n";
-
std::cout << " # Get transactions over $500\n";
-
std::cout << " " << programName << " payments.log | awk -F'|' '$2 > 500'\n\n";
-
std::cout << " # Summary stats\n";
-
std::cout << " " << programName << " -s payments.log\n";
-
}
-
-
std::string extractXmlValue(const std::string& xml, const std::string& tag) {
-
std::regex pattern("<" + tag + "(?:[^>]*)>([^<]*)</" + tag + ">");
-
std::smatch match;
-
if (std::regex_search(xml, match, pattern) && match.size() > 1) {
-
return match[1].str();
-
}
-
return "";
-
}
-
-
std::string extractXmlAttribute(const std::string& xml, const std::string& attribute) {
-
std::regex pattern(attribute + "=\"([^\"]*)\"");
-
std::smatch match;
-
if (std::regex_search(xml, match, pattern) && match.size() > 1) {
-
return match[1].str();
-
}
-
return "";
-
}
-
-
std::vector<std::string> extractRequests(const std::string& logContent) {
-
std::vector<std::string> requests;
-
std::regex pattern("PPAPIService: Request: (.*)");
-
-
std::string::const_iterator searchStart(logContent.cbegin());
-
std::smatch match;
-
while (std::regex_search(searchStart, logContent.cend(), match, pattern)) {
-
if (match.size() > 1) {
-
requests.push_back(match[1].str());
-
}
-
searchStart = match.suffix().first;
-
}
-
-
return requests;
-
}
-
-
std::vector<std::string> extractResponses(const std::string& logContent) {
-
std::vector<std::string> responses;
-
std::regex pattern("PPAPIService: Response: <\\?.*\\?>(.*)");
-
-
std::string::const_iterator searchStart(logContent.cbegin());
-
std::smatch match;
-
while (std::regex_search(searchStart, logContent.cend(), match, pattern)) {
-
if (match.size() > 1) {
-
responses.push_back(match[1].str());
-
}
-
searchStart = match.suffix().first;
-
}
-
-
return responses;
-
}
-
-
std::vector<Response> parseResponses(const std::vector<std::string>& responseXmls) {
-
std::vector<Response> responses;
-
-
for (const auto& xml : responseXmls) {
-
Response response;
-
response.transId = extractXmlValue(xml, "TransactionID");
-
response.status = extractXmlValue(xml, "Ack");
-
response.corrId = extractXmlValue(xml, "CorrelationID");
-
response.procAmount = extractXmlValue(xml, "Amount");
-
-
responses.push_back(response);
-
}
-
-
return responses;
-
}
-
-
std::vector<Transaction> parseTransactions(const std::vector<std::string>& requestXmls, const std::vector<Response>& responses) {
-
std::vector<Transaction> transactions;
-
int transNum = 1;
-
-
for (size_t i = 0; i < requestXmls.size(); ++i) {
-
const auto& xml = requestXmls[i];
-
-
Transaction transaction;
-
transaction.transNum = transNum++;
-
-
// Extract request fields
-
transaction.amount = extractXmlValue(xml, "ebl:OrderTotal");
-
transaction.currency = extractXmlAttribute(xml, "currencyID");
-
transaction.firstName = extractXmlValue(xml, "ebl:FirstName");
-
transaction.lastName = extractXmlValue(xml, "ebl:LastName");
-
transaction.street = extractXmlValue(xml, "ebl:Street1");
-
transaction.city = extractXmlValue(xml, "ebl:CityName");
-
transaction.state = extractXmlValue(xml, "ebl:StateOrProvince");
-
transaction.zip = extractXmlValue(xml, "ebl:PostalCode");
-
transaction.ccType = extractXmlValue(xml, "ebl:CreditCardType");
-
transaction.ccLast4 = extractXmlValue(xml, "ebl:CreditCardLastFourDigits");
-
transaction.expMonth = extractXmlValue(xml, "ebl:ExpMonth");
-
transaction.expYear = extractXmlValue(xml, "ebl:ExpYear");
-
transaction.cvv = extractXmlValue(xml, "ebl:CVV2");
-
-
// Get corresponding response data
-
if (i < responses.size()) {
-
transaction.transId = responses[i].transId;
-
transaction.status = responses[i].status;
-
transaction.corrId = responses[i].corrId;
-
transaction.procAmount = responses[i].procAmount;
-
}
-
-
transactions.push_back(transaction);
-
}
-
-
return transactions;
-
}
-
-
void outputRawData(const std::vector<Transaction>& transactions) {
-
for (const auto& t : transactions) {
-
std::cout << t.transNum << "|"
-
<< t.amount << "|"
-
<< t.currency << "|"
-
<< t.firstName << "|"
-
<< t.lastName << "|"
-
<< t.street << "|"
-
<< t.city << "|"
-
<< t.state << "|"
-
<< t.zip << "|"
-
<< t.ccType << "|"
-
<< t.ccLast4 << "|"
-
<< t.expMonth << "|"
-
<< t.expYear << "|"
-
<< t.cvv << "|"
-
<< t.transId << "|"
-
<< t.status << "|"
-
<< t.corrId << "|"
-
<< t.procAmount << std::endl;
-
}
-
}
-
-
void outputSummary(const std::vector<Transaction>& transactions) {
-
std::cout << "=== SUMMARY ===" << std::endl;
-
-
// Count transactions
-
int total = transactions.size();
-
int successful = std::count_if(transactions.begin(), transactions.end(),
-
[](const Transaction& t) { return t.status == "Success"; });
-
-
std::cout << "Total Transactions: " << total << std::endl;
-
std::cout << "Successful: " << successful << std::endl;
-
std::cout << "Failed: " << (total - successful) << std::endl;
-
std::cout << std::endl;
-
-
// Top 5 states
-
std::map<std::string, int> stateCounts;
-
for (const auto& t : transactions) {
-
stateCounts[t.state]++;
-
}
-
-
std::cout << "Top 5 States by Transaction Count:" << std::endl;
-
std::vector<std::pair<std::string, int>> stateCountVec(stateCounts.begin(), stateCounts.end());
-
std::sort(stateCountVec.begin(), stateCountVec.end(),
-
[](const auto& a, const auto& b) { return a.second > b.second; });
-
-
int count = 0;
-
for (const auto& sc : stateCountVec) {
-
if (count++ >= 5) break;
-
std::cout << " " << sc.first << ": " << sc.second << std::endl;
-
}
-
std::cout << std::endl;
-
-
// Transaction amount stats
-
std::vector<double> amounts;
-
for (const auto& t : transactions) {
-
try {
-
amounts.push_back(std::stod(t.amount));
-
} catch (...) {
-
// Skip invalid amounts
-
}
-
}
-
-
if (!amounts.empty()) {
-
double totalAmount = std::accumulate(amounts.begin(), amounts.end(), 0.0);
-
double largest = *std::max_element(amounts.begin(), amounts.end());
-
double smallest = *std::min_element(amounts.begin(), amounts.end());
-
-
std::cout << "Transaction Amount Stats:" << std::endl;
-
std::cout << " Total: $" << std::fixed << std::setprecision(2) << totalAmount << std::endl;
-
std::cout << " Largest: $" << std::fixed << std::setprecision(2) << largest << std::endl;
-
std::cout << " Smallest: $" << std::fixed << std::setprecision(2) << smallest << std::endl;
-
}
-
}