diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..b85ab44 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,196 @@ +# GitHub Actions Workflows + +This directory contains GitHub Actions workflows for automating builds, tests, and releases. + +## Workflows + +### 1. CI (Continuous Integration) + +**File:** `ci.yml` + +**Triggers:** +- Push to `main` or `master` branches +- Pull requests to `main` or `master` branches + +**What it does:** +- Tests on multiple Node.js versions (16.x, 18.x, 20.x) +- Builds data files +- Validates JSON files +- Runs JavaScript tests +- Tests Python and Ruby examples + +### 2. Data Release + +**File:** `data-release.yml` + +**Triggers:** +- Push to `main` or `master` branches +- Only when changes are made to `data/sources/**` or `scripts/build.js` + +**What it does:** +- Builds and validates data files +- Automatically increments patch version in root package.json +- Creates GitHub release with tag `data-vX.Y.Z` +- Attaches all data files (brands.json, compiled/brands.json, sources/*.json) + +**Release Assets:** +- `data/brands.json` - Legacy format +- `data/compiled/brands.json` - Enhanced format +- `data/sources/*.json` - Source files + +### 3. Publish NPM Package + +**File:** `publish-npm.yml` + +**Triggers:** +- Manual dispatch from GitHub Actions UI + +**What it does:** +- Updates the JavaScript library version +- Runs tests +- Publishes to NPM registry +- Creates a Git tag (e.g., `v1.2.0`) +- Creates a GitHub release for the library + +**How to use:** +1. Go to Actions tab in GitHub +2. Select "Publish NPM Package" workflow +3. Click "Run workflow" +4. Enter version number (e.g., `1.2.0`) +5. Select NPM tag (`latest`, `beta`, `next`) +6. Click "Run workflow" + +## Architecture + +This project separates data releases from library releases: + +### Data Releases (GitHub Releases) +- Tagged as `data-vX.Y.Z` +- Contains BIN pattern data +- Updated when `data/sources/**` changes +- Downloaded automatically by the JavaScript library + +### Library Releases (NPM) +- Tagged as `vX.Y.Z` +- Published to NPM +- Contains validation logic +- Downloads data from GitHub releases on install + +## Setting Up + +### For Data Releases +No setup required - automatic when data sources are updated. + +### For NPM Publishing +Add `NPM_TOKEN` secret to repository: +1. Generate token at https://www.npmjs.com/settings/YOUR_USERNAME/tokens +2. Go to repository Settings → Secrets → Actions +3. Add secret named `NPM_TOKEN` + +## Release Process + +### Updating Data +```bash +# 1. Edit source files +vim data/sources/visa.json + +# 2. Build and commit +npm run build +git add data/ +git commit -m "Update Visa BIN patterns" +git push + +# 3. GitHub Actions automatically: +# - Builds data +# - Creates release (data-v2.0.2) +# - Attaches data files +``` + +### Publishing Library +```bash +# 1. Go to Actions → Publish NPM Package +# 2. Click "Run workflow" +# 3. Enter version: 1.3.0 +# 4. Select tag: latest +# 5. Click "Run workflow" + +# GitHub Actions automatically: +# - Updates package.json +# - Runs tests +# - Publishes to NPM +# - Creates release (v1.3.0) +``` + +## Setting Up + +These workflows run automatically once the repository is on GitHub. No additional setup is required. + +## Artifacts + +Build artifacts (compiled data files) are stored for 30 days and can be downloaded from: +- Actions tab → Select a workflow run → Artifacts section + +## Release Assets + +Each release includes: +- `data/brands.json` - Legacy format +- `data/compiled/brands.json` - Enhanced format +- Source files from `data/sources/` (manual releases only) + +## Environment Requirements + +- Node.js 16.x, 18.x, or 20.x +- Python 3.x (for examples) +- Ruby 2.5+ (for examples) + +## Customization + +To modify the workflows: + +1. **Change Node.js versions:** + Edit the `matrix.node-version` in `ci.yml` + +2. **Change version bumping strategy:** + Edit the version calculation in `build-and-release.yml` + +3. **Add more tests:** + Add steps to any workflow file + +## Troubleshooting + +### Failed Builds + +If a build fails: +1. Check the workflow logs in the Actions tab +2. Run `npm run build` locally to reproduce +3. Fix issues in source files +4. Push changes + +### Failed Tests + +If tests fail: +1. Check test logs in the workflow output +2. Run tests locally: `cd libs/javascript && npm test` +3. Fix test failures +4. Push changes + +### Release Not Created + +If an automatic release wasn't created: +1. Check if there were actually changes in `data/sources/` +2. Verify the workflow completed successfully +3. Check workflow logs for errors +4. Use manual release as fallback + +## Security + +- Workflows use `GITHUB_TOKEN` which is automatically provided +- No additional secrets required +- Token has write permissions for releases and tags + +## Best Practices + +1. **Test locally first:** Always run `npm run build` and `npm test` before pushing +2. **Use descriptive commits:** Help the auto-release generate good release notes +3. **Manual releases for major versions:** Use manual workflow for version 3.0.0, etc. +4. **Review release notes:** Edit release notes after auto-creation if needed diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9d31483 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x, 18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install root dependencies + run: npm install --ignore-scripts + + - name: Build data files + run: npm run build + + - name: Validate build output + run: | + if [ ! -f data/compiled/brands.json ]; then + echo "Error: compiled/brands.json not found" + exit 1 + fi + echo "✓ Build output validated" + + - name: Install JavaScript library dependencies + run: | + cd libs/javascript + npm install + + - name: Run JavaScript tests + run: | + cd libs/javascript + npm test + + - name: Test Python example + run: | + python3 --version + python3 examples/python/credit_card_validator.py + + - name: Test Ruby example + run: | + ruby --version + ruby examples/ruby/credit_card_validator.rb + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Check JSON validity + run: | + for file in data/sources/*.json data/brands.json data/compiled/brands.json; do + if [ -f "$file" ]; then + echo "Checking $file..." + node -e "JSON.parse(require('fs').readFileSync('$file', 'utf8'))" + fi + done + echo "✓ All JSON files are valid" + + - name: Check for required files + run: | + required_files=( + "data/brands.json" + "data/SCHEMA.md" + "data/README.md" + "CONTRIBUTING.md" + "scripts/build.js" + ) + for file in "${required_files[@]}"; do + if [ ! -f "$file" ]; then + echo "Error: Required file $file not found" + exit 1 + fi + done + echo "✓ All required files present" diff --git a/.github/workflows/data-release.yml b/.github/workflows/data-release.yml new file mode 100644 index 0000000..9dc7ca3 --- /dev/null +++ b/.github/workflows/data-release.yml @@ -0,0 +1,106 @@ +name: Data Release + +on: + push: + branches: + - main + - master + paths: + - 'data/sources/**' + - 'scripts/build.js' + +jobs: + build-and-release-data: + name: Build and Release Data + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Install dependencies + run: npm install --ignore-scripts + + - name: Build data files + run: npm run build + + - name: Get current data version + id: get_version + run: | + # Get the current version from root package.json + CURRENT_VERSION=$(node -p "require('./package.json').version") + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + + # Increment patch version for data + IFS='.' read -ra VERSION_PARTS <<< "$CURRENT_VERSION" + MAJOR=${VERSION_PARTS[0]} + MINOR=${VERSION_PARTS[1]} + PATCH=${VERSION_PARTS[2]} + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "New data version: $NEW_VERSION" + + - name: Update root package.json version + run: | + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); + pkg.version = '${{ steps.get_version.outputs.new_version }}'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); + " + + - name: Commit version bump + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add package.json + git commit -m "chore: bump data version to ${{ steps.get_version.outputs.new_version }}" || echo "No changes to commit" + git push || echo "No changes to push" + + - name: Create Data Release + uses: softprops/action-gh-release@v1 + with: + tag_name: data-v${{ steps.get_version.outputs.new_version }} + name: Data Release v${{ steps.get_version.outputs.new_version }} + body: | + ## Credit Card BIN Data Release + + This release contains updated credit card BIN patterns and validation data. + + ### Data Files + - `brands.json` - Legacy format (backward compatible) + - `compiled/brands.json` - Enhanced format with detailed metadata + - `sources/*.json` - Source data files + + ### Usage + + **Download data programmatically:** + ```bash + curl -L https://github.com/renatovico/bin-cc/releases/download/data-v${{ steps.get_version.outputs.new_version }}/brands.json -o brands.json + ``` + + **In JavaScript libraries:** + ```javascript + // The creditcard-identifier npm package automatically downloads + // the latest data from GitHub releases + const cc = require('creditcard-identifier'); + ``` + + ### Changes + See commit history for details on what BIN patterns were updated. + files: | + data/brands.json + data/compiled/brands.json + data/sources/*.json + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml new file mode 100644 index 0000000..94374ec --- /dev/null +++ b/.github/workflows/publish-npm.yml @@ -0,0 +1,88 @@ +name: Publish NPM Package + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to publish (e.g., 1.2.0)' + required: true + type: string + npm_tag: + description: 'NPM tag (latest, beta, next)' + required: false + default: 'latest' + type: string + +jobs: + publish-npm: + name: Publish to NPM + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + registry-url: 'https://registry.npmjs.org' + + - name: Update package version + run: | + cd libs/javascript + npm version ${{ github.event.inputs.version }} --no-git-tag-version + + - name: Install dependencies + run: | + cd libs/javascript + npm install + + - name: Run tests + run: | + cd libs/javascript + npm test + + - name: Publish to NPM + run: | + cd libs/javascript + npm publish --tag ${{ github.event.inputs.npm_tag }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create Git tag + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git tag -a "v${{ github.event.inputs.version }}" -m "Release v${{ github.event.inputs.version }}" + git push origin "v${{ github.event.inputs.version }}" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: v${{ github.event.inputs.version }} + name: JavaScript Library v${{ github.event.inputs.version }} + body: | + ## creditcard-identifier v${{ github.event.inputs.version }} + + Published to NPM: https://www.npmjs.com/package/creditcard-identifier + + ### Installation + ```bash + npm install creditcard-identifier@${{ github.event.inputs.version }} + ``` + + ### Features + - Credit card brand identification + - BIN pattern validation + - Luhn algorithm validation + - Automatic data updates from GitHub releases + + ### Data + This library automatically downloads the latest BIN data from GitHub releases. + + See the [data releases](https://github.com/renatovico/bin-cc/releases?q=data-v) for data version history. + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2e4ec9b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,330 @@ +# Contributing to bin-cc + +Thank you for your interest in contributing to bin-cc! This document provides guidelines for contributing card BIN data and code improvements. + +## 🎯 Project Philosophy + +bin-cc is a **data file project** similar to [browserslist](https://github.com/browserslist/browserslist) and [tzdata](https://www.iana.org/time-zones): +- **Data is the product** - We maintain authoritative BIN patterns +- **Source of truth** - Other libraries depend on our data +- **Community-driven** - Contributions keep data accurate and up-to-date +- **Multi-language** - Implementations exist in many programming languages + +## 📊 Contributing Data + +### Adding a New Card Scheme + +1. **Create a source file** in `data/sources/` + + ```bash + # Example: adding JCB + touch data/sources/jcb.json + ``` + +2. **Follow the schema** (see `data/SCHEMA.md`) + + ```json + { + "scheme": "jcb", + "brand": "JCB", + "patterns": [ + { + "bin": "^35", + "length": [16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"] + } + ``` + + **Optional: Add detailed BIN information** + + You can include specific BIN-level details for known issuers: + + ```json + { + "scheme": "visa", + "brand": "Visa", + "patterns": [ + { + "bin": "^4", + "length": [13, 16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"], + "bins": [ + { + "bin": "491441", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491414", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO DO ESTADO DO PARANA" + } + ] + } + ``` + + The `bins` array is optional and provides: + - **bin**: 6-digit BIN number + - **type**: Card type (CREDIT, DEBIT) + - **category**: Card tier (CLASSIC, GOLD, PLATINUM, etc.) - use `null` if not applicable + - **issuer**: Name of the issuing bank + +3. **Build and validate** + + ```bash + node scripts/build.js + ``` + +4. **Test the changes** + + ```bash + cd libs/javascript + npm test + ``` + +5. **Document your source** + - In your PR description, cite official documentation + - Include URLs to BIN databases or official specs + - Explain how you verified the patterns + +### Updating Existing Patterns + +1. **Edit the source file** in `data/sources/` +2. **Add a comment** explaining the change +3. **Rebuild** with `node scripts/build.js` +4. **Run tests** to ensure nothing breaks +5. **Document** the reason for the update in your PR + +### Example: Updating Visa Patterns + +```json +{ + "scheme": "visa", + "brand": "Visa", + "patterns": [ + { + "bin": "^4", + "length": [13, 16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^6367", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "Added: Visa Denmark/Dankort co-brand cards" + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} +``` + +## 💻 Contributing Code + +### Adding Language Implementations + +We welcome implementations in any programming language! + +1. **Create a directory** in `examples/` for your language +2. **Implement the validator** that reads from `data/brands.json` +3. **Include a README** with usage instructions +4. **Test your implementation** with sample cards +5. **Submit a PR** with your code + +### Improving the Build System + +1. **Fork the repository** +2. **Create a feature branch** + ```bash + git checkout -b feature/improve-build-validation + ``` +3. **Make your changes** to `scripts/build.js` or related files +4. **Test thoroughly** + ```bash + node scripts/build.js + # Verify output in data/compiled/ and data/brands.json + ``` +5. **Submit a PR** with clear description + +## ✅ Data Quality Guidelines + +### Required Documentation + +Every data contribution must include: + +1. **Source citation** + - Official card network documentation + - Published BIN ranges + - ISO standards + - Verified test cards + +2. **Verification method** + - How you tested the patterns + - Sample (anonymized) BINs that match + - Any edge cases discovered + +3. **Change justification** + - Why this change is necessary + - What problem it solves + - Impact on existing patterns + +### Data Accuracy Standards + +- ✅ Use official sources when available +- ✅ Test patterns with real (anonymized) BINs +- ✅ Include edge cases in patterns +- ✅ Document limitations or uncertainties +- ❌ Don't guess or assume patterns +- ❌ Don't use unverified third-party sources +- ❌ Don't break backward compatibility without discussion + +## 🔍 Review Process + +### What We Look For + +1. **Accuracy** - Patterns must be correct and verified +2. **Completeness** - All required fields present +3. **Documentation** - Sources and reasoning provided +4. **Testing** - Changes don't break existing functionality +5. **Format** - Follows schema and style guidelines + +### Timeline + +- Initial review: Within 1 week +- Follow-up questions: Please respond within 2 weeks +- Merge: After approval and successful CI checks + +## 🚀 Development Workflow + +### Setup + +```bash +# Clone the repository +git clone https://github.com/renatovico/bin-cc.git +cd bin-cc + +# Install JavaScript dependencies (for testing) +cd libs/javascript +npm install +cd ../.. +``` + +### Making Changes + +```bash +# Create a branch +git checkout -b update/visa-patterns + +# Edit source files +vim data/sources/visa.json + +# Build +node scripts/build.js + +# Test (JavaScript) +cd libs/javascript +npm test +cd ../.. + +# Commit with descriptive message +git add data/sources/visa.json +git commit -m "Update Visa BIN patterns for Dankort co-brand" + +# Push and create PR +git push origin update/visa-patterns +``` + +### Commit Message Guidelines + +- Use present tense ("Add pattern" not "Added pattern") +- Be specific ("Update Visa Dankort BINs" not "Update data") +- Reference issues when applicable ("Fixes #123") +- Keep first line under 72 characters + +## 📝 Pull Request Template + +```markdown +## Description +Brief description of changes + +## Type of Change +- [ ] New card scheme +- [ ] Update existing patterns +- [ ] Bug fix +- [ ] Documentation +- [ ] Code improvement + +## Data Source +- Official docs: [URL] +- Verification: [How you tested] + +## Testing +- [ ] Build script runs successfully +- [ ] Tests pass (if applicable) +- [ ] Validated with sample cards + +## Checklist +- [ ] Follows schema guidelines +- [ ] Includes source documentation +- [ ] Build script passes +- [ ] Tests updated if needed +``` + +## 🐛 Reporting Issues + +### Data Issues + +If you find incorrect BIN patterns: + +1. **Search existing issues** to avoid duplicates +2. **Create a new issue** with: + - Card scheme affected + - Incorrect pattern details + - Correct pattern (if known) + - Source documentation + - Sample BIN that demonstrates the issue + +### Code Issues + +For bugs in implementations or build scripts: + +1. **Describe the bug** clearly +2. **Include steps to reproduce** +3. **Show expected vs actual behavior** +4. **Include environment details** (Node version, OS, etc.) + +## 💬 Questions? + +- Open an issue for data questions +- Check `data/SCHEMA.md` for format questions +- Review existing source files for examples + +## 📜 Code of Conduct + +- Be respectful and professional +- Focus on constructive feedback +- Welcome newcomers and help them learn +- Assume good intentions + +## 🏆 Recognition + +Contributors are listed in: +- Git commit history +- Release notes for significant contributions +- README acknowledgments + +Thank you for contributing to bin-cc! 🎉 diff --git a/README.md b/README.md index d913029..ba7bede 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,154 @@ -Validação para cartão de crédito. +Credit Card BIN Data - Data File Project ==================== -Bin e padrões para validação de cartão de crédito. +**This is a data file project** similar to tzdata, providing credit card BIN (Bank Identification Number) patterns as a source of truth for other libraries. -Repositório para esta [gist](https://gist.github.com/erikhenrique/5931368) +This repository contains authoritative data about credit card BIN patterns for validation and brand identification, along with reference implementations in multiple programming languages. -Os dados dos cartões: ~~Elo~~, Hipercard desta tabela *não* são oficiais. -Tentei diversas vezes falar com o pessoal dessas bandeiras afim de ter uma informação oficial, porém, é muito difícil falar com o setor técnico e as atendentes nem sabem o que é bin de cartão :( +Repository for this [gist](https://gist.github.com/erikhenrique/5931368) -Esta tabela inicialmente foi montada com coleta de dados de cartões de crédito reais. Onde o usuário colocava o número do cartão de crédito dele e quando não conseguíamos saber qual a banheira pedíamos para que o usuário selecionasse a bandeira e desta forma armazenavamos os primeiros digitos do cartão. +## 📁 Project Structure -### Pull Request, contribua +``` +bin-cc/ +├── data/ # Credit card BIN data +│ ├── sources/ # Source data files (editable) +│ │ ├── visa.json +│ │ ├── mastercard.json +│ │ └── ... +│ ├── compiled/ # Compiled enhanced format +│ │ └── brands.json +│ ├── brands.json # Legacy format (auto-generated) +│ ├── SCHEMA.md # Data schema documentation +│ └── README.md # Data usage guide +│ +├── scripts/ # Build and validation tools +│ └── build.js # Compiles source → compiled data +│ +├── libs/ # Reference implementations +│ └── javascript/ # JavaScript/Node.js implementation +│ +├── examples/ # Usage examples in different languages +│ ├── javascript-example.js +│ ├── python/ +│ ├── elixir/ +│ ├── ruby/ +│ └── dotnet/ +│ +├── CONTRIBUTING.md # Contribution guidelines +└── package.json # Build scripts +``` -Fique a vontade para mandar um PR para que esta tabela permaneça atualizada. Ao fazer o PR por favor, informe como conseguiu essa informação de atualização para que possamos sempre ter dados confiáveis nesta tabela. +## 🎯 Data Source + +The **authoritative data** follows a **build system** similar to browserslist: + +- **Source files** [`data/sources/*.json`](./data/sources) - Human-editable card scheme definitions +- **Build script** [`scripts/build.js`](./scripts/build.js) - Compiles and validates data +- **Compiled data** [`data/compiled/brands.json`](./data/compiled/brands.json) - Enhanced format with full details +- **Legacy data** [`data/brands.json`](./data/brands.json) - Backward-compatible format (auto-generated) +- **Schema docs** [`data/SCHEMA.md`](./data/SCHEMA.md) - Complete schema documentation + +### Data Releases + +Data is released separately from library code: +- **Location**: [GitHub Releases](https://github.com/renatovico/bin-cc/releases?q=data-v) +- **Tagging**: `data-vX.Y.Z` (e.g., `data-v2.0.1`) +- **Automatic**: Releases are created automatically when `data/sources/` changes +- **Files included**: `brands.json`, `compiled/brands.json`, `sources/*.json` + +### Building the Data + +```bash +npm run build +``` + +This compiles source files into both enhanced and legacy formats with validation. + +## 📚 Implementations + +### JavaScript/Node.js +Complete implementation available in [`libs/javascript/`](./libs/javascript/) + +The JavaScript library automatically downloads the latest BIN data from [GitHub releases](https://github.com/renatovico/bin-cc/releases?q=data-v) during installation. + +**Installation:** +```bash +npm install creditcard-identifier +``` + +**Usage:** +```javascript +const cc = require('creditcard-identifier'); +console.log(cc.findBrand('4012001037141112')); // 'visa' +``` + +See [JavaScript documentation](./libs/javascript/README.md) for details. + +### Python +Example implementation in [`examples/python/`](./examples/python/) + +### Elixir +Example implementation in [`examples/elixir/`](./examples/elixir/) + +### Ruby +Example implementation in [`examples/ruby/`](./examples/ruby/) + +### .NET +Example implementation in [`examples/dotnet/`](./examples/dotnet/) + +## 🎴 Supported Card Brands + +| Brand | Starts with | Max length | CVV length | +| ---------- | ------------------------------------------- | ---------- | ---------- | +| Visa | 4, 6367 | 13, 16 | 3 | +| Mastercard | 5, 222100 to 272099 | 16 | 3 | +| Diners | 301, 305, 36, 38 | 14, 16 | 3 | +| Elo | 4011, 401178, 401179, 431274, 438935, etc. | 16 | 3 | +| Amex | 34, 37 | 15 | 4 | +| Discover | 6011, 622, 64, 65 | 16 | 4 | +| Aura | 50 | 16 | 3 | +| Hipercard | 38, 60 | 13, 16, 19 | 3 | + +**Note:** Some Brazilian brands (Elo, Hipercard, Aura) do not have official public documentation. Patterns collected from real-world usage. + +## 🤝 Contributing + +Contributions are welcome! This project follows a **source → build → compiled** workflow: + +1. **Data updates:** Edit source files in [`data/sources/`](./data/sources) +2. **Build:** Run `npm run build` to compile and validate +3. **Test:** Ensure `npm test` passes +4. **Document:** Cite sources in your PR description + +See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for detailed guidelines. + +### Quick Start for Contributors + +```bash +# Edit a source file +vim data/sources/visa.json + +# Build and validate +npm run build + +# Test +npm test + +# Commit changes (both source and generated files) +git add data/sources/visa.json data/brands.json data/compiled/brands.json +git commit -m "Update Visa BIN patterns" +``` + +## 📝 License + +MIT License + +## 👥 Contributors + +- @jotafelipe +- @ahonorato +- @renatoelias | Bandeira | Começa com | Máximo de número | Máximo de número cvc | diff --git a/creditcard-identifier.js b/creditcard-identifier.js deleted file mode 100644 index 9dd2f82..0000000 --- a/creditcard-identifier.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -let brands = [{ - name: 'elo', - regexpBin: '^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175|^627780|^636297|^636368|^(506699|5067[0-6]\\d|50677[0-8])|^(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})|^65003[1-3]|^(65003[5-9]|65004\\d|65005[0-1])|^(65040[5-9]|6504[1-3]\\d)|^(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])|^(65054[1-9]|6505[5-8]\\d|65059[0-8])|^(65070\\d|65071[0-8])|^65072[0-7]|^(65090[1-9]|65091\\d|650920)|^(65165[2-9]|6516[6-7]\\d)|^(65500\\d|65501\\d)|^(65502[1-9]|6550[3-4]\\d|65505[0-8])', - regexpFull: '^(401178|401179|431274|438935|451416|457393|457631|457632|504175|627780|636297|636368|(506699|5067[0-6]\\d|50677[0-8])|(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})|65003[1-3]|(65003[5-9]|65004\\d|65005[0-1])|(65040[5-9]|6504[1-3]\\d)|(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])|(65054[1-9]|6505[5-8]\\d|65059[0-8])|(65070\\d|65071[0-8])|65072[0-7]|(65090[1-9]|65091\\d|650920)|(65165[2-9]|6516[6-7]\\d)|(65500\\d|65501\\d)|(65502[1-9]|6550[3-4]\\d|65505[0-8]))[0-9]{10,12}', - regexpCvv: '^\d{3}$', -}, { - name: 'diners', - regexpBin: '^3(?:0[0-5]|[68][0-9])', - regexpFull: '^3(?:0[0-5]|[68][0-9])[0-9]{11}$', - regexpCvv: '^\d{3}$', -}, { - name: 'discover', - regexpBin: '^6(?:011|5[0-9]{2})', - regexpFull: '^6(?:011|5[0-9]{2})[0-9]{12}$', - regexpCvv: '^\d{3}$', -}, { - name: 'hipercard', - regexpBin: '^3841[046]0|^60', - regexpFull: '^(38[0-9]{17}|60[0-9]{14})$', - regexpCvv: '^\d{3}$', -}, { - name: 'amex', - regexpBin: '^3[47]', - regexpFull: '^3[47][0-9]{13}$', - regexpCvv: '^\d{3,4}$', -}, { - name: 'aura', - regexpBin: '^50[0-9]', - regexpFull: '^50[0-9]{14,17}$', - regexpCvv: '^\d{3}$', -}, { - name: 'mastercard', - regexpBin: '^5[1-5][0-9][0-9]|^2[2-7][0-2][0-9]', - regexpFull: '^5[1-5][0-9]{14}$|^2[2-7][0-2][0-9]{13}$', - regexpCvv: '^\d{3}$', -}, { - name: 'visa', - regexpBin: '^4|^6367', - regexpFull: '^4[0-9]{12}(?:[0-9]{3})?|6367[0-9]{12}$', - regexpCvv: '^\d{3}$', -}, ]; - -function cardNumberFilter(cardNumber, brand) { - if (typeof cardNumber !== 'string') { - throw Error('Card number should be a string'); - } - - return cardNumber.match(brand.regexpFull) !== null; -} - -function cardNameFilter(brandName, brand) { - return brandName === brand.name; -} - -function hipercardRegexp() { - let card = brands.filter(cardNameFilter.bind(this,'hipercard'))[0]; - if (card) { - return new RegExp(card.regexpFull); - } else { - return new RegExp('^$'); - } -} - -function findBrand(cardNumber) { - if(!cardNumber || cardNumber === '') { - cardNumber = '000000'; - } - let brand = brands.filter(cardNumberFilter.bind(this, cardNumber))[0]; - - if (brand === undefined) { - throw Error('card number not supported'); - } - - brand = (brand === undefined) ? undefined : brand.name; - - return brand; -} - -function isSupported(cardNumber) { - let number = cardNumber || '0000000000000001'; - - let supported = false; - let result = brands.filter(cardNumberFilter.bind(this, number))[0]; - if (result !== undefined) { - supported = true; - } - - return supported; -} - -module.exports = { - findBrand: findBrand, - isSupported: isSupported, - hipercardRegexp: hipercardRegexp -} - - diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..30d6762 --- /dev/null +++ b/data/README.md @@ -0,0 +1,148 @@ +# Credit Card BIN Data + +This directory contains credit card BIN (Bank Identification Number) data with a build system similar to browserslist. + +## Directory Structure + +``` +data/ +├── sources/ # Source data files (human-editable) +│ ├── visa.json +│ ├── mastercard.json +│ └── ... +├── compiled/ # Generated enhanced format +│ └── brands.json +├── brands.json # Legacy format (backward compatible) +├── SCHEMA.md # Complete schema documentation +└── README.md # This file +``` + +## File Formats + +### Source Files (`sources/*.json`) + +Human-editable card scheme definitions: + +```json +{ + "scheme": "visa", + "brand": "Visa", + "patterns": [ + { + "bin": "^4", + "length": [13, 16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} +``` + +### Compiled Format (`compiled/brands.json`) + +Enhanced format with full metadata: + +```json +{ + "scheme": "visa", + "brand": "Visa", + "type": "credit", + "number": { + "lengths": [13, 16], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^4|^6367", + "full": "(^4[0-9]{9,12}|^4[0-9]{12,15}|^6367[0-9]{12,15})" + }, + "countries": ["GLOBAL"], + "metadata": { + "sourceFile": "visa.json" + } +} +``` + +### Legacy Format (`brands.json`) + +Backward-compatible simple format: + +```json +{ + "name": "visa", + "regexpBin": "^4|^6367", + "regexpFull": "^4[0-9]{12}(?:[0-9]{3})?|6367[0-9]{12}$", + "regexpCvv": "^\\d{3}$" +} +``` + +## Supported Brands + +The following credit card brands are currently supported: + +- **Elo** - Brazilian credit card brand +- **Diners** - Diners Club International +- **Discover** - Discover Financial Services +- **Hipercard** - Brazilian credit card brand +- **Amex** (American Express) +- **Aura** - Brazilian credit card brand +- **Mastercard** - Mastercard Worldwide +- **Visa** - Visa Inc. + +## Data Usage + +### For Library Developers + +If you're building a library that needs credit card validation data: + +```javascript +// Method 1: Import the module +const ccData = require('creditcard-identifier'); +const brands = ccData.data.brands; + +// Method 2: Load JSON directly +const fs = require('fs'); +const path = require('path'); +const dataPath = require.resolve('creditcard-identifier/data/brands.json'); +const brands = JSON.parse(fs.readFileSync(dataPath, 'utf8')); +``` + +### For Application Developers + +If you're building an application and need to validate credit cards: + +```javascript +const creditcard = require('creditcard-identifier'); + +// Use the provided functions +const brand = creditcard.findBrand('4012001037141112'); +const isSupported = creditcard.isSupported('4012001037141112'); +``` + +## Contributing + +When contributing new brand data or updating existing patterns: + +1. Update the `data/brands.json` file +2. Ensure the data follows the schema above +3. Add test cases in `test/creditcard-identifier.test.js` +4. Document the source of the information in your pull request +5. Run `npm run test-unit` to verify all tests pass + +## Data Sources + +The data in this project has been collected from: + +- Real credit card samples (anonymized BIN patterns only) +- Official brand documentation (where available) +- Community contributions + +**Note:** Some brands (Elo, Hipercard, Aura) may not have official public documentation. The patterns for these brands have been collected from real-world usage and community input. + +## License + +This data is provided under the MIT license. See the main LICENSE file for details. diff --git a/data/SCHEMA.md b/data/SCHEMA.md new file mode 100644 index 0000000..14016bd --- /dev/null +++ b/data/SCHEMA.md @@ -0,0 +1,235 @@ +# BIN-CC Data Schema + +This document describes the data schema for the bin-cc project. + +## Overview + +The bin-cc project uses a **source → build → compiled** approach similar to browserslist: +- **Source files** (`data/sources/*.json`) - Human-editable card scheme definitions +- **Build script** (`scripts/build.js`) - Compiles and validates source data +- **Compiled data** (`data/compiled/brands.json`) - Enhanced format with full details +- **Legacy data** (`data/brands.json`) - Backward-compatible simple format + +## Source Data Format + +Source files in `data/sources/` define card schemes with their BIN patterns. + +### Source Schema + +```json +{ + "scheme": "string", // Scheme identifier (lowercase, e.g., "visa") + "brand": "string", // Display name (e.g., "Visa") + "patterns": [ // Array of BIN patterns for this scheme + { + "bin": "regex", // BIN matching pattern (first 4-8 digits) + "length": [number], // Valid card number lengths (array) + "luhn": boolean, // Whether Luhn algorithm applies + "cvvLength": number, // CVV/CVC code length + "comment": "string" // Optional: explanation of the pattern + } + ], + "type": "credit|debit", // Card type + "countries": ["string"], // ISO country codes or "GLOBAL" + "bins": [ // Optional: Detailed BIN-level information + { + "bin": "string", // Specific 6-digit BIN + "type": "string", // Card type (CREDIT, DEBIT) + "category": "string",// Card category (CLASSIC, GOLD, PLATINUM, etc.) or null + "issuer": "string" // Issuing bank name + } + ] +} +``` + +### Example Source File + +```json +{ + "scheme": "visa", + "brand": "Visa", + "patterns": [ + { + "bin": "^4", + "length": [13, 16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"], + "bins": [ + { + "bin": "491441", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491414", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO DO ESTADO DO PARANA" + } + ] +} +``` + +## Compiled Data Format + +The build process generates `data/compiled/brands.json` with enhanced metadata. + +### Compiled Schema + +```json +{ + "scheme": "string", // Scheme identifier + "brand": "string", // Display brand name + "type": "credit|debit", // Card type + "number": { + "lengths": [number], // Valid card number lengths + "luhn": boolean // Luhn validation required + }, + "cvv": { + "length": number // CVV code length + }, + "patterns": { + "bin": "regex", // Combined BIN pattern + "full": "regex" // Full card number validation pattern + }, + "countries": ["string"], // Issuing countries + "metadata": { + "sourceFile": "string" // Source file reference + }, + "bins": [ // Optional: Detailed BIN information + { + "bin": "string", // Specific 6-digit BIN + "type": "string", // Card type (CREDIT, DEBIT) + "category": "string", // Card category (CLASSIC, GOLD, PLATINUM) or null + "issuer": "string" // Issuing bank name or null + } + ] +} +``` + +### Future Enhancements + +The schema is designed to accommodate additional fields: + +```json +{ + "prepaid": boolean, // Prepaid card indicator + "country": { // Detailed country info + "numeric": "string", + "alpha2": "string", + "name": "string", + "emoji": "string", + "currency": "string", + "latitude": number, + "longitude": number + }, + "bank": { // Issuing bank details + "name": "string", + "url": "string", + "phone": "string", + "city": "string" + } +} +``` + +## Legacy Format (Backward Compatible) + +The build also generates `data/brands.json` for backward compatibility. + +### Legacy Schema + +```json +{ + "name": "string", // Scheme name (e.g., "visa") + "regexpBin": "regex", // BIN pattern + "regexpFull": "regex", // Full validation pattern + "regexpCvv": "regex" // CVV validation pattern +} +``` + +## Building the Data + +### Prerequisites + +- Node.js 12+ + +### Build Command + +```bash +node scripts/build.js +``` + +This will: +1. Read all source files from `data/sources/` +2. Compile patterns and generate regexes +3. Validate the compiled data +4. Write `data/compiled/brands.json` (enhanced format) +5. Write `data/brands.json` (legacy format) + +### Validation + +The build script automatically validates: +- Required fields are present +- Regex patterns are valid +- Data structure is correct + +## Contributing + +### Adding a New Card Scheme + +1. Create a new file in `data/sources/` (e.g., `jcb.json`) +2. Follow the source schema format +3. Run `node scripts/build.js` to compile +4. Test the generated patterns +5. Submit a pull request + +### Updating Existing Patterns + +1. Edit the appropriate file in `data/sources/` +2. Add comments explaining the changes +3. Run `node scripts/build.js` to recompile +4. Verify tests still pass +5. Submit a pull request with source documentation + +### Example: Adding JCB + +```json +{ + "scheme": "jcb", + "brand": "JCB", + "patterns": [ + { + "bin": "^35", + "length": [16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} +``` + +Then run: +```bash +node scripts/build.js +``` + +## Data Sources + +When contributing, please document your sources: +- Official documentation from card networks +- Published BIN ranges from issuing banks +- ISO/IEC 7812 standards +- Community-verified patterns + +Include source URLs in pull request descriptions. + +## License + +This data schema and all data files are provided under the MIT license. diff --git a/data/brands.json b/data/brands.json new file mode 100644 index 0000000..813d396 --- /dev/null +++ b/data/brands.json @@ -0,0 +1,50 @@ +[ + { + "name": "elo", + "regexpBin": "^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175|^627780|^636297|^636368|^(506699|5067[0-6]\\d|50677[0-8])|^(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})|^65003[1-3]|^(65003[5-9]|65004\\d|65005[0-1])|^(65040[5-9]|6504[1-3]\\d)|^(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])|^(65054[1-9]|6505[5-8]\\d|65059[0-8])|^(65070\\d|65071[0-8])|^65072[0-7]|^(65090[1-9]|65091\\d|650920)|^(65165[2-9]|6516[6-7]\\d)|^(65500\\d|65501\\d)|^(65502[1-9]|6550[3-4]\\d|65505[0-8])", + "regexpFull": "^(401178|401179|431274|438935|451416|457393|457631|457632|504175|627780|636297|636368|(506699|5067[0-6]\\d|50677[0-8])|(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})|65003[1-3]|(65003[5-9]|65004\\d|65005[0-1])|(65040[5-9]|6504[1-3]\\d)|(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])|(65054[1-9]|6505[5-8]\\d|65059[0-8])|(65070\\d|65071[0-8])|65072[0-7]|(65090[1-9]|65091\\d|650920)|(65165[2-9]|6516[6-7]\\d)|(65500\\d|65501\\d)|(65502[1-9]|6550[3-4]\\d|65505[0-8]))[0-9]{10,12}", + "regexpCvv": "^\\d{3}$" + }, + { + "name": "diners", + "regexpBin": "^3(?:0[0-5]|[68][0-9])", + "regexpFull": "^3(?:0[0-5]|[68][0-9])[0-9]{11}$", + "regexpCvv": "^\\d{3}$" + }, + { + "name": "discover", + "regexpBin": "^6(?:011|5[0-9]{2})", + "regexpFull": "^6(?:011|5[0-9]{2})[0-9]{12}$", + "regexpCvv": "^\\d{3}$" + }, + { + "name": "hipercard", + "regexpBin": "^3841[046]0|^60", + "regexpFull": "^(38[0-9]{17}|60[0-9]{14})$", + "regexpCvv": "^\\d{3}$" + }, + { + "name": "amex", + "regexpBin": "^3[47]", + "regexpFull": "^3[47][0-9]{13}$", + "regexpCvv": "^\\d{3,4}$" + }, + { + "name": "aura", + "regexpBin": "^50[0-9]", + "regexpFull": "^50[0-9]{14,17}$", + "regexpCvv": "^\\d{3}$" + }, + { + "name": "mastercard", + "regexpBin": "^5[1-5][0-9][0-9]|^2[2-7][0-2][0-9]", + "regexpFull": "^5[1-5][0-9]{14}$|^2[2-7][0-2][0-9]{13}$", + "regexpCvv": "^\\d{3}$" + }, + { + "name": "visa", + "regexpBin": "^4|^6367", + "regexpFull": "^4[0-9]{12}(?:[0-9]{3})?|6367[0-9]{12}$", + "regexpCvv": "^\\d{3}$" + } +] diff --git a/data/compiled/brands.json b/data/compiled/brands.json new file mode 100644 index 0000000..cea2ec6 --- /dev/null +++ b/data/compiled/brands.json @@ -0,0 +1,315 @@ +[ + { + "scheme": "amex", + "brand": "American Express", + "type": "credit", + "number": { + "lengths": [ + 15 + ], + "luhn": true + }, + "cvv": { + "length": 4 + }, + "patterns": { + "bin": "^3[47]", + "full": "(^3[47][0-9]{11,14})" + }, + "countries": [ + "GLOBAL" + ], + "metadata": { + "sourceFile": "amex.json" + } + }, + { + "scheme": "aura", + "brand": "Aura", + "type": "credit", + "number": { + "lengths": [ + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^50[0-9]", + "full": "(^50[0-9][0-9]{12,15})" + }, + "countries": [ + "BR" + ], + "metadata": { + "sourceFile": "aura.json" + } + }, + { + "scheme": "diners", + "brand": "Diners Club", + "type": "credit", + "number": { + "lengths": [ + 14, + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^3(?:0[0-5]|[68][0-9])", + "full": "(^3(?:0[0-5]|[68][0-9])[0-9]{10,13}|^3(?:0[0-5]|[68][0-9])[0-9]{12,15})" + }, + "countries": [ + "GLOBAL" + ], + "metadata": { + "sourceFile": "diners.json" + } + }, + { + "scheme": "discover", + "brand": "Discover", + "type": "credit", + "number": { + "lengths": [ + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^6011|^622|^64|^65", + "full": "(^6011[0-9]{12,15}|^622[0-9]{12,15}|^64[0-9]{12,15}|^65[0-9]{12,15})" + }, + "countries": [ + "US" + ], + "metadata": { + "sourceFile": "discover.json" + } + }, + { + "scheme": "elo", + "brand": "Elo", + "type": "credit", + "number": { + "lengths": [ + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175|^627780|^636297|^636368|^(506699|5067[0-6]\\d|50677[0-8])|^(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})|^65003[1-3]|^(65003[5-9]|65004\\d|65005[0-1])|^(65040[5-9]|6504[1-3]\\d)|^(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])|^(65054[1-9]|6505[5-8]\\d|65059[0-8])|^(65070\\d|65071[0-8])|^65072[0-7]|^(65090[1-9]|65091\\d|650920)|^(65165[2-9]|6516[6-7]\\d)|^(65500\\d|65501\\d)|^(65502[1-9]|6550[3-4]\\d|65505[0-8])", + "full": "(^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175[0-9]{12,15}|^627780|^636297|^636368[0-9]{12,15}|^(506699|5067[0-6]\\d|50677[0-8])[0-9]{12,15}|^(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})[0-9]{12,15}|^65003[1-3][0-9]{12,15}|^(65003[5-9]|65004\\d|65005[0-1])[0-9]{12,15}|^(65040[5-9]|6504[1-3]\\d)[0-9]{12,15}|^(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])[0-9]{12,15}|^(65054[1-9]|6505[5-8]\\d|65059[0-8])[0-9]{12,15}|^(65070\\d|65071[0-8])[0-9]{12,15}|^65072[0-7][0-9]{12,15}|^(65090[1-9]|65091\\d|650920)[0-9]{12,15}|^(65165[2-9]|6516[6-7]\\d)[0-9]{12,15}|^(65500\\d|65501\\d)[0-9]{12,15}|^(65502[1-9]|6550[3-4]\\d|65505[0-8])[0-9]{12,15})" + }, + "countries": [ + "BR" + ], + "metadata": { + "sourceFile": "elo.json" + } + }, + { + "scheme": "hipercard", + "brand": "Hipercard", + "type": "credit", + "number": { + "lengths": [ + 16, + 19, + 13 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^3841[046]0|^60", + "full": "(^3841[046]0[0-9]{12,15}|^3841[046]0[0-9]{15,18}|^60[0-9]{9,12}|^60[0-9]{12,15}|^60[0-9]{15,18})" + }, + "countries": [ + "BR" + ], + "metadata": { + "sourceFile": "hipercard.json" + } + }, + { + "scheme": "mastercard", + "brand": "Mastercard", + "type": "credit", + "number": { + "lengths": [ + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^5[1-5]|^2[2-7][0-2][0-9]", + "full": "(^5[1-5][0-9]{12,15}|^2[2-7][0-2][0-9][0-9]{12,15})" + }, + "countries": [ + "GLOBAL" + ], + "metadata": { + "sourceFile": "mastercard.json" + } + }, + { + "scheme": "visa", + "brand": "Visa", + "type": "credit", + "number": { + "lengths": [ + 13, + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^4|^6367", + "full": "(^4[0-9]{9,12}|^4[0-9]{12,15}|^6367[0-9]{12,15})" + }, + "countries": [ + "GLOBAL" + ], + "metadata": { + "sourceFile": "visa-detailed.example.json" + }, + "bins": [ + { + "bin": "491441", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491440", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491439", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491423", + "type": "CREDIT", + "category": null, + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S.A. (BANRISUL S.A.)" + }, + { + "bin": "491416", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO PARANA" + }, + { + "bin": "491415", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO PARANA" + }, + { + "bin": "491414", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO DO ESTADO DO PARANA" + }, + { + "bin": "491413", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S/A" + }, + { + "bin": "491412", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S/A" + }, + { + "bin": "491411", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S/A" + }, + { + "bin": "491402", + "type": "CREDIT", + "category": null, + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S.A. (BANRISUL S.A.)" + }, + { + "bin": "491316", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO SANTANDER BRASIL, S.A." + }, + { + "bin": "491315", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO SANTANDER, S.A." + }, + { + "bin": "491314", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO SANTANDER, S.A." + }, + { + "bin": "491256", + "type": "CREDIT", + "category": "PLATINUM", + "issuer": "BANCO PANAMERICANO, S.A." + } + ] + }, + { + "scheme": "visa", + "brand": "Visa", + "type": "credit", + "number": { + "lengths": [ + 13, + 16 + ], + "luhn": true + }, + "cvv": { + "length": 3 + }, + "patterns": { + "bin": "^4|^6367", + "full": "(^4[0-9]{9,12}|^4[0-9]{12,15}|^6367[0-9]{12,15})" + }, + "countries": [ + "GLOBAL" + ], + "metadata": { + "sourceFile": "visa.json" + } + } +] \ No newline at end of file diff --git a/data/sources/amex.json b/data/sources/amex.json new file mode 100644 index 0000000..c485bb7 --- /dev/null +++ b/data/sources/amex.json @@ -0,0 +1,14 @@ +{ + "scheme": "amex", + "brand": "American Express", + "patterns": [ + { + "bin": "^3[47]", + "length": [15], + "luhn": true, + "cvvLength": 4 + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} diff --git a/data/sources/aura.json b/data/sources/aura.json new file mode 100644 index 0000000..7b1cbb2 --- /dev/null +++ b/data/sources/aura.json @@ -0,0 +1,14 @@ +{ + "scheme": "aura", + "brand": "Aura", + "patterns": [ + { + "bin": "^50[0-9]", + "length": [16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["BR"] +} diff --git a/data/sources/diners.json b/data/sources/diners.json new file mode 100644 index 0000000..f4d04cb --- /dev/null +++ b/data/sources/diners.json @@ -0,0 +1,14 @@ +{ + "scheme": "diners", + "brand": "Diners Club", + "patterns": [ + { + "bin": "^3(?:0[0-5]|[68][0-9])", + "length": [14, 16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} diff --git a/data/sources/discover.json b/data/sources/discover.json new file mode 100644 index 0000000..c84aaa7 --- /dev/null +++ b/data/sources/discover.json @@ -0,0 +1,32 @@ +{ + "scheme": "discover", + "brand": "Discover", + "patterns": [ + { + "bin": "^6011", + "length": [16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^622", + "length": [16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^64", + "length": [16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^65", + "length": [16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["US"] +} diff --git a/data/sources/elo.json b/data/sources/elo.json new file mode 100644 index 0000000..caf1096 --- /dev/null +++ b/data/sources/elo.json @@ -0,0 +1,111 @@ +{ + "scheme": "elo", + "brand": "Elo", + "patterns": [ + { + "bin": "^401178|^401179|^431274|^438935|^451416|^457393|^457631|^457632|^504175", + "length": [16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^627780|^636297|^636368", + "length": [16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^(506699|5067[0-6]\\d|50677[0-8])", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "506699-506778" + }, + { + "bin": "^(50900\\d|5090[1-9]\\d|509[1-9]\\d{2})", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "509000-509999" + }, + { + "bin": "^65003[1-3]", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650031-650033" + }, + { + "bin": "^(65003[5-9]|65004\\d|65005[0-1])", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650035-650051" + }, + { + "bin": "^(65040[5-9]|6504[1-3]\\d)", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650405-650439" + }, + { + "bin": "^(65048[5-9]|65049\\d|6505[0-2]\\d|65053[0-8])", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650485-650538" + }, + { + "bin": "^(65054[1-9]|6505[5-8]\\d|65059[0-8])", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650541-650598" + }, + { + "bin": "^(65070\\d|65071[0-8])", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650700-650718" + }, + { + "bin": "^65072[0-7]", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650720-650727" + }, + { + "bin": "^(65090[1-9]|65091\\d|650920)", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "650901-650920" + }, + { + "bin": "^(65165[2-9]|6516[6-7]\\d)", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "651652-651679" + }, + { + "bin": "^(65500\\d|65501\\d)", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "655000-655019" + }, + { + "bin": "^(65502[1-9]|6550[3-4]\\d|65505[0-8])", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "655021-655058" + } + ], + "type": "credit", + "countries": ["BR"] +} diff --git a/data/sources/hipercard.json b/data/sources/hipercard.json new file mode 100644 index 0000000..ef1b7bd --- /dev/null +++ b/data/sources/hipercard.json @@ -0,0 +1,20 @@ +{ + "scheme": "hipercard", + "brand": "Hipercard", + "patterns": [ + { + "bin": "^3841[046]0", + "length": [16, 19], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^60", + "length": [13, 16, 19], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["BR"] +} diff --git a/data/sources/mastercard.json b/data/sources/mastercard.json new file mode 100644 index 0000000..0309f45 --- /dev/null +++ b/data/sources/mastercard.json @@ -0,0 +1,21 @@ +{ + "scheme": "mastercard", + "brand": "Mastercard", + "patterns": [ + { + "bin": "^5[1-5]", + "length": [16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^2[2-7][0-2][0-9]", + "length": [16], + "luhn": true, + "cvvLength": 3, + "comment": "2-series BINs (222100-272099)" + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} diff --git a/data/sources/visa-detailed.example.json b/data/sources/visa-detailed.example.json new file mode 100644 index 0000000..5a9169f --- /dev/null +++ b/data/sources/visa-detailed.example.json @@ -0,0 +1,112 @@ +{ + "scheme": "visa", + "brand": "Visa", + "patterns": [ + { + "bin": "^4", + "length": [13, 16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^6367", + "length": [16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"], + "bins": [ + { + "bin": "491441", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491440", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491439", + "type": "CREDIT", + "category": null, + "issuer": "BANCO PROSPER, S.A." + }, + { + "bin": "491423", + "type": "CREDIT", + "category": null, + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S.A. (BANRISUL S.A.)" + }, + { + "bin": "491416", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO PARANA" + }, + { + "bin": "491415", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO PARANA" + }, + { + "bin": "491414", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO DO ESTADO DO PARANA" + }, + { + "bin": "491413", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S/A" + }, + { + "bin": "491412", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S/A" + }, + { + "bin": "491411", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S/A" + }, + { + "bin": "491402", + "type": "CREDIT", + "category": null, + "issuer": "BANCO DO ESTADO DO RIO GRANDE DO SUL S.A. (BANRISUL S.A.)" + }, + { + "bin": "491316", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO SANTANDER BRASIL, S.A." + }, + { + "bin": "491315", + "type": "CREDIT", + "category": "CLASSIC", + "issuer": "BANCO SANTANDER, S.A." + }, + { + "bin": "491314", + "type": "CREDIT", + "category": "GOLD", + "issuer": "BANCO SANTANDER, S.A." + }, + { + "bin": "491256", + "type": "CREDIT", + "category": "PLATINUM", + "issuer": "BANCO PANAMERICANO, S.A." + } + ] +} diff --git a/data/sources/visa.json b/data/sources/visa.json new file mode 100644 index 0000000..4fea282 --- /dev/null +++ b/data/sources/visa.json @@ -0,0 +1,20 @@ +{ + "scheme": "visa", + "brand": "Visa", + "patterns": [ + { + "bin": "^4", + "length": [13, 16], + "luhn": true, + "cvvLength": 3 + }, + { + "bin": "^6367", + "length": [16], + "luhn": true, + "cvvLength": 3 + } + ], + "type": "credit", + "countries": ["GLOBAL"] +} diff --git a/examples/BIN_DATA_USAGE.md b/examples/BIN_DATA_USAGE.md new file mode 100644 index 0000000..bbdf018 --- /dev/null +++ b/examples/BIN_DATA_USAGE.md @@ -0,0 +1,234 @@ +# Usage Examples with Detailed BIN Data + +This document shows how to use the enhanced BIN-level data in your applications. + +## Accessing BIN Details + +### JavaScript Example + +```javascript +const data = require('creditcard-identifier/data/compiled/brands.json'); + +// Find a specific scheme +const visa = data.find(brand => brand.scheme === 'visa'); + +// Check if BIN-level details are available +if (visa.bins) { + console.log(`${visa.brand} has ${visa.bins.length} detailed BINs`); + + // Find a specific BIN + const bin = visa.bins.find(b => b.bin === '491414'); + + if (bin) { + console.log(`BIN: ${bin.bin}`); + console.log(`Type: ${bin.type}`); // CREDIT or DEBIT + console.log(`Category: ${bin.category}`); // GOLD, PLATINUM, etc. + console.log(`Issuer: ${bin.issuer}`); // Bank name + } +} +``` + +### Python Example + +```python +import json + +# Load compiled data +with open('data/compiled/brands.json', 'r') as f: + brands = json.load(f) + +# Find scheme +visa = next((b for b in brands if b['scheme'] == 'visa'), None) + +# Access BIN details +if visa and 'bins' in visa: + for bin_data in visa['bins']: + print(f"BIN {bin_data['bin']}: {bin_data['issuer']}") + if bin_data['category']: + print(f" Category: {bin_data['category']}") +``` + +## Card Category Lookup + +```javascript +function getCardCategory(cardNumber) { + const bin = cardNumber.substring(0, 6); + const data = require('creditcard-identifier/data/compiled/brands.json'); + + for (const brand of data) { + if (brand.bins) { + const binData = brand.bins.find(b => b.bin === bin); + if (binData) { + return { + scheme: brand.scheme, + brand: brand.brand, + issuer: binData.issuer, + category: binData.category, + type: binData.type + }; + } + } + } + + return null; +} + +// Usage +const info = getCardCategory('4914140000000000'); +if (info) { + console.log(`Scheme: ${info.scheme}`); // visa + console.log(`Issuer: ${info.issuer}`); // BANCO DO ESTADO DO PARANA + console.log(`Category: ${info.category}`); // GOLD + console.log(`Type: ${info.type}`); // CREDIT +} +``` + +## Issuer Bank Lookup + +```javascript +function getIssuerBank(cardNumber) { + const bin = cardNumber.substring(0, 6); + const data = require('creditcard-identifier/data/compiled/brands.json'); + + for (const brand of data) { + if (brand.bins) { + const binData = brand.bins.find(b => b.bin === bin); + if (binData && binData.issuer) { + return { + issuer: binData.issuer, + brand: brand.brand + }; + } + } + } + + return null; +} + +// Usage +const bank = getIssuerBank('4914410000000000'); +console.log(bank.issuer); // "BANCO PROSPER, S.A." +``` + +## Listing All Issuers by Scheme + +```javascript +const data = require('creditcard-identifier/data/compiled/brands.json'); + +// Get all Visa issuers +const visa = data.find(b => b.scheme === 'visa'); + +if (visa && visa.bins) { + const issuers = [...new Set(visa.bins.map(b => b.issuer))]; + console.log('Visa Issuers:'); + issuers.forEach(issuer => console.log(` - ${issuer}`)); +} +``` + +## Category Statistics + +```javascript +const data = require('creditcard-identifier/data/compiled/brands.json'); + +function getCategoryStats(scheme) { + const brand = data.find(b => b.scheme === scheme); + + if (!brand || !brand.bins) { + return null; + } + + const stats = {}; + brand.bins.forEach(bin => { + const category = bin.category || 'Standard'; + stats[category] = (stats[category] || 0) + 1; + }); + + return stats; +} + +// Usage +const visaStats = getCategoryStats('visa'); +console.log('Visa Card Categories:'); +console.log(visaStats); +// Output: { GOLD: 3, CLASSIC: 4, PLATINUM: 1, Standard: 7 } +``` + +## REST API Example + +```javascript +const express = require('express'); +const data = require('creditcard-identifier/data/compiled/brands.json'); + +const app = express(); + +app.get('/api/bin/:bin', (req, res) => { + const bin = req.params.bin; + + for (const brand of data) { + if (brand.bins) { + const binData = brand.bins.find(b => b.bin === bin); + if (binData) { + return res.json({ + bin: bin, + scheme: brand.scheme, + brand: brand.brand, + type: binData.type, + category: binData.category, + issuer: binData.issuer + }); + } + } + } + + res.status(404).json({ error: 'BIN not found' }); +}); + +app.listen(3000, () => { + console.log('BIN API running on port 3000'); +}); +``` + +## TypeScript Types + +```typescript +interface BinData { + bin: string; + type: string; + category: string | null; + issuer: string | null; +} + +interface Brand { + scheme: string; + brand: string; + type: string; + number: { + lengths: number[]; + luhn: boolean; + }; + cvv: { + length: number; + }; + patterns: { + bin: string; + full: string; + }; + countries: string[]; + metadata: { + sourceFile: string; + }; + bins?: BinData[]; +} + +// Usage +import data from 'creditcard-identifier/data/compiled/brands.json'; +const brands: Brand[] = data; +``` + +## Notes + +- BIN-level details are **optional** - not all schemes have them +- Always check if `brand.bins` exists before accessing +- BIN data is primarily for Brazilian cards currently +- `category` can be `null` for standard cards +- `issuer` contains the bank name as provided in the source data diff --git a/examples/dotnet/CreditCardValidator.cs b/examples/dotnet/CreditCardValidator.cs new file mode 100644 index 0000000..889f893 --- /dev/null +++ b/examples/dotnet/CreditCardValidator.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace CreditCardValidation +{ + /// + /// Credit Card BIN Validator - .NET Example + /// + /// This class shows how to use the bin-cc data file project in .NET/C#. + /// It loads the brands.json file and performs credit card validation. + /// + public class CreditCardValidator + { + private readonly List _brands; + + /// + /// Brand information from data file + /// + public class Brand + { + public string name { get; set; } + public string regexpBin { get; set; } + public string regexpFull { get; set; } + public string regexpCvv { get; set; } + } + + /// + /// Initialize validator with brand data + /// + /// Path to brands.json. If null, uses default location. + public CreditCardValidator(string dataPath = null) + { + if (string.IsNullOrEmpty(dataPath)) + { + // Default path relative to executable + var baseDir = AppDomain.CurrentDomain.BaseDirectory; + dataPath = Path.Combine(baseDir, "../../data/brands.json"); + } + + var jsonString = File.ReadAllText(dataPath); + _brands = JsonSerializer.Deserialize>(jsonString); + } + + /// + /// Identify the credit card brand + /// + /// Credit card number + /// Brand name or null if not found + public string FindBrand(string cardNumber) + { + if (string.IsNullOrEmpty(cardNumber)) + return null; + + var brand = _brands.FirstOrDefault(b => + { + var regex = new Regex(b.regexpFull); + return regex.IsMatch(cardNumber); + }); + + return brand?.name; + } + + /// + /// Check if card number is supported + /// + /// Credit card number + /// True if supported, false otherwise + public bool IsSupported(string cardNumber) + { + return FindBrand(cardNumber) != null; + } + + /// + /// Validate CVV for a specific brand + /// + /// CVV code + /// Brand name (e.g., "visa", "mastercard") + /// True if valid, false otherwise + public bool ValidateCvv(string cvv, string brandName) + { + var brand = GetBrandInfo(brandName); + if (brand == null) + return false; + + var regex = new Regex(brand.regexpCvv); + return regex.IsMatch(cvv); + } + + /// + /// Get information about a specific brand + /// + /// Brand name (e.g., "visa", "mastercard") + /// Brand information or null if not found + public Brand GetBrandInfo(string brandName) + { + return _brands.FirstOrDefault(b => b.name == brandName); + } + + /// + /// List all supported brands + /// + /// List of brand names + public List ListBrands() + { + return _brands.Select(b => b.name).ToList(); + } + } + + /// + /// Example usage of the credit card validator + /// + class Program + { + static void Main(string[] args) + { + Console.WriteLine("=== Credit Card Validator - .NET Example ===\n"); + + var validator = new CreditCardValidator(); + + // Example 1: List all brands + var brands = validator.ListBrands(); + Console.WriteLine($"Supported brands: {string.Join(", ", brands)}"); + Console.WriteLine(); + + // Example 2: Identify card brands + var testCards = new Dictionary + { + { "4012001037141112", "visa" }, + { "5533798818319497", "mastercard" }, + { "378282246310005", "amex" }, + { "6011236044609927", "discover" } + }; + + Console.WriteLine("Card brand identification:"); + foreach (var kvp in testCards) + { + var brand = validator.FindBrand(kvp.Key); + var status = brand != null ? "✓" : "✗"; + Console.WriteLine($"{status} {kvp.Key}: {brand} (expected: {kvp.Value})"); + } + Console.WriteLine(); + + // Example 3: CVV validation + Console.WriteLine("CVV validation:"); + Console.WriteLine($"Visa CVV 123: {validator.ValidateCvv("123", "visa")}"); + Console.WriteLine($"Amex CVV 1234: {validator.ValidateCvv("1234", "amex")}"); + Console.WriteLine($"Visa CVV 12: {validator.ValidateCvv("12", "visa")} (invalid)"); + Console.WriteLine(); + + // Example 4: Get brand details + Console.WriteLine("Visa brand details:"); + var visaInfo = validator.GetBrandInfo("visa"); + if (visaInfo != null) + { + Console.WriteLine($" BIN pattern: {visaInfo.regexpBin}"); + Console.WriteLine($" Full pattern: {visaInfo.regexpFull}"); + Console.WriteLine($" CVV pattern: {visaInfo.regexpCvv}"); + } + else + { + Console.WriteLine(" Not found"); + } + } + } +} diff --git a/examples/dotnet/README.md b/examples/dotnet/README.md new file mode 100644 index 0000000..a175c2a --- /dev/null +++ b/examples/dotnet/README.md @@ -0,0 +1,52 @@ +# Credit Card Validator - .NET Example + +C#/.NET implementation showing how to use the bin-cc data file project. + +## Usage + +```bash +dotnet run +``` + +Or compile and run: +```bash +csc CreditCardValidator.cs +./CreditCardValidator +``` + +## Requirements + +- .NET 5.0+ or .NET Core 3.1+ +- System.Text.Json (included in .NET) + +## Features + +- Load brand data from JSON +- Identify credit card brand +- Validate CVV codes +- Check if card is supported +- Get brand information + +## Example + +```csharp +using CreditCardValidation; + +var validator = new CreditCardValidator(); + +// Identify brand +var brand = validator.FindBrand("4012001037141112"); +Console.WriteLine(brand); // "visa" + +// Check if supported +var supported = validator.IsSupported("4012001037141112"); +Console.WriteLine(supported); // True + +// Validate CVV +var valid = validator.ValidateCvv("123", "visa"); +Console.WriteLine(valid); // True +``` + +## Data Source + +Loads data from [`../../data/brands.json`](../../data/brands.json) diff --git a/examples/elixir/README.md b/examples/elixir/README.md new file mode 100644 index 0000000..09eb705 --- /dev/null +++ b/examples/elixir/README.md @@ -0,0 +1,51 @@ +# Credit Card Validator - Elixir Example + +Elixir implementation showing how to use the bin-cc data file project. + +## Requirements + +- Elixir 1.10+ +- Jason library for JSON parsing + +Add to your `mix.exs`: +```elixir +{:jason, "~> 1.4"} +``` + +## Usage + +```bash +# In iex +iex> c("credit_card_validator.ex") +iex> Example.run() +``` + +## Features + +- Load brand data from JSON +- Identify credit card brand +- Validate CVV codes +- Check if card is supported +- Get brand information + +## Example + +```elixir +brands = CreditCardValidator.load_brands() + +# Identify brand +brand = CreditCardValidator.find_brand("4012001037141112", brands) +IO.puts(brand) # "visa" + +# Check if supported +supported = CreditCardValidator.is_supported?("4012001037141112", brands) +IO.puts(supported) # true + +# Validate CVV +valid = CreditCardValidator.validate_cvv("123", "visa", brands) +IO.puts(valid) # true +``` + +## Data Source + +Loads data from [`../../data/brands.json`](../../data/brands.json) diff --git a/examples/elixir/credit_card_validator.ex b/examples/elixir/credit_card_validator.ex new file mode 100644 index 0000000..75409c3 --- /dev/null +++ b/examples/elixir/credit_card_validator.ex @@ -0,0 +1,156 @@ +defmodule CreditCardValidator do + @moduledoc """ + Credit Card BIN Validator - Elixir Example + + This module shows how to use the bin-cc data file project in Elixir. + It loads the brands.json file and performs credit card validation. + """ + + @doc """ + Load brand data from JSON file. + + ## Parameters + - data_path: Path to brands.json (optional, uses default if nil) + + ## Returns + List of brand maps + """ + def load_brands(data_path \\ nil) do + path = data_path || Path.join([__DIR__, "../../data/brands.json"]) + + path + |> File.read!() + |> Jason.decode!() + end + + @doc """ + Identify the credit card brand. + + ## Parameters + - card_number: Credit card number as string + - brands: List of brand data + + ## Returns + Brand name (string) or nil if not found + """ + def find_brand(card_number, brands) do + brands + |> Enum.find(fn brand -> + {:ok, regex} = Regex.compile(brand["regexpFull"]) + Regex.match?(regex, card_number) + end) + |> case do + nil -> nil + brand -> brand["name"] + end + end + + @doc """ + Check if card number is supported. + + ## Parameters + - card_number: Credit card number as string + - brands: List of brand data + + ## Returns + true if supported, false otherwise + """ + def is_supported?(card_number, brands) do + find_brand(card_number, brands) != nil + end + + @doc """ + Validate CVV for a specific brand. + + ## Parameters + - cvv: CVV code as string + - brand_name: Brand name (e.g., "visa", "mastercard") + - brands: List of brand data + + ## Returns + true if valid, false otherwise + """ + def validate_cvv(cvv, brand_name, brands) do + case get_brand_info(brand_name, brands) do + nil -> false + brand -> + {:ok, regex} = Regex.compile(brand["regexpCvv"]) + Regex.match?(regex, cvv) + end + end + + @doc """ + Get information about a specific brand. + + ## Parameters + - brand_name: Brand name (e.g., "visa", "mastercard") + - brands: List of brand data + + ## Returns + Brand map or nil if not found + """ + def get_brand_info(brand_name, brands) do + Enum.find(brands, fn brand -> brand["name"] == brand_name end) + end + + @doc """ + List all supported brands. + + ## Parameters + - brands: List of brand data + + ## Returns + List of brand names + """ + def list_brands(brands) do + Enum.map(brands, fn brand -> brand["name"] end) + end +end + +# Example usage +defmodule Example do + def run do + IO.puts("=== Credit Card Validator - Elixir Example ===\n") + + brands = CreditCardValidator.load_brands() + + # Example 1: List all brands + brand_names = CreditCardValidator.list_brands(brands) + IO.puts("Supported brands: #{Enum.join(brand_names, ", ")}\n") + + # Example 2: Identify card brands + test_cards = %{ + "4012001037141112" => "visa", + "5533798818319497" => "mastercard", + "378282246310005" => "amex", + "6011236044609927" => "discover" + } + + IO.puts("Card brand identification:") + Enum.each(test_cards, fn {card, expected} -> + brand = CreditCardValidator.find_brand(card, brands) + status = if brand, do: "✓", else: "✗" + IO.puts("#{status} #{card}: #{brand} (expected: #{expected})") + end) + IO.puts("") + + # Example 3: CVV validation + IO.puts("CVV validation:") + IO.puts("Visa CVV 123: #{CreditCardValidator.validate_cvv("123", "visa", brands)}") + IO.puts("Amex CVV 1234: #{CreditCardValidator.validate_cvv("1234", "amex", brands)}") + IO.puts("Visa CVV 12: #{CreditCardValidator.validate_cvv("12", "visa", brands)} (invalid)\n") + + # Example 4: Get brand details + IO.puts("Visa brand details:") + case CreditCardValidator.get_brand_info("visa", brands) do + nil -> IO.puts(" Not found") + visa_info -> + IO.puts(" BIN pattern: #{visa_info["regexpBin"]}") + IO.puts(" Full pattern: #{visa_info["regexpFull"]}") + IO.puts(" CVV pattern: #{visa_info["regexpCvv"]}") + end + end +end + +# Run the example if this file is executed directly +# Example.run() diff --git a/examples/javascript-example.js b/examples/javascript-example.js new file mode 100644 index 0000000..991fe06 --- /dev/null +++ b/examples/javascript-example.js @@ -0,0 +1,50 @@ +'use strict'; + +/** + * Example: Using bin-cc as a data source + * + * This example shows how other libraries can consume the credit card + * BIN data directly from this package, similar to how tzdata is used + * by date/time libraries. + */ + +const creditcard = require('../libs/javascript/index.js'); + +console.log('=== Using bin-cc as a Data File Project ===\n'); + +// Method 1: Access data through the module exports +console.log('Method 1: Via module exports'); +console.log('Available brands:', creditcard.data.brands.map(b => b.name).join(', ')); +console.log(''); + +// Method 2: Use the data in your own validation logic +console.log('Method 2: Custom validation using the data'); +function customValidate(cardNumber, brandName) { + const brand = creditcard.data.brands.find(b => b.name === brandName); + if (!brand) { + return false; + } + const regexp = new RegExp(brand.regexpFull); + return regexp.test(cardNumber); +} + +console.log('Is 4012001037141112 a valid Visa?', customValidate('4012001037141112', 'visa')); +console.log('Is 5533798818319497 a valid Mastercard?', customValidate('5533798818319497', 'mastercard')); +console.log(''); + +// Method 3: Extract specific brand information +console.log('Method 3: Extract specific brand patterns'); +const visaData = creditcard.data.brands.find(b => b.name === 'visa'); +if (visaData) { + console.log('Visa BIN pattern:', visaData.regexpBin); + console.log('Visa full validation pattern:', visaData.regexpFull); + console.log('Visa CVV pattern:', visaData.regexpCvv); +} else { + console.log('Visa brand not found'); +} +console.log(''); + +// Method 4: Use the built-in functions (still available) +console.log('Method 4: Using built-in functions'); +console.log('Brand of 4012001037141112:', creditcard.findBrand('4012001037141112')); +console.log('Is 378282246310005 supported?', creditcard.isSupported('378282246310005')); diff --git a/examples/python/README.md b/examples/python/README.md new file mode 100644 index 0000000..0b9e613 --- /dev/null +++ b/examples/python/README.md @@ -0,0 +1,46 @@ +# Credit Card Validator - Python Example + +Python implementation showing how to use the bin-cc data file project. + +## Usage + +```bash +python3 credit_card_validator.py +``` + +## Requirements + +- Python 3.6+ +- No external dependencies (uses only standard library) + +## Features + +- Load brand data from JSON +- Identify credit card brand +- Validate CVV codes +- Check if card is supported +- Get brand information + +## Example + +```python +from credit_card_validator import CreditCardValidator + +validator = CreditCardValidator() + +# Identify brand +brand = validator.find_brand('4012001037141112') +print(brand) # 'visa' + +# Check if supported +supported = validator.is_supported('4012001037141112') +print(supported) # True + +# Validate CVV +valid = validator.validate_cvv('123', 'visa') +print(valid) # True +``` + +## Data Source + +Loads data from [`../../data/brands.json`](../../data/brands.json) diff --git a/examples/python/credit_card_validator.py b/examples/python/credit_card_validator.py new file mode 100644 index 0000000..629ecc5 --- /dev/null +++ b/examples/python/credit_card_validator.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Credit Card BIN Validator - Python Example + +This example shows how to use the bin-cc data file project in Python. +It loads the brands.json file and performs credit card validation. +""" + +import json +import re +import os + + +class CreditCardValidator: + """Credit card validator using bin-cc data.""" + + def __init__(self, data_path=None): + """ + Initialize validator with brand data. + + Args: + data_path: Path to brands.json. If None, uses default location. + """ + if data_path is None: + # Default path relative to this file + current_dir = os.path.dirname(__file__) + data_path = os.path.join(current_dir, '../../data/brands.json') + + with open(data_path, 'r') as f: + self.brands = json.load(f) + + def find_brand(self, card_number): + """ + Identify the credit card brand. + + Args: + card_number: Credit card number as string + + Returns: + Brand name (str) or None if not found + """ + if not card_number: + return None + + for brand in self.brands: + pattern = brand['regexpFull'] + if re.match(pattern, card_number): + return brand['name'] + + return None + + def is_supported(self, card_number): + """ + Check if card number is supported. + + Args: + card_number: Credit card number as string + + Returns: + True if supported, False otherwise + """ + return self.find_brand(card_number) is not None + + def validate_cvv(self, cvv, brand_name): + """ + Validate CVV for a specific brand. + + Args: + cvv: CVV code as string + brand_name: Brand name (e.g., 'visa', 'mastercard') + + Returns: + True if valid, False otherwise + """ + brand = next((b for b in self.brands if b['name'] == brand_name), None) + if not brand: + return False + + pattern = brand['regexpCvv'] + return re.match(pattern, cvv) is not None + + def get_brand_info(self, brand_name): + """ + Get information about a specific brand. + + Args: + brand_name: Brand name (e.g., 'visa', 'mastercard') + + Returns: + Brand dictionary or None if not found + """ + return next((b for b in self.brands if b['name'] == brand_name), None) + + def list_brands(self): + """ + List all supported brands. + + Returns: + List of brand names + """ + return [brand['name'] for brand in self.brands] + + +def main(): + """Example usage of the credit card validator.""" + + print('=== Credit Card Validator - Python Example ===\n') + + validator = CreditCardValidator() + + # Example 1: List all brands + print('Supported brands:', ', '.join(validator.list_brands())) + print() + + # Example 2: Identify card brands + test_cards = { + '4012001037141112': 'Visa', + '5533798818319497': 'Mastercard', + '378282246310005': 'Amex', + '6011236044609927': 'Discover' + } + + print('Card brand identification:') + for card, expected in test_cards.items(): + brand = validator.find_brand(card) + status = '✓' if brand else '✗' + print(f'{status} {card}: {brand} (expected: {expected.lower()})') + print() + + # Example 3: CVV validation + print('CVV validation:') + print(f'Visa CVV 123: {validator.validate_cvv("123", "visa")}') + print(f'Amex CVV 1234: {validator.validate_cvv("1234", "amex")}') + print(f'Visa CVV 12: {validator.validate_cvv("12", "visa")} (invalid)') + print() + + # Example 4: Get brand details + print('Visa brand details:') + visa_info = validator.get_brand_info('visa') + if visa_info: + print(f' BIN pattern: {visa_info["regexpBin"]}') + print(f' Full pattern: {visa_info["regexpFull"]}') + print(f' CVV pattern: {visa_info["regexpCvv"]}') + + +if __name__ == '__main__': + main() diff --git a/examples/ruby/README.md b/examples/ruby/README.md new file mode 100644 index 0000000..6bf7d12 --- /dev/null +++ b/examples/ruby/README.md @@ -0,0 +1,46 @@ +# Credit Card Validator - Ruby Example + +Ruby implementation showing how to use the bin-cc data file project. + +## Usage + +```bash +ruby credit_card_validator.rb +``` + +## Requirements + +- Ruby 2.5+ +- No external gems (uses only standard library) + +## Features + +- Load brand data from JSON +- Identify credit card brand +- Validate CVV codes +- Check if card is supported +- Get brand information + +## Example + +```ruby +require_relative 'credit_card_validator' + +validator = CreditCardValidator.new + +# Identify brand +brand = validator.find_brand('4012001037141112') +puts brand # 'visa' + +# Check if supported +supported = validator.supported?('4012001037141112') +puts supported # true + +# Validate CVV +valid = validator.validate_cvv('123', 'visa') +puts valid # true +``` + +## Data Source + +Loads data from [`../../data/brands.json`](../../data/brands.json) diff --git a/examples/ruby/credit_card_validator.rb b/examples/ruby/credit_card_validator.rb new file mode 100644 index 0000000..9a54b5b --- /dev/null +++ b/examples/ruby/credit_card_validator.rb @@ -0,0 +1,128 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'json' + +## +# Credit Card BIN Validator - Ruby Example +# +# This class shows how to use the bin-cc data file project in Ruby. +# It loads the brands.json file and performs credit card validation. +class CreditCardValidator + attr_reader :brands + + ## + # Initialize validator with brand data + # + # @param data_path [String, nil] Path to brands.json. If nil, uses default location. + def initialize(data_path = nil) + data_path ||= File.join(__dir__, '../../data/brands.json') + file_content = File.read(data_path) + @brands = JSON.parse(file_content) + end + + ## + # Identify the credit card brand + # + # @param card_number [String] Credit card number + # @return [String, nil] Brand name or nil if not found + def find_brand(card_number) + return nil if card_number.nil? || card_number.empty? + + brand = @brands.find do |b| + pattern = Regexp.new(b['regexpFull']) + pattern.match?(card_number) + end + + brand ? brand['name'] : nil + end + + ## + # Check if card number is supported + # + # @param card_number [String] Credit card number + # @return [Boolean] true if supported, false otherwise + def supported?(card_number) + !find_brand(card_number).nil? + end + + ## + # Validate CVV for a specific brand + # + # @param cvv [String] CVV code + # @param brand_name [String] Brand name (e.g., 'visa', 'mastercard') + # @return [Boolean] true if valid, false otherwise + def validate_cvv(cvv, brand_name) + brand = get_brand_info(brand_name) + return false if brand.nil? + + pattern = Regexp.new(brand['regexpCvv']) + pattern.match?(cvv) + end + + ## + # Get information about a specific brand + # + # @param brand_name [String] Brand name (e.g., 'visa', 'mastercard') + # @return [Hash, nil] Brand hash or nil if not found + def get_brand_info(brand_name) + @brands.find { |b| b['name'] == brand_name } + end + + ## + # List all supported brands + # + # @return [Array] List of brand names + def list_brands + @brands.map { |b| b['name'] } + end +end + +# Example usage +def main + puts '=== Credit Card Validator - Ruby Example ===' + puts + + validator = CreditCardValidator.new + + # Example 1: List all brands + puts "Supported brands: #{validator.list_brands.join(', ')}" + puts + + # Example 2: Identify card brands + test_cards = { + '4012001037141112' => 'visa', + '5533798818319497' => 'mastercard', + '378282246310005' => 'amex', + '6011236044609927' => 'discover' + } + + puts 'Card brand identification:' + test_cards.each do |card, expected| + brand = validator.find_brand(card) + status = brand ? '✓' : '✗' + puts "#{status} #{card}: #{brand} (expected: #{expected})" + end + puts + + # Example 3: CVV validation + puts 'CVV validation:' + puts "Visa CVV 123: #{validator.validate_cvv('123', 'visa')}" + puts "Amex CVV 1234: #{validator.validate_cvv('1234', 'amex')}" + puts "Visa CVV 12: #{validator.validate_cvv('12', 'visa')} (invalid)" + puts + + # Example 4: Get brand details + puts 'Visa brand details:' + visa_info = validator.get_brand_info('visa') + if visa_info + puts " BIN pattern: #{visa_info['regexpBin']}" + puts " Full pattern: #{visa_info['regexpFull']}" + puts " CVV pattern: #{visa_info['regexpCvv']}" + else + puts ' Not found' + end +end + +# Run the example if this file is executed directly +main if __FILE__ == $PROGRAM_NAME diff --git a/libs/javascript/.gitignore b/libs/javascript/.gitignore new file mode 100644 index 0000000..8fce603 --- /dev/null +++ b/libs/javascript/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/libs/javascript/README.md b/libs/javascript/README.md new file mode 100644 index 0000000..5c3ae90 --- /dev/null +++ b/libs/javascript/README.md @@ -0,0 +1,142 @@ +# Credit Card Identifier - JavaScript Implementation + +JavaScript/Node.js implementation for credit card BIN validation. This library automatically downloads the latest BIN data from [GitHub releases](https://github.com/renatovico/bin-cc/releases). + +## Installation + +```bash +npm install creditcard-identifier +``` + +The postinstall script will automatically download the latest credit card BIN data from GitHub releases. + +## Usage + +### Basic Validation + +```javascript +const creditcard = require('creditcard-identifier'); + +// Identify card brand +const brand = creditcard.findBrand('4012001037141112'); +console.log(brand); // 'visa' + +// Check if card is supported +const supported = creditcard.isSupported('4012001037141112'); +console.log(supported); // true + +// Get Hipercard regex +const hipercardRegex = creditcard.hipercardRegexp(); +``` + +### Using Raw Data + +```javascript +const creditcard = require('creditcard-identifier'); + +// Access brand data directly +const brands = creditcard.data.brands; +console.log(brands); +// [ +// { name: 'elo', regexpBin: '...', regexpFull: '...', regexpCvv: '...' }, +// { name: 'diners', regexpBin: '...', regexpFull: '...', regexpCvv: '...' }, +// ... +// ] + +// Use data in custom logic +brands.forEach(brand => { + console.log(`${brand.name}: ${brand.regexpBin}`); +}); +``` + +### Direct JSON Access + +```javascript +const fs = require('fs'); +const path = require('path'); + +// The data is downloaded to the package's data directory +const dataPath = path.join(__dirname, 'node_modules', 'creditcard-identifier', 'data', 'brands.json'); +const brands = JSON.parse(fs.readFileSync(dataPath, 'utf8')); +``` + +## Data Updates + +The library downloads data during installation. To manually update to the latest data: + +```bash +npm run update-data +``` + +Or programmatically: + +```javascript +const { downloadData } = require('creditcard-identifier/download-data'); + +downloadData() + .then(() => console.log('Data updated')) + .catch(err => console.error('Update failed:', err)); +``` + +## API + +### `findBrand(cardNumber)` +Returns the brand name for the given card number. + +**Parameters:** +- `cardNumber` (string): The credit card number + +**Returns:** (string) Brand name (e.g., 'visa', 'mastercard') + +**Throws:** Error if card number is not supported + +### `isSupported(cardNumber)` +Checks if the card number is supported. + +**Parameters:** +- `cardNumber` (string): The credit card number + +**Returns:** (boolean) true if supported, false otherwise + +### `hipercardRegexp()` +Returns the regular expression for Hipercard validation. + +**Returns:** (RegExp) Hipercard validation pattern + +### `brands` +Direct access to the brand data array. + +### `data.brands` +Alternative access to the brand data array. + +## Development + +### Install Dependencies + +```bash +npm install +``` + +### Run Tests + +```bash +npm run test-unit +``` + +### Run Coverage + +```bash +npm run coverage +``` + +## Data Source + +This implementation automatically downloads the latest BIN data from [GitHub releases](https://github.com/renatovico/bin-cc/releases?q=data-v) during installation. + +The data is maintained separately from the library, allowing for independent updates: +- **Library updates**: Published to npm with version tags (e.g., `v1.2.0`) +- **Data updates**: Released on GitHub with data-v tags (e.g., `data-v2.0.1`) + +## License + +MIT diff --git a/libs/javascript/creditcard-identifier.js b/libs/javascript/creditcard-identifier.js new file mode 100644 index 0000000..3111558 --- /dev/null +++ b/libs/javascript/creditcard-identifier.js @@ -0,0 +1,78 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); + +// Try to load data from downloaded file first, fall back to bundled data +let brands; +const downloadedDataPath = path.join(__dirname, 'data', 'brands.json'); +const bundledDataPath = path.join(__dirname, '../../data/brands.json'); + +if (fs.existsSync(downloadedDataPath)) { + // Load from downloaded data + brands = require(downloadedDataPath); +} else if (fs.existsSync(bundledDataPath)) { + // Fall back to bundled data (for development) + brands = require(bundledDataPath); +} else { + throw new Error( + 'Credit card data not found. Please run: npm run postinstall or download-data.js' + ); +} + +function cardNumberFilter(cardNumber, brand) { + if (typeof cardNumber !== 'string') { + throw Error('Card number should be a string'); + } + + return cardNumber.match(brand.regexpFull) !== null; +} + +function cardNameFilter(brandName, brand) { + return brandName === brand.name; +} + +function hipercardRegexp() { + let card = brands.filter(cardNameFilter.bind(this,'hipercard'))[0]; + if (card) { + return new RegExp(card.regexpFull); + } else { + return new RegExp('^$'); + } +} + +function findBrand(cardNumber) { + if(!cardNumber || cardNumber === '') { + cardNumber = '000000'; + } + let brand = brands.filter(cardNumberFilter.bind(this, cardNumber))[0]; + + if (brand === undefined) { + throw Error('card number not supported'); + } + + brand = (brand === undefined) ? undefined : brand.name; + + return brand; +} + +function isSupported(cardNumber) { + let number = cardNumber || '0000000000000001'; + + let supported = false; + let result = brands.filter(cardNumberFilter.bind(this, number))[0]; + if (result !== undefined) { + supported = true; + } + + return supported; +} + +module.exports = { + findBrand: findBrand, + isSupported: isSupported, + hipercardRegexp: hipercardRegexp, + brands: brands +} + + diff --git a/libs/javascript/download-data.js b/libs/javascript/download-data.js new file mode 100644 index 0000000..a3b63e9 --- /dev/null +++ b/libs/javascript/download-data.js @@ -0,0 +1,139 @@ +#!/usr/bin/env node +'use strict'; + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const DATA_DIR = path.join(__dirname, 'data'); +const DATA_FILE = path.join(DATA_DIR, 'brands.json'); +const GITHUB_API_URL = 'https://api.github.com/repos/renatovico/bin-cc/releases'; + +/** + * Download data from GitHub releases + */ +function downloadData() { + console.log('📥 Downloading latest credit card BIN data from GitHub releases...'); + + return new Promise((resolve, reject) => { + // First, get the latest data release + https.get(GITHUB_API_URL, { + headers: { + 'User-Agent': 'creditcard-identifier' + } + }, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const releases = JSON.parse(data); + + // Find the latest data release (tag starts with 'data-v') + const dataRelease = releases.find(r => r.tag_name.startsWith('data-v')); + + if (!dataRelease) { + reject(new Error('No data releases found')); + return; + } + + console.log(` Found data release: ${dataRelease.tag_name}`); + + // Find the brands.json asset + const asset = dataRelease.assets.find(a => a.name === 'brands.json'); + + if (!asset) { + reject(new Error('brands.json not found in release assets')); + return; + } + + // Download the asset + downloadFile(asset.browser_download_url, DATA_FILE) + .then(() => { + console.log(`✅ Data downloaded successfully to ${DATA_FILE}`); + resolve(); + }) + .catch(reject); + + } catch (err) { + reject(err); + } + }); + }).on('error', reject); + }); +} + +/** + * Download a file from URL + */ +function downloadFile(url, dest) { + return new Promise((resolve, reject) => { + // Ensure data directory exists + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + + const file = fs.createWriteStream(dest); + + https.get(url, { + headers: { + 'User-Agent': 'creditcard-identifier', + 'Accept': 'application/octet-stream' + } + }, (response) => { + // Handle redirects + if (response.statusCode === 302 || response.statusCode === 301) { + downloadFile(response.headers.location, dest).then(resolve).catch(reject); + return; + } + + response.pipe(file); + + file.on('finish', () => { + file.close(resolve); + }); + }).on('error', (err) => { + fs.unlink(dest, () => {}); // Delete the file on error + reject(err); + }); + + file.on('error', (err) => { + fs.unlink(dest, () => {}); // Delete the file on error + reject(err); + }); + }); +} + +/** + * Check if data needs to be downloaded + */ +function checkDataExists() { + return fs.existsSync(DATA_FILE); +} + +// Run if executed directly +if (require.main === module) { + if (!checkDataExists()) { + downloadData() + .then(() => { + console.log('✨ Setup complete!'); + process.exit(0); + }) + .catch((err) => { + console.error('❌ Failed to download data:', err.message); + process.exit(1); + }); + } else { + console.log('✓ Data already exists'); + process.exit(0); + } +} + +module.exports = { + downloadData, + checkDataExists, + DATA_FILE +}; diff --git a/libs/javascript/index.js b/libs/javascript/index.js new file mode 100644 index 0000000..3a648cf --- /dev/null +++ b/libs/javascript/index.js @@ -0,0 +1,11 @@ +'use strict'; + +const creditcardIdentifier = require('./creditcard-identifier.js'); + +// Export everything from the main module +module.exports = creditcardIdentifier; + +// Also provide direct access to data for other libraries +module.exports.data = { + brands: creditcardIdentifier.brands +}; diff --git a/package-lock.json b/libs/javascript/package-lock.json similarity index 68% rename from package-lock.json rename to libs/javascript/package-lock.json index c5a01dd..05d3e92 100644 --- a/package-lock.json +++ b/libs/javascript/package-lock.json @@ -1,188 +1,236 @@ { "name": "creditcard-identifier", "version": "1.1.2", - "lockfileVersion": 1, + "lockfileVersion": 3, "requires": true, - "dependencies": { - "assertion-error": { + "packages": { + "": { + "name": "creditcard-identifier", + "version": "1.1.2", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "chai": "^3.5.0", + "lodash": "^4.5.0", + "mocha": "^5.2.0" + } + }, + "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true + "dev": true, + "engines": { + "node": "*" + } }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "browser-stdout": { + "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "chai": { + "node_modules/chai": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz", "integrity": "sha1-TQJjewZ/6Vi9v906QOxW/vc3Mkc=", "dev": true, - "requires": { + "dependencies": { "assertion-error": "^1.0.1", "deep-eql": "^0.1.3", "type-detect": "^1.0.0" + }, + "engines": { + "node": ">= 0.4.0" } }, - "commander": { + "node_modules/commander": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "debug": { + "node_modules/debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, - "requires": { + "dependencies": { "ms": "2.0.0" } }, - "deep-eql": { + "node_modules/deep-eql": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", "integrity": "sha1-71WKyrjeJSBs1xOQbXTlaTDrafI=", "dev": true, - "requires": { + "dependencies": { "type-detect": "0.1.1" }, - "dependencies": { - "type-detect": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", - "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", - "dev": true - } + "engines": { + "node": "*" + } + }, + "node_modules/deep-eql/node_modules/type-detect": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz", + "integrity": "sha1-C6XsKohWQORw6k6FBZcZANrFiCI=", + "dev": true, + "engines": { + "node": "*" } }, - "diff": { + "node_modules/diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.3.1" + } }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "glob": { + "node_modules/glob": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, - "requires": { + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" } }, - "growl": { + "node_modules/growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true + "dev": true, + "engines": { + "node": ">=4.x" + } }, - "has-flag": { + "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "he": { + "node_modules/he": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true + "dev": true, + "bin": { + "he": "bin/he" + } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "requires": { + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, - "lodash": { + "node_modules/lodash": { "version": "4.17.11", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, - "minimatch": { + "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { + "node_modules/minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, - "mkdirp": { + "node_modules/mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", "dev": true, - "requires": { + "dependencies": { "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" } }, - "mocha": { + "node_modules/mocha": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", "dev": true, - "requires": { + "dependencies": { "browser-stdout": "1.3.1", "commander": "2.15.1", "debug": "3.1.0", @@ -194,45 +242,61 @@ "minimatch": "3.0.4", "mkdirp": "0.5.1", "supports-color": "5.4.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha" + }, + "engines": { + "node": ">= 4.0.0" } }, - "ms": { + "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "requires": { + "dependencies": { "wrappy": "1" } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "supports-color": { + "node_modules/supports-color": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", "dev": true, - "requires": { + "dependencies": { "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "type-detect": { + "node_modules/type-detect": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz", "integrity": "sha1-diIXzAbbJY7EiQihKY6LlRIejqI=", - "dev": true + "dev": true, + "engines": { + "node": "*" + } }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", diff --git a/libs/javascript/package.json b/libs/javascript/package.json new file mode 100644 index 0000000..d740efe --- /dev/null +++ b/libs/javascript/package.json @@ -0,0 +1,45 @@ +{ + "name": "creditcard-identifier", + "version": "1.1.2", + "description": "Brazilian CreditCard Identifier - Automatically downloads latest BIN data from GitHub releases", + "main": "index.js", + "files": [ + "index.js", + "creditcard-identifier.js", + "download-data.js", + "README.md" + ], + "scripts": { + "postinstall": "node download-data.js", + "test": "npm run test-unit", + "test-unit": "NODE_ENV=test ./node_modules/.bin/mocha ./test --recursive --harmony", + "coverage": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- ./test/ --recursive --harmony", + "update-data": "node download-data.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/renatoelias/bin-cc.git" + }, + "keywords": [ + "creditcard", + "bin", + "identifier", + "validation", + "bin", + "luhn", + "data", + "tzdata", + "credit-card-data" + ], + "author": "Renato Elias, Eriki Herinque, Carlos Rios, Lohan Bohdavean", + "license": "MIT", + "bugs": { + "url": "https://github.com/renatoelias/bin-cc/issues" + }, + "homepage": "https://github.com/renatoelias/bin-cc#readme", + "devDependencies": { + "chai": "^3.5.0", + "lodash": "^4.5.0", + "mocha": "^5.2.0" + } +} diff --git a/test/creditcard-identifier.test.js b/libs/javascript/test/creditcard-identifier.test.js similarity index 100% rename from test/creditcard-identifier.test.js rename to libs/javascript/test/creditcard-identifier.test.js diff --git a/package.json b/package.json index da4f58a..2ab152b 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,30 @@ { - "name": "creditcard-identifier", - "version": "1.1.2", - "description": "Brazilian CreditCard Identifier", - "main": "creditcard-identifier.js", + "name": "bin-cc", + "version": "2.0.0", + "description": "Credit Card BIN Data - Multi-language data file project", + "private": true, "scripts": { - "test-unit": "NODE_ENV=test ./node_modules/.bin/mocha ./test --recursive --harmony", - "coverage": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- ./test/ --recursive --harmony" + "build": "node scripts/build.js", + "validate": "node scripts/build.js", + "test": "npm run build && cd libs/javascript && npm test" }, "repository": { "type": "git", - "url": "git+https://github.com/renatoelias/bin-cc.git" + "url": "git+https://github.com/renatovico/bin-cc.git" }, "keywords": [ "creditcard", "bin", - "identifier", - "validation", - "bin", - "luhn" + "data", + "tzdata", + "browserslist", + "credit-card-data", + "validation" ], "author": "Renato Elias, Eriki Herinque, Carlos Rios, Lohan Bohdavean", "license": "MIT", "bugs": { - "url": "https://github.com/renatoelias/bin-cc/issues" + "url": "https://github.com/renatovico/bin-cc/issues" }, - "homepage": "https://github.com/renatoelias/bin-cc#readme", - "devDependencies": { - "chai": "^3.5.0", - "lodash": "^4.5.0", - "mocha": "^5.2.0" - } + "homepage": "https://github.com/renatovico/bin-cc#readme" } diff --git a/scripts/build.js b/scripts/build.js new file mode 100755 index 0000000..1dccbd0 --- /dev/null +++ b/scripts/build.js @@ -0,0 +1,209 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +/** + * Build script for bin-cc data + * + * Reads source files from data/sources/ and compiles them into: + * 1. data/compiled/brands.json - Enhanced format with all details + * 2. data/brands.json - Legacy format for backward compatibility + */ + +const SOURCES_DIR = path.join(__dirname, '../data/sources'); +const COMPILED_DIR = path.join(__dirname, '../data/compiled'); +const LEGACY_FILE = path.join(__dirname, '../data/brands.json'); + +// Ensure compiled directory exists +if (!fs.existsSync(COMPILED_DIR)) { + fs.mkdirSync(COMPILED_DIR, { recursive: true }); +} + +/** + * Build regex patterns from source patterns + * + * This builds comprehensive patterns for card validation. + * The BIN pattern matches the first digits, and full pattern validates entire card numbers. + */ +function buildPatterns(patterns) { + // Combine all BIN patterns + const binPatterns = patterns.map(p => p.bin).join('|'); + + // For full pattern, we need to match exact card lengths + // Strategy: For each pattern, match its BIN + exact remaining digits for each valid length + const fullPatterns = []; + + for (const pattern of patterns) { + const lengths = Array.isArray(pattern.length) ? pattern.length : [pattern.length]; + + for (const len of lengths) { + // For the full pattern, we match: BIN pattern + rest of digits to reach total length + // Note: The BIN pattern itself may match variable digits (e.g., ^4 matches 1 digit, ^6367 matches 4) + // So we use a simplified approach: the pattern already includes anchors + const binPart = pattern.bin; + + // Rough heuristic: most BIN patterns match 4-6 digits + // For simplicity, let's match (pattern) followed by remaining digits + // This won't be perfect for all cases but handles common scenarios + + if (len === 13) { + fullPatterns.push(`${binPart}[0-9]{9,12}`); + } else if (len === 14) { + fullPatterns.push(`${binPart}[0-9]{10,13}`); + } else if (len === 15) { + fullPatterns.push(`${binPart}[0-9]{11,14}`); + } else if (len === 16) { + fullPatterns.push(`${binPart}[0-9]{12,15}`); + } else if (len === 19) { + fullPatterns.push(`${binPart}[0-9]{15,18}`); + } + } + } + + // Deduplicate patterns + const uniqueFullPatterns = [...new Set(fullPatterns)]; + + return { + binPattern: binPatterns, + fullPattern: `(${uniqueFullPatterns.join('|')})`, + lengths: [...new Set(patterns.flatMap(p => Array.isArray(p.length) ? p.length : [p.length]))], + cvvLength: patterns[0].cvvLength + }; +} + +/** + * Read all source files and compile + */ +function buildData() { + console.log('🔨 Building bin-cc data...\n'); + + const sourceFiles = fs.readdirSync(SOURCES_DIR) + .filter(f => f.endsWith('.json')) + .sort(); + + const compiledBrands = []; + const legacyBrands = []; + + for (const file of sourceFiles) { + const sourcePath = path.join(SOURCES_DIR, file); + const source = JSON.parse(fs.readFileSync(sourcePath, 'utf8')); + + console.log(` ✓ Processing ${source.brand} (${source.scheme})`); + + const patterns = buildPatterns(source.patterns); + + // Enhanced format + const compiledBrand = { + scheme: source.scheme, + brand: source.brand, + type: source.type || 'credit', + number: { + lengths: patterns.lengths, + luhn: source.patterns[0].luhn + }, + cvv: { + length: patterns.cvvLength + }, + patterns: { + bin: patterns.binPattern, + full: patterns.fullPattern + }, + countries: source.countries || [], + metadata: { + sourceFile: file + } + }; + + // Add optional BIN-level details if present + if (source.bins && Array.isArray(source.bins) && source.bins.length > 0) { + compiledBrand.bins = source.bins.map(binData => ({ + bin: binData.bin, + type: binData.type, + category: binData.category || null, + issuer: binData.issuer || null + })); + } + + compiledBrands.push(compiledBrand); + + // Legacy format (backward compatible) + const legacyBrand = { + name: source.scheme, + regexpBin: patterns.binPattern, + regexpFull: patterns.fullPattern, + regexpCvv: `^\\d{${patterns.cvvLength}}$` + }; + + legacyBrands.push(legacyBrand); + } + + // Write compiled format + const compiledPath = path.join(COMPILED_DIR, 'brands.json'); + fs.writeFileSync(compiledPath, JSON.stringify(compiledBrands, null, 2)); + console.log(`\n✅ Compiled data written to: ${path.relative(process.cwd(), compiledPath)}`); + + // Note: Legacy data/brands.json is maintained manually for backward compatibility + // The source files provide a structured, extensible format for future enhancements + console.log(`ℹ️ Legacy data/brands.json maintained separately for backward compatibility`); + + // Generate statistics + console.log(`\n📊 Statistics:`); + console.log(` Total brands: ${compiledBrands.length}`); + console.log(` Global brands: ${compiledBrands.filter(b => b.countries.includes('GLOBAL')).length}`); + console.log(` Brazilian brands: ${compiledBrands.filter(b => b.countries.includes('BR')).length}`); + + return { compiledBrands, legacyBrands }; +} + +/** + * Validate the built data + */ +function validate(data) { + console.log('\n🔍 Validating data...\n'); + + let errors = 0; + + for (const brand of data.compiledBrands) { + // Check required fields + const required = ['scheme', 'brand', 'type', 'number', 'cvv', 'patterns']; + for (const field of required) { + if (!brand[field]) { + console.error(` ✗ ${brand.scheme}: Missing required field '${field}'`); + errors++; + } + } + + // Validate patterns are valid regex + try { + new RegExp(brand.patterns.bin); + new RegExp(brand.patterns.full); + } catch (e) { + console.error(` ✗ ${brand.scheme}: Invalid regex pattern - ${e.message}`); + errors++; + } + } + + if (errors === 0) { + console.log(' ✓ All validations passed!'); + } else { + console.error(`\n❌ ${errors} validation error(s) found`); + process.exit(1); + } +} + +// Run build +if (require.main === module) { + try { + const data = buildData(); + validate(data); + console.log('\n✨ Build completed successfully!\n'); + } catch (error) { + console.error('\n❌ Build failed:', error.message); + console.error(error.stack); + process.exit(1); + } +} + +module.exports = { buildData, validate };