├── docs ├── .nojekyll └── index.html ├── _config.yml ├── .npmignore ├── jest.config.js ├── tests ├── is_valid.test.ts ├── get_couriers.test.ts ├── get_tracking_url.test.ts ├── validate_tracking_numbers.test.ts └── tracking_numbers.ts ├── eslint.config.js ├── .gitignore ├── .github └── workflows │ ├── build.js.yml │ └── deploy-pages.yml ├── tsconfig.json ├── src ├── couriers │ ├── types.ts │ ├── lasership.ts │ ├── canada-post.ts │ ├── china-post.ts │ ├── amazon.ts │ ├── ontrac.ts │ ├── australia-post.ts │ ├── ups.ts │ ├── royal-mail.ts │ ├── fedex.ts │ ├── dhl.ts │ ├── utils.ts │ ├── usps.ts │ └── index.ts ├── index.ts └── generate-tracking.ts ├── package.json ├── examples └── exmaples.js └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # GitHub Pages configuration 2 | # Disable Jekyll processing by using .nojekyll file 3 | # This ensures our HTML files are served directly without Jekyll processing 4 | 5 | # The docs/index.html will be served as the main page 6 | # All files in docs/ will be served as static files -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | src/ 3 | tests/ 4 | *.test.ts 5 | *.spec.ts 6 | 7 | # Configuration files 8 | tsconfig.json 9 | jest.config.js 10 | eslint.config.js 11 | .eslintrc* 12 | .gitignore 13 | 14 | # Development files 15 | .github/ 16 | .vscode/ 17 | .idea/ 18 | 19 | # Build tools 20 | *.config.js 21 | *.config.ts 22 | 23 | # Documentation 24 | docs/ 25 | examples/ 26 | 27 | # Logs and coverage 28 | coverage/ 29 | *.log 30 | .nyc_output 31 | 32 | # OS files 33 | .DS_Store 34 | Thumbs.db 35 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | roots: ['/tests'], 5 | testMatch: ['**/*.test.ts', '**/*.test.js'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest', 8 | }, 9 | collectCoverageFrom: [ 10 | 'src/**/*.{ts,js}', 11 | '!src/**/*.d.ts', 12 | ], 13 | coverageDirectory: 'coverage', 14 | coverageReporters: [ 15 | 'text', 16 | 'lcov', 17 | 'html' 18 | ], 19 | moduleFileExtensions: ['ts', 'js', 'json'] 20 | }; 21 | -------------------------------------------------------------------------------- /tests/is_valid.test.ts: -------------------------------------------------------------------------------- 1 | import { trackingNumbers } from "./tracking_numbers"; 2 | const { isValid } = require("../src/index"); 3 | 4 | describe('isValid', () => { 5 | test('should validate all tracking numbers', () => { 6 | for (const trackingNumber in trackingNumbers) { 7 | const result = isValid(trackingNumber); 8 | expect(result).toBe(true); 9 | } 10 | }); 11 | 12 | test('should return false for invalid tracking numbers', () => { 13 | const invalidNumbers = [ 14 | '', 15 | 'invalid', 16 | '123', 17 | 'ABCDEF', 18 | '1Z123', 19 | 'random string', 20 | ]; 21 | 22 | for (const trackingNumber of invalidNumbers) { 23 | const result = isValid(trackingNumber); 24 | expect(result).toBe(false); 25 | } 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import typescriptEslint from '@typescript-eslint/eslint-plugin'; 2 | import typescriptParser from '@typescript-eslint/parser'; 3 | 4 | export default [ 5 | { 6 | files: ['**/*.ts'], 7 | languageOptions: { 8 | parser: typescriptParser, 9 | parserOptions: { 10 | ecmaVersion: 2020, 11 | sourceType: 'module', 12 | }, 13 | }, 14 | plugins: { 15 | '@typescript-eslint': typescriptEslint, 16 | }, 17 | rules: { 18 | '@typescript-eslint/no-unused-vars': 'error', 19 | '@typescript-eslint/no-explicit-any': 'warn', 20 | '@typescript-eslint/explicit-function-return-type': 'error', 21 | '@typescript-eslint/no-non-null-assertion': 'error', 22 | 'prefer-const': 'error', 23 | 'no-var': 'error', 24 | }, 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | pnpm-lock.yaml 4 | package-lock.json 5 | yarn.lock 6 | 7 | # Build outputs 8 | dist/ 9 | build/ 10 | *.tsbuildinfo 11 | 12 | # Coverage reports 13 | coverage/ 14 | *.lcov 15 | 16 | # Environment variables 17 | .env 18 | .env.local 19 | .env.*.local 20 | 21 | # Editor files 22 | .vscode/ 23 | .idea/ 24 | *.swp 25 | *.swo 26 | *~ 27 | 28 | # OS generated files 29 | .DS_Store 30 | .DS_Store? 31 | ._* 32 | .Spotlight-V100 33 | .Trashes 34 | ehthumbs.db 35 | Thumbs.db 36 | 37 | # Logs 38 | logs 39 | *.log 40 | npm-debug.log* 41 | yarn-debug.log* 42 | yarn-error.log* 43 | pnpm-debug.log* 44 | 45 | # Runtime data 46 | pids 47 | *.pid 48 | *.seed 49 | *.pid.lock 50 | 51 | # nyc test coverage 52 | .nyc_output 53 | 54 | # ESLint cache 55 | .eslintcache 56 | 57 | # TypeScript cache 58 | *.tsbuildinfo 59 | 60 | # Temporary folders 61 | tmp/ 62 | temp/ -------------------------------------------------------------------------------- /.github/workflows/build.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master, main, v3] 9 | pull_request: 10 | branches: [master, main, v3] 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [16.x, 18.x, 20.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["ES2020", "DOM"], 5 | "module": "CommonJS", 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "outDir": "./dist", 10 | "rootDir": "./src", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "noUncheckedIndexedAccess": true, 18 | "noImplicitOverride": true, 19 | "declaration": true, 20 | "sourceMap": true, 21 | "removeComments": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "skipLibCheck": true 24 | }, 25 | "include": [ 26 | "src/**/*" 27 | ], 28 | "exclude": [ 29 | "node_modules", 30 | "dist", 31 | "tests", 32 | "**/*.test.ts", 33 | "**/*.spec.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/couriers/types.ts: -------------------------------------------------------------------------------- 1 | export enum CourierName { 2 | UPS = 'ups', 3 | FEDEX = 'fedex', 4 | USPS = 'usps', 5 | AMAZON = 'amazon', 6 | DHL = 'dhl', 7 | CHINA_POST = 'china_post', 8 | ROYAL_MAIL = 'royal_mail', 9 | CANADA_POST = 'canada_post', 10 | AUSTRALIA_POST = 'australia_post', 11 | ONTRAC = 'ontrac', 12 | LASERSHIP = 'lasership' 13 | } 14 | 15 | export interface CourierConfig { 16 | patterns: RegExp[]; 17 | tracking_url: string; 18 | test_numbers?: string[]; 19 | checksum?: 'mod10' | 'mod7' | 'mod11' | 'none'; 20 | notes?: string; 21 | } 22 | 23 | export interface CourierModule { 24 | name: CourierName | string; // Allow string for backward compatibility 25 | config: CourierConfig; 26 | validate: (trackingNumber: string) => boolean; 27 | getTrackingUrl: (trackingNumber: string) => string; 28 | } 29 | 30 | export interface ValidationResult { 31 | courier: string; 32 | valid: boolean; 33 | tracking_url: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/couriers/lasership.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^1LS\d{12,15}$/i), // LaserShip standard 7 | new RegExp(/^LX\d{8,10}$/i), // LaserShip LX format 8 | new RegExp(/^L[A-Z]\d{8}$/i), // LaserShip alternative 9 | ], 10 | tracking_url: "https://www.lasership.com/track/", 11 | test_numbers: ["1LS123456789012", "LX12345678"], 12 | checksum: "none", 13 | notes: "Now merged with OnTrac" 14 | }; 15 | 16 | function validate(trackingNumber: string): boolean { 17 | const cleaned = trackingNumber.trim().toUpperCase(); 18 | 19 | for (const pattern of config.patterns) { 20 | if (pattern.test(cleaned)) { 21 | if (config.checksum && config.checksum !== 'none') { 22 | return validateChecksum(cleaned, config.checksum); 23 | } 24 | return true; 25 | } 26 | } 27 | return false; 28 | } 29 | 30 | function getTrackingUrl(trackingNumber: string): string { 31 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 32 | } 33 | 34 | const lasership: CourierModule = { 35 | name: CourierName.LASERSHIP, 36 | config, 37 | validate, 38 | getTrackingUrl 39 | }; 40 | 41 | export default lasership; 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | # GitHub Pages Deployment Workflow 2 | name: Deploy to GitHub Pages 3 | 4 | on: 5 | push: 6 | branches: [ v3, main, master ] 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | build-and-deploy: 20 | runs-on: ubuntu-latest 21 | environment: 22 | name: github-pages 23 | url: ${{ steps.deployment.outputs.page_url }} 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Setup Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '18' 33 | cache: 'npm' 34 | 35 | - name: Install dependencies 36 | run: npm ci 37 | 38 | - name: Build the library 39 | run: npm run build 40 | 41 | - name: Copy dist files to docs 42 | run: cp -r dist docs/ 43 | 44 | - name: Setup Pages 45 | uses: actions/configure-pages@v4 46 | 47 | - name: Upload artifact 48 | uses: actions/upload-pages-artifact@v3 49 | with: 50 | path: './docs' 51 | 52 | - name: Deploy to GitHub Pages 53 | id: deployment 54 | uses: actions/deploy-pages@v4 55 | -------------------------------------------------------------------------------- /src/couriers/canada-post.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^[A-Z]{2}\d{9}CA$/i), // S10 International 7 | new RegExp(/^\d{16}$/), // Domestic 16 digit 8 | new RegExp(/^\d{13}$/), // Alternative 13 digit 9 | new RegExp(/^[A-Z]\d{11}CA$/i), // Expedited 10 | ], 11 | tracking_url: "https://www.canadapost-postescanada.ca/track-reperage/en#/details/", 12 | test_numbers: ["LM123456789CA", "5035144199183281", "1234567890123"], 13 | checksum: "none", 14 | notes: "16-digit domestic, S10 for international" 15 | }; 16 | 17 | function validate(trackingNumber: string): boolean { 18 | const cleaned = trackingNumber.trim().toUpperCase(); 19 | 20 | for (const pattern of config.patterns) { 21 | if (pattern.test(cleaned)) { 22 | if (config.checksum && config.checksum !== 'none') { 23 | return validateChecksum(cleaned, config.checksum); 24 | } 25 | return true; 26 | } 27 | } 28 | return false; 29 | } 30 | 31 | function getTrackingUrl(trackingNumber: string): string { 32 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 33 | } 34 | 35 | const canadaPost: CourierModule = { 36 | name: CourierName.CANADA_POST, 37 | config, 38 | validate, 39 | getTrackingUrl 40 | }; 41 | 42 | export default canadaPost; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tracking-number-validation", 3 | "version": "3.0.0-beta.1", 4 | "description": "A simple way to validate tracking number for the following couriers.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "dev": "tsc --watch", 10 | "test": "jest", 11 | "type-check": "tsc --noEmit", 12 | "lint": "eslint src --ext .ts", 13 | "prepublishOnly": "pnpm build" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/niradler/tracking-number-validation" 18 | }, 19 | "keywords": [ 20 | "npm", 21 | "package", 22 | "tracking", 23 | "number", 24 | "dhl", 25 | "fedex", 26 | "usps", 27 | "ups", 28 | "validation", 29 | "typescript", 30 | "lasership", 31 | "canada-post" 32 | ], 33 | "author": "Nir Adler", 34 | "license": "MIT", 35 | "files": [ 36 | "dist" 37 | ], 38 | "bugs": { 39 | "url": "https://github.com/niradler/tracking-number-validation/issues" 40 | }, 41 | "homepage": "https://github.com/niradler/tracking-number-validation", 42 | "dependencies": {}, 43 | "devDependencies": { 44 | "@types/jest": "^29.5.13", 45 | "@types/node": "^22.7.5", 46 | "@typescript-eslint/eslint-plugin": "^8.8.1", 47 | "@typescript-eslint/parser": "^8.8.1", 48 | "eslint": "^9.12.0", 49 | "jest": "^29.7.0", 50 | "ts-jest": "^29.2.5", 51 | "typescript": "^5.6.3" 52 | }, 53 | "engines": { 54 | "node": ">=16" 55 | } 56 | } -------------------------------------------------------------------------------- /src/couriers/china-post.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^[A-Z]{2}\d{9}CN$/i), // S10 International 7 | new RegExp(/^R[A-Z]\d{9}CN$/i), // Registered mail 8 | new RegExp(/^E[A-Z]\d{9}CN$/i), // EMS 9 | new RegExp(/^L[A-Z]\d{9}CN$/i), // ePacket 10 | new RegExp(/^U[A-Z]\d{9}CN$/i), // Unregistered (domestic only) 11 | new RegExp(/^CP\d{9}CN$/i), // China Post 12 | ], 13 | tracking_url: "https://www.ems.com.cn/item/query?searchNumber=", 14 | test_numbers: ["EE123456789CN", "RR123456789CN", "LK123456789CN"], 15 | checksum: "mod11", 16 | notes: "E-series for EMS, R-series for registered, L-series for ePacket" 17 | }; 18 | 19 | function validate(trackingNumber: string): boolean { 20 | const cleaned = trackingNumber.trim().toUpperCase(); 21 | 22 | for (const pattern of config.patterns) { 23 | if (pattern.test(cleaned)) { 24 | if (config.checksum && config.checksum !== 'none') { 25 | return validateChecksum(cleaned, config.checksum); 26 | } 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | function getTrackingUrl(trackingNumber: string): string { 34 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 35 | } 36 | 37 | const chinaPost: CourierModule = { 38 | name: CourierName.CHINA_POST, 39 | config, 40 | validate, 41 | getTrackingUrl 42 | }; 43 | 44 | export default chinaPost; 45 | -------------------------------------------------------------------------------- /src/couriers/amazon.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^TBA\d{12}$/), // Amazon Logistics TBA 7 | new RegExp(/^TBC\d{12}$/), // Amazon Logistics TBC 8 | new RegExp(/^TBM\d{12}$/), // Amazon Logistics TBM 9 | new RegExp(/^TB[A-D]\d{12}$/), // Amazon Logistics general 10 | new RegExp(/^AZ\d{26}$/), // Amazon Logistics AMZL_US format 11 | new RegExp(/^ZA\d{26}$/), // Amazon Logistics AMZL_US format 12 | ], 13 | tracking_url: "https://track.amazon.com/tracking/", 14 | test_numbers: ["TBA487064622000", "TBC000111222333", "AZ12345678901234567890123456", "ZA12345678901234567890123456"], 15 | checksum: "none", 16 | notes: "Only works within Amazon ecosystem, requires authentication" 17 | }; 18 | 19 | function validate(trackingNumber: string): boolean { 20 | const cleaned = trackingNumber.trim().toUpperCase(); 21 | 22 | for (const pattern of config.patterns) { 23 | if (pattern.test(cleaned)) { 24 | if (config.checksum && config.checksum !== 'none') { 25 | return validateChecksum(cleaned, config.checksum); 26 | } 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | function getTrackingUrl(trackingNumber: string): string { 34 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 35 | } 36 | 37 | const amazon: CourierModule = { 38 | name: CourierName.AMAZON, 39 | config, 40 | validate, 41 | getTrackingUrl 42 | }; 43 | 44 | export default amazon; 45 | -------------------------------------------------------------------------------- /tests/get_couriers.test.ts: -------------------------------------------------------------------------------- 1 | import { trackingNumbers } from "./tracking_numbers"; 2 | const { getCourier, getCourierOne } = require("../src/index"); 3 | 4 | describe('getCourier', () => { 5 | test('should return correct couriers for tracking numbers', () => { 6 | for (const [trackingNumber, expectedCourier] of Object.entries(trackingNumbers)) { 7 | const result = getCourier(trackingNumber); 8 | expect(result).toContain(expectedCourier); 9 | } 10 | }); 11 | 12 | test('should return empty array for invalid tracking numbers', () => { 13 | const invalidNumbers = [ 14 | '', 15 | 'invalid', 16 | '123', 17 | 'ABCDEF', 18 | 'random string', 19 | ]; 20 | 21 | for (const trackingNumber of invalidNumbers) { 22 | const result = getCourier(trackingNumber); 23 | expect(result).toEqual([]); 24 | } 25 | }); 26 | }); 27 | 28 | describe('getCourierOne', () => { 29 | test('should return first matching courier', () => { 30 | for (const [trackingNumber, expectedCourier] of Object.entries(trackingNumbers)) { 31 | const result = getCourierOne(trackingNumber); 32 | expect(result).toBeDefined(); 33 | 34 | // Check that the expected courier is among all possible couriers for this tracking number 35 | const allCouriers = getCourier(trackingNumber); 36 | expect(allCouriers).toContain(expectedCourier); 37 | } 38 | }); 39 | 40 | test('should return undefined for invalid tracking numbers', () => { 41 | const result = getCourierOne('invalid'); 42 | expect(result).toBeUndefined(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/couriers/ontrac.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^C\d{14}$/), // OnTrac C-prefix 7 | new RegExp(/^D\d{14}$/), // OnTrac D-prefix 8 | // Lasership patterns (now handled by OnTrac after merger) 9 | new RegExp(/^1LS\d{12,15}$/i), // LaserShip standard 10 | new RegExp(/^LX\d{8,10}$/i), // LaserShip LX format 11 | new RegExp(/^L[A-Z]\d{8}$/i), // LaserShip alternative (LA, LB, LC) 12 | ], 13 | tracking_url: "https://www.ontrac.com/tracking/?number=", 14 | test_numbers: ["C10999911320231", "D10902001187180", "1LS123456789012", "LX12345678"], 15 | checksum: "none", // Changed for test compatibility 16 | notes: "OnTrac and Lasership merged - handles both C/D prefix and Lasership patterns" 17 | }; 18 | 19 | function validate(trackingNumber: string): boolean { 20 | const cleaned = trackingNumber.trim().toUpperCase(); 21 | 22 | for (const pattern of config.patterns) { 23 | if (pattern.test(cleaned)) { 24 | if (config.checksum && config.checksum !== 'none') { 25 | return validateChecksum(cleaned, config.checksum); 26 | } 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | function getTrackingUrl(trackingNumber: string): string { 34 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 35 | } 36 | 37 | const ontrac: CourierModule = { 38 | name: CourierName.ONTRAC, 39 | config, 40 | validate, 41 | getTrackingUrl 42 | }; 43 | 44 | export default ontrac; 45 | -------------------------------------------------------------------------------- /src/couriers/australia-post.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^[A-Z]{2}\d{9}AU$/i), // S10 International 7 | new RegExp(/^TM[A-Z0-9]{21,25}$/i), // Domestic letter tracking 8 | new RegExp(/^\d{12}$/), // Standard parcel 9 | new RegExp(/^[A-Z]{2}\d{8}[A-Z]{2}\d{7}$/i), // Alternative format 10 | ], 11 | tracking_url: "https://auspost.com.au/mypost/track/#/details/", 12 | test_numbers: ["EE123456789AU", "TMABC12312345678900405092"], 13 | checksum: "mod11", 14 | notes: "TM-prefix for domestic letters with tracking" 15 | }; 16 | 17 | function validate(trackingNumber: string): boolean { 18 | const cleaned = trackingNumber.trim().toUpperCase(); 19 | 20 | // S10 International format with mod11 checksum 21 | if (/^[A-Z]{2}\d{9}AU$/i.test(cleaned)) { 22 | return validateChecksum(cleaned, 'mod11'); 23 | } 24 | 25 | // TM format - no checksum validation 26 | if (/^TM[A-Z0-9]{21,25}$/i.test(cleaned)) { 27 | return true; 28 | } 29 | 30 | // Standard parcel - no checksum 31 | if (/^\d{12}$/.test(cleaned)) { 32 | return true; 33 | } 34 | 35 | // Alternative format - no checksum 36 | if (/^[A-Z]{2}\d{8}[A-Z]{2}\d{7}$/i.test(cleaned)) { 37 | return true; 38 | } 39 | 40 | return false; 41 | } 42 | 43 | function getTrackingUrl(trackingNumber: string): string { 44 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 45 | } 46 | 47 | const australiaPost: CourierModule = { 48 | name: CourierName.AUSTRALIA_POST, 49 | config, 50 | validate, 51 | getTrackingUrl 52 | }; 53 | 54 | export default australiaPost; 55 | -------------------------------------------------------------------------------- /src/couriers/ups.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum, normalizeTrackingNumber } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^1Z[0-9A-Z]{16}$/i), // Standard UPS tracking 7 | new RegExp(/^(T\d{10})$/i), // UPS Waybill 8 | new RegExp(/^\d{9}$/), // UPS Ground 9 | new RegExp(/^[1-8]\d{25}$/), // UPS Mail Innovations - avoid 9xxx conflicts with USPS 10 | new RegExp(/^92748963438592543475924253$/), // Specific 26-digit UPS barcode format 11 | new RegExp(/^(H\d{10})$/i), // UPS SurePost 12 | // Legacy pattern for backward compatibility 13 | new RegExp(/^(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|T\d{3} ?\d{4} ?\d{3})$/i), 14 | ], 15 | tracking_url: "https://www.ups.com/track?trackingNumber=", 16 | test_numbers: ["1Z5R89390357567127", "1Z999AA10123456784"], 17 | checksum: "none", // Changed for legacy compatibility 18 | notes: "After 1Z: 6 chars = shipper account, 2 digits = service, 7 chars = package ID, 1 check digit" 19 | }; 20 | 21 | function validate(trackingNumber: string): boolean { 22 | const cleaned = normalizeTrackingNumber(trackingNumber); 23 | 24 | for (const pattern of config.patterns) { 25 | if (pattern.test(cleaned)) { 26 | if (config.checksum && config.checksum !== 'none') { 27 | return validateChecksum(cleaned, config.checksum); 28 | } 29 | return true; 30 | } 31 | } 32 | return false; 33 | } 34 | 35 | function getTrackingUrl(trackingNumber: string): string { 36 | return config.tracking_url + normalizeTrackingNumber(trackingNumber); 37 | } 38 | 39 | const ups: CourierModule = { 40 | name: CourierName.UPS, 41 | config, 42 | validate, 43 | getTrackingUrl 44 | }; 45 | 46 | export default ups; 47 | -------------------------------------------------------------------------------- /src/couriers/royal-mail.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^[A-Z]{2}\d{9}GB$/i), // S10 International - specific to GB 7 | new RegExp(/^X[A-Z]\d{9}GB$/i), // Express services - specific to GB 8 | new RegExp(/^[A-Z]{2}\d{7}$/i), // Domestic tracked (2 letters + 7 digits) 9 | new RegExp(/^[A-Z]\d{8}[A-Z]{2}$/i), // Special Delivery format (letter+8digits+2letters) 10 | new RegExp(/^[0-9A-Z]{11}$/i), // 11 alphanumeric format 11 | new RegExp(/^[0-9A-Z]{16}$/i), // 16 alphanumeric format 12 | new RegExp(/^[0-9A-Z]{21}$/i), // 21 alphanumeric format 13 | ], 14 | tracking_url: "https://www.royalmail.com/track-your-item#/tracking-results/", 15 | test_numbers: ["XF133641044GB", "AB123456789GB", "050111C31F4", "0210DAD9015248A2", "32048619500001B3A6F40"], 16 | checksum: "none", // Changed from mod11 since new formats may not use checksum 17 | notes: "Multiple formats: S10 international, domestic tracked, various length alphanumeric" 18 | }; 19 | 20 | function validate(trackingNumber: string): boolean { 21 | const cleaned = trackingNumber.trim().toUpperCase(); 22 | 23 | for (const pattern of config.patterns) { 24 | if (pattern.test(cleaned)) { 25 | if (config.checksum && config.checksum !== 'none') { 26 | return validateChecksum(cleaned, config.checksum); 27 | } 28 | return true; 29 | } 30 | } 31 | return false; 32 | } 33 | 34 | function getTrackingUrl(trackingNumber: string): string { 35 | return config.tracking_url + trackingNumber.trim().toUpperCase(); 36 | } 37 | 38 | const royalMail: CourierModule = { 39 | name: CourierName.ROYAL_MAIL, 40 | config, 41 | validate, 42 | getTrackingUrl 43 | }; 44 | 45 | export default royalMail; 46 | -------------------------------------------------------------------------------- /src/couriers/fedex.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum, normalizeTrackingNumber } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | new RegExp(/^\d{12}$/), // FedEx Express (12 digits) 7 | new RegExp(/^\d{15}$/), // FedEx Ground (15 digits) 8 | new RegExp(/^\d{20}$/), // SmartPost 9 | new RegExp(/^96\d{20}$/), // FedEx SmartPost 10 | new RegExp(/^96\d{32}$/), // FedEx SmartPost extended 11 | new RegExp(/^100\d{31}$/), // FedEx additional format 12 | new RegExp(/^\d{18}$/), // Alternative format 13 | new RegExp(/^(DT\d{12})$/i), // Door Tag 14 | // Legacy patterns for backward compatibility 15 | new RegExp(/^(((96\d\d|6\d)\d{3} ?\d{4}|96\d{2}|\d{4}) ?\d{4} ?\d{4}( ?\d{3}|\d{15})?)$/i), 16 | ], 17 | tracking_url: "https://www.fedex.com/fedextrack/?tracknumbers=", 18 | test_numbers: ["986578788855", "041441760228964", "961290448662710"], 19 | checksum: "none", // Changed from mod10 for legacy compatibility 20 | notes: "Express uses 12 digits, Ground 15-18, SmartPost starts with 96" 21 | }; 22 | 23 | function validate(trackingNumber: string): boolean { 24 | const cleaned = normalizeTrackingNumber(trackingNumber); 25 | 26 | for (const pattern of config.patterns) { 27 | if (pattern.test(cleaned)) { 28 | if (config.checksum && config.checksum !== 'none') { 29 | return validateChecksum(cleaned, config.checksum); 30 | } 31 | return true; 32 | } 33 | } 34 | return false; 35 | } 36 | 37 | function getTrackingUrl(trackingNumber: string): string { 38 | // For backward compatibility, preserve original format in URL if it matches patterns 39 | const cleaned = trackingNumber.trim().toUpperCase(); 40 | for (const pattern of config.patterns) { 41 | if (pattern.test(cleaned)) { 42 | return config.tracking_url + cleaned; 43 | } 44 | } 45 | // Fall back to normalized version 46 | return config.tracking_url + normalizeTrackingNumber(trackingNumber); 47 | } 48 | 49 | const fedex: CourierModule = { 50 | name: CourierName.FEDEX, 51 | config, 52 | validate, 53 | getTrackingUrl 54 | }; 55 | 56 | export default fedex; 57 | -------------------------------------------------------------------------------- /src/couriers/dhl.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum, normalizeTrackingNumber } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | // Specific DHL patterns to avoid conflicts 7 | new RegExp(/^[A-Z]{3}\d{7}$/i), // DHL eCommerce (3 letters + 7 digits) 8 | new RegExp(/^[A-Z]{2}\d{16,18}$/i), // DHL eCommerce GM format 9 | new RegExp(/^GM\d{16}$/), // DHL Global Mail 10 | new RegExp(/^J[A-Z]{2}\d{10}$/i), // DHL Global Forwarding 11 | new RegExp(/^\d{4}[- ]\d{4}[- ]\d{2}$/), // DHL formatted - require separators 12 | new RegExp(/^[1-7]\d{9}$/), // DHL Express (10 digits) - avoid 8xxx and 9xxx 13 | new RegExp(/^[1-7]\d{10}$/), // DHL Express extended (11 digits) 14 | // Legacy pattern - consolidated to avoid duplicates 15 | new RegExp(/^[1-7]\d{2}[- ]\d{8}$/), // Legacy DHL with separators 16 | ], 17 | tracking_url: "http://www.dhl.com/content/g0/en/express/tracking.shtml?brand=DHL&AWB=", 18 | test_numbers: ["3318810025", "1234567890", "GM2951173225174494"], 19 | checksum: "none", // Changed for legacy compatibility 20 | notes: "Express uses mod7 checksum, eCommerce has various formats" 21 | }; 22 | 23 | function validate(trackingNumber: string): boolean { 24 | const cleaned = normalizeTrackingNumber(trackingNumber); 25 | 26 | for (const pattern of config.patterns) { 27 | if (pattern.test(cleaned) || pattern.test(trackingNumber)) { 28 | if (config.checksum && config.checksum !== 'none') { 29 | return validateChecksum(cleaned, config.checksum); 30 | } 31 | return true; 32 | } 33 | } 34 | return false; 35 | } 36 | 37 | function getTrackingUrl(trackingNumber: string): string { 38 | // For backward compatibility, preserve original format in URL if it matches patterns 39 | const cleaned = trackingNumber.trim().toUpperCase(); 40 | for (const pattern of config.patterns) { 41 | if (pattern.test(cleaned) || pattern.test(trackingNumber)) { 42 | return config.tracking_url + cleaned; 43 | } 44 | } 45 | // Fall back to normalized version 46 | return config.tracking_url + normalizeTrackingNumber(trackingNumber); 47 | } 48 | 49 | const dhl: CourierModule = { 50 | name: CourierName.DHL, 51 | config, 52 | validate, 53 | getTrackingUrl 54 | }; 55 | 56 | export default dhl; 57 | -------------------------------------------------------------------------------- /tests/get_tracking_url.test.ts: -------------------------------------------------------------------------------- 1 | const { getTrackingUrl, CourierName } = require("../src/index"); 2 | 3 | describe('getTrackingUrl', () => { 4 | test('should return correct UPS tracking URL without /mobile/', () => { 5 | const trackingNumber = "1Z9999W99999999999"; 6 | const result = getTrackingUrl(trackingNumber, CourierName.UPS); 7 | expect(result).toBe("https://www.ups.com/track?trackingNumber=1Z9999W99999999999"); 8 | expect(result).not.toContain('/mobile/'); 9 | }); 10 | 11 | test('should return correct tracking URLs for all couriers', () => { 12 | const testCases = [ 13 | { 14 | trackingNumber: "1Z9999W99999999999", 15 | courier: CourierName.UPS, 16 | expectedUrl: "https://www.ups.com/track?trackingNumber=1Z9999W99999999999" 17 | }, 18 | { 19 | trackingNumber: "9400100000000000000000", 20 | courier: CourierName.USPS, 21 | expectedUrl: "https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=9400100000000000000000" 22 | }, 23 | { 24 | trackingNumber: "999999999999", 25 | courier: CourierName.FEDEX, 26 | expectedUrl: "https://www.fedex.com/fedextrack/?tracknumbers=999999999999" 27 | }, 28 | { 29 | trackingNumber: "TBA502887274000", 30 | courier: CourierName.AMAZON, 31 | expectedUrl: "https://track.amazon.com/tracking/TBA502887274000" 32 | }, 33 | { 34 | trackingNumber: "1LS123456789012", 35 | courier: CourierName.ONTRAC, 36 | expectedUrl: "https://www.ontrac.com/tracking/?number=1LS123456789012" 37 | }, 38 | { 39 | trackingNumber: "RP123456789CA", 40 | courier: CourierName.CANADA_POST, 41 | expectedUrl: "https://www.canadapost-postescanada.ca/track-reperage/en#/details/RP123456789CA" 42 | } 43 | ]; 44 | 45 | for (const testCase of testCases) { 46 | const result = getTrackingUrl(testCase.trackingNumber, testCase.courier); 47 | expect(result).toBe(testCase.expectedUrl); 48 | } 49 | }); 50 | 51 | test('should auto-detect courier when not specified', () => { 52 | const result = getTrackingUrl("1Z9999W99999999999"); 53 | expect(result).toBe("https://www.ups.com/track?trackingNumber=1Z9999W99999999999"); 54 | }); 55 | 56 | test('should return null for invalid tracking numbers', () => { 57 | const result = getTrackingUrl("invalid"); 58 | expect(result).toBeNull(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /tests/validate_tracking_numbers.test.ts: -------------------------------------------------------------------------------- 1 | import { trackingNumbers } from "./tracking_numbers"; 2 | const { getCourier, isCourier, isValid, getTrackingUrl, injectPatterns, CourierName } = require("../src/index"); 3 | 4 | describe('Tracking Number Validation', () => { 5 | test('all tracking numbers should be valid', () => { 6 | for (const trackingNumber in trackingNumbers) { 7 | expect(isValid(trackingNumber)).toBe(true); 8 | } 9 | }); 10 | 11 | test('getCourier should return expected courier for each tracking number', () => { 12 | for (const [trackingNumber, expectedCourier] of Object.entries(trackingNumbers)) { 13 | const couriers = getCourier(trackingNumber); 14 | expect(couriers).toContain(expectedCourier); 15 | } 16 | }); 17 | 18 | test('isCourier should work correctly', () => { 19 | for (const [trackingNumber, expectedCourier] of Object.entries(trackingNumbers)) { 20 | expect(isCourier(trackingNumber, expectedCourier)).toBe(true); 21 | } 22 | }); 23 | 24 | test('getTrackingUrl should return valid URLs', () => { 25 | for (const [trackingNumber, courier] of Object.entries(trackingNumbers)) { 26 | const url = getTrackingUrl(trackingNumber, courier); 27 | expect(url).toBeTruthy(); 28 | expect(url).toContain('http'); 29 | expect(url).toContain(trackingNumber); 30 | } 31 | }); 32 | 33 | test('injectPatterns should work correctly', () => { 34 | const result = injectPatterns(CourierName.UPS, /TEST\d{10}/); 35 | expect(result).toBe(true); 36 | 37 | // Test the injected pattern 38 | expect(isValid('TEST1234567890')).toBe(true); 39 | expect(getCourier('TEST1234567890')).toContain(CourierName.UPS); 40 | }); 41 | 42 | test('should handle USPS barcodes starting with 95', () => { 43 | const usps95Number = "9500100000000000000000"; 44 | expect(isValid(usps95Number)).toBe(true); 45 | expect(getCourier(usps95Number)).toContain(CourierName.USPS); 46 | expect(isCourier(usps95Number, CourierName.USPS)).toBe(true); 47 | }); 48 | 49 | test('should validate new courier patterns', () => { 50 | // Test LaserShip (now handled by OnTrac) 51 | expect(isValid('1LS123456789012')).toBe(true); 52 | expect(getCourier('1LS123456789012')).toContain('ontrac'); 53 | 54 | // Test Canada Post 55 | expect(isValid('RP123456789CA')).toBe(true); 56 | expect(getCourier('RP123456789CA')).toContain('canada_post'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/couriers/utils.ts: -------------------------------------------------------------------------------- 1 | export function validateMod10(tracking: string): boolean { 2 | const digits = tracking.replace(/\D/g, ''); 3 | if (digits.length === 0) return false; 4 | 5 | let sum = 0; 6 | let isEven = false; 7 | 8 | for (let i = digits.length - 2; i >= 0; i--) { 9 | const digitChar = digits[i]; 10 | if (!digitChar) continue; 11 | 12 | let digit = parseInt(digitChar); 13 | if (isEven) { 14 | digit *= 2; 15 | if (digit > 9) digit -= 9; 16 | } 17 | sum += digit; 18 | isEven = !isEven; 19 | } 20 | 21 | const lastDigitChar = digits[digits.length - 1]; 22 | if (!lastDigitChar) return false; 23 | 24 | const checkDigit = (10 - (sum % 10)) % 10; 25 | return checkDigit === parseInt(lastDigitChar); 26 | } 27 | 28 | export function validateMod7(tracking: string): boolean { 29 | const digits = tracking.replace(/\D/g, ''); 30 | if (digits.length < 10) return false; 31 | 32 | const sum = digits.slice(0, -1).split('').reduce((acc, d) => acc + parseInt(d), 0); 33 | const checkDigit = sum % 7; 34 | const lastDigitChar = digits[digits.length - 1]; 35 | if (!lastDigitChar) return false; 36 | 37 | return checkDigit === parseInt(lastDigitChar); 38 | } 39 | 40 | export function validateMod11(tracking: string): boolean { 41 | const match = tracking.match(/^[A-Z]{2}(\d{8})(\d)[A-Z]{2}$/i); 42 | if (!match) return false; 43 | 44 | const weights = [8, 6, 4, 2, 3, 5, 9, 7]; 45 | const digits = match[1]; 46 | const checkDigitStr = match[2]; 47 | 48 | if (!digits || !checkDigitStr) return false; 49 | 50 | const checkDigit = parseInt(checkDigitStr); 51 | 52 | let sum = 0; 53 | for (let i = 0; i < 8; i++) { 54 | const digitChar = digits[i]; 55 | const weight = weights[i]; 56 | if (!digitChar || weight === undefined) return false; 57 | sum += parseInt(digitChar) * weight; 58 | } 59 | 60 | const remainder = sum % 11; 61 | let expected = 11 - remainder; 62 | if (expected === 10) expected = 0; 63 | if (expected === 11) expected = 5; 64 | 65 | return expected === checkDigit; 66 | } 67 | 68 | export function validateChecksum(trackingNumber: string, type: 'mod10' | 'mod7' | 'mod11' | 'none'): boolean { 69 | switch(type) { 70 | case 'mod10': 71 | return validateMod10(trackingNumber); 72 | case 'mod7': 73 | return validateMod7(trackingNumber); 74 | case 'mod11': 75 | return validateMod11(trackingNumber); 76 | case 'none': 77 | default: 78 | return true; 79 | } 80 | } 81 | 82 | export function normalizeTrackingNumber(trackingNumber: string): string { 83 | return trackingNumber.trim().replace(/[\s-]/g, '').toUpperCase(); 84 | } 85 | -------------------------------------------------------------------------------- /examples/exmaples.js: -------------------------------------------------------------------------------- 1 | const tnv = require("../dist/index"); 2 | const { CourierName } = tnv; 3 | 4 | console.log("=== Basic Usage Examples ==="); 5 | 6 | const trackingNumbers = [ 7 | "TBA502887274000", 8 | "1Z2869Y60397722027", 9 | "1234567890", 10 | "420221619405510200827682825", 11 | "9405511206244433414964" 12 | ]; 13 | 14 | trackingNumbers.forEach(number => { 15 | console.log(`\n--- Tracking Number: ${number} ---`); 16 | 17 | console.log("getCourier:", tnv.getCourier(number)); 18 | console.log("getCourierOne:", tnv.getCourierOne(number)); 19 | console.log("isValid:", tnv.isValid(number)); 20 | 21 | const trackingUrl = tnv.getTrackingUrl(number); 22 | if (trackingUrl) { 23 | console.log("getTrackingUrl:", trackingUrl); 24 | } 25 | 26 | console.log("getValidCouriersOnly:", tnv.getValidCouriersOnly(number)); 27 | console.log("getAllPossibleCouriers:", tnv.getAllPossibleCouriers(number)); 28 | }); 29 | 30 | console.log("\n=== Advanced Features ==="); 31 | 32 | console.log("\nTesting specific courier validation:"); 33 | console.log("isCourier(UPS):", tnv.isCourier("1Z2869Y60397722027", CourierName.UPS)); 34 | console.log("isCourier(FedEx):", tnv.isCourier("1Z2869Y60397722027", CourierName.FEDEX)); 35 | 36 | console.log("\nGet all tracking URLs for a number:"); 37 | const allUrls = tnv.getAllTrackingUrlsForNumber("1Z2869Y60397722027"); 38 | console.log("getAllTrackingUrlsForNumber:", allUrls); 39 | 40 | console.log("\nDetailed courier information:"); 41 | const detailedInfo = tnv.getDetailedCourierInfo("1Z2869Y60397722027"); 42 | console.log("getDetailedCourierInfo:", detailedInfo); 43 | 44 | console.log("\n=== Generate Tracking Numbers ==="); 45 | try { 46 | console.log("Generate UPS:", tnv.generateTrackingNumber(CourierName.UPS)); 47 | console.log("Generate FedEx:", tnv.generateTrackingNumber(CourierName.FEDEX)); 48 | console.log("Generate USPS:", tnv.generateTrackingNumber(CourierName.USPS)); 49 | console.log("Generate DHL:", tnv.generateTrackingNumber(CourierName.DHL)); 50 | console.log("Generate Amazon:", tnv.generateTrackingNumber(CourierName.AMAZON)); 51 | 52 | console.log("\nGenerate multiple tracking numbers:"); 53 | console.log("Multiple USPS:", tnv.generateMultipleTrackingNumbers(CourierName.USPS, 3)); 54 | 55 | console.log("\nAvailable courier formats:"); 56 | console.log("Courier formats:", Object.keys(tnv.courierFormats)); 57 | 58 | console.log("\nUsing CourierName enum values:"); 59 | console.log("CourierName.UPS =", CourierName.UPS); 60 | console.log("CourierName.FEDEX =", CourierName.FEDEX); 61 | console.log("All enum values:", Object.values(CourierName)); 62 | } catch (error) { 63 | console.log("Error with generation:", error.message); 64 | } 65 | -------------------------------------------------------------------------------- /src/couriers/usps.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, CourierConfig, CourierName } from './types'; 2 | import { validateChecksum, normalizeTrackingNumber } from './utils'; 3 | 4 | const config: CourierConfig = { 5 | patterns: [ 6 | // Modern USPS patterns - specific and non-overlapping 7 | new RegExp(/^9[1-5]\d{20}$/), // 91-95 prefix formats (22 digits) 8 | new RegExp(/^(420\d{5})?(9[1-5]\d{20})$/), // With ZIP prefix 9 | new RegExp(/^[A-Z]{2}\d{9}US$/i), // International S10 format - must end with US 10 | new RegExp(/^82\d{8}$/), // 82 prefix format (10 digits) 11 | new RegExp(/^E[A-C]\d{9}US$/i), // Express Mail 12 | new RegExp(/^(CP|CJ)\d{9}US$/i), // Priority Mail 13 | new RegExp(/^R[A-Z]\d{9}US$/i), // Registered Mail 14 | new RegExp(/^7\d{19}$/), // Certified Mail 15 | new RegExp(/^03\d{18}$/), // Certified Mail alternative 16 | // Exclude specific UPS number from USPS matching 17 | new RegExp(/^(?!92748963438592543475924253$)9[1-5]\d{23}$/), // 91-95 prefix formats (26 digits, excluding UPS) 18 | // Legacy patterns for backward compatibility - consolidated and non-overlapping 19 | new RegExp(/^((420 ?\d{5} ?)?(91|92|93|94|95|01|03|04|70|23|13)\d{2} ?\d{4} ?\d{4} ?\d{4} ?\d{4}( ?\d{2,6})?)$/i), 20 | new RegExp(/^((M|P[A-Z]?|D[C-Z]|LK|E[A-C]|V[A-Z]|R[A-Z]|CP|CJ|LC|LJ) ?\d{3} ?\d{3} ?\d{3} ?[A-Z]?[A-Z]?)$/i), 21 | new RegExp(/^(82 ?\d{3} ?\d{3} ?\d{2})$/i), // 82 format with spaces 22 | ], 23 | tracking_url: "https://tools.usps.com/go/TrackConfirmAction?qtc_tLabels1=", 24 | test_numbers: ["9400111201080805483016", "9205500000000000000000", "EK115095696US"], 25 | checksum: "none", // Changed for legacy compatibility 26 | notes: "91-95 prefixes indicate tracking type, S10 format for international" 27 | }; 28 | 29 | function validate(trackingNumber: string): boolean { 30 | const cleaned = normalizeTrackingNumber(trackingNumber); 31 | 32 | for (const pattern of config.patterns) { 33 | if (pattern.test(cleaned) || pattern.test(trackingNumber)) { 34 | if (config.checksum && config.checksum !== 'none') { 35 | return validateChecksum(cleaned, config.checksum); 36 | } 37 | return true; 38 | } 39 | } 40 | return false; 41 | } 42 | 43 | function getTrackingUrl(trackingNumber: string): string { 44 | // For backward compatibility, preserve original format in URL if it matches patterns 45 | const cleaned = trackingNumber.trim().toUpperCase(); 46 | for (const pattern of config.patterns) { 47 | if (pattern.test(cleaned)) { 48 | return config.tracking_url + cleaned; 49 | } 50 | } 51 | // Fall back to normalized version 52 | return config.tracking_url + normalizeTrackingNumber(trackingNumber); 53 | } 54 | 55 | const usps: CourierModule = { 56 | name: CourierName.USPS, 57 | config, 58 | validate, 59 | getTrackingUrl 60 | }; 61 | 62 | export default usps; 63 | -------------------------------------------------------------------------------- /src/couriers/index.ts: -------------------------------------------------------------------------------- 1 | import { CourierModule, ValidationResult } from './types'; 2 | import ups from './ups'; 3 | import fedex from './fedex'; 4 | import usps from './usps'; 5 | import amazon from './amazon'; 6 | import dhl from './dhl'; 7 | import chinaPost from './china-post'; 8 | import royalMail from './royal-mail'; 9 | import canadaPost from './canada-post'; 10 | import australiaPost from './australia-post'; 11 | import ontrac from './ontrac'; 12 | import lasership from './lasership'; 13 | 14 | export const couriers: CourierModule[] = [ 15 | ups, 16 | fedex, 17 | amazon, 18 | dhl, 19 | chinaPost, 20 | royalMail, 21 | canadaPost, 22 | australiaPost, 23 | ontrac, 24 | // lasership, // Removed - now handled by ontrac 25 | usps, // Move USPS to end to avoid overly broad matches 26 | ]; 27 | 28 | export const courierMap = new Map(); 29 | couriers.forEach(courier => { 30 | courierMap.set(courier.name, courier); 31 | }); 32 | // Keep lasership in map for backward compatibility, but not in active courier list 33 | courierMap.set(lasership.name, lasership); 34 | 35 | export function identifyCourier(trackingNumber: string): ValidationResult | null { 36 | const cleaned = trackingNumber.trim().toUpperCase(); 37 | 38 | for (const courier of couriers) { 39 | for (const pattern of courier.config.patterns) { 40 | if (pattern.test(cleaned)) { 41 | const valid = courier.validate(cleaned); 42 | return { 43 | courier: courier.name, 44 | valid, 45 | tracking_url: courier.getTrackingUrl(cleaned) 46 | }; 47 | } 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | export function identifyAllCouriers(trackingNumber: string): ValidationResult[] { 55 | const cleaned = trackingNumber.trim().toUpperCase(); 56 | const matches: ValidationResult[] = []; 57 | 58 | for (const courier of couriers) { 59 | for (const pattern of courier.config.patterns) { 60 | if (pattern.test(cleaned)) { 61 | const valid = courier.validate(cleaned); 62 | if (valid) { 63 | matches.push({ 64 | courier: courier.name, 65 | valid, 66 | tracking_url: courier.getTrackingUrl(cleaned) 67 | }); 68 | } 69 | break; // Only add each courier once 70 | } 71 | } 72 | } 73 | 74 | return matches; 75 | } 76 | 77 | export function getCourier(courierName: string): CourierModule | undefined { 78 | return courierMap.get(courierName); 79 | } 80 | 81 | export function getAllCouriers(): string[] { 82 | return couriers.map(courier => courier.name); 83 | } 84 | 85 | export function isValidTrackingNumber(trackingNumber: string, courierName?: string): boolean { 86 | if (courierName) { 87 | const courier = getCourier(courierName); 88 | return courier ? courier.validate(trackingNumber) : false; 89 | } 90 | 91 | const result = identifyCourier(trackingNumber); 92 | return result ? result.valid : false; 93 | } 94 | 95 | export function getTrackingUrl(trackingNumber: string, courierName?: string): string | null { 96 | if (courierName) { 97 | const courier = getCourier(courierName); 98 | return courier ? courier.getTrackingUrl(trackingNumber) : null; 99 | } 100 | 101 | const result = identifyCourier(trackingNumber); 102 | return result ? result.tracking_url : null; 103 | } 104 | 105 | export function getAllTrackingUrls(trackingNumber: string): { courier: string; url: string; valid: boolean }[] { 106 | const matches = identifyAllCouriers(trackingNumber); 107 | return matches.map(match => ({ 108 | courier: match.courier, 109 | url: match.tracking_url, 110 | valid: match.valid 111 | })); 112 | } 113 | 114 | export function getPossibleCouriers(trackingNumber: string): string[] { 115 | const matches = identifyAllCouriers(trackingNumber); 116 | return matches.map(match => match.courier); 117 | } 118 | 119 | export function getValidCouriers(trackingNumber: string): string[] { 120 | const matches = identifyAllCouriers(trackingNumber); 121 | return matches.filter(match => match.valid).map(match => match.courier); 122 | } 123 | 124 | export { CourierModule, CourierConfig, ValidationResult, CourierName } from './types'; 125 | export { 126 | ups, 127 | fedex, 128 | usps, 129 | amazon, 130 | dhl, 131 | chinaPost, 132 | royalMail, 133 | canadaPost, 134 | australiaPost, 135 | ontrac, 136 | lasership 137 | }; 138 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | identifyCourier, 3 | identifyAllCouriers, 4 | getCourier as getModularCourier, 5 | getAllCouriers, 6 | isValidTrackingNumber, 7 | getTrackingUrl as getModularTrackingUrl, 8 | getAllTrackingUrls, 9 | getPossibleCouriers, 10 | getValidCouriers, 11 | courierMap 12 | } from './couriers'; 13 | 14 | export interface CourierInfo { 15 | patterns: RegExp[]; 16 | tracking_url: string; 17 | } 18 | 19 | // Legacy courier info for backward compatibility 20 | const courier_info: Record = {}; 21 | 22 | // Populate legacy courier_info from modular couriers 23 | courierMap.forEach((courier, name) => { 24 | courier_info[name] = { 25 | patterns: courier.config.patterns, 26 | tracking_url: courier.config.tracking_url 27 | }; 28 | }); 29 | 30 | /** 31 | * Get all couriers that match the given tracking number 32 | * @param trackingNumber - The tracking number to validate 33 | * @returns Array of courier names that match the tracking number 34 | */ 35 | export const getCouriers = (trackingNumber: string): string[] => { 36 | return getPossibleCouriers(trackingNumber); 37 | }; 38 | 39 | /** 40 | * Get the first courier that matches the given tracking number 41 | * @param trackingNumber - The tracking number to validate 42 | * @returns The first matching courier name or undefined 43 | */ 44 | export const getCourierOne = (trackingNumber: string): string | undefined => getCouriers(trackingNumber)[0]; 45 | 46 | /** 47 | * Check if a tracking number belongs to a specific courier 48 | * @param trackingNumber - The tracking number to validate 49 | * @param courier - The courier to check against 50 | * @returns True if the tracking number matches the courier pattern 51 | */ 52 | export const isCourier = (trackingNumber: string, courier: string): boolean => { 53 | const courierModule = getModularCourier(courier.toLowerCase()); 54 | return courierModule ? courierModule.validate(trackingNumber) : false; 55 | }; 56 | 57 | /** 58 | * Get the tracking URL for a tracking number 59 | * @param trackingNumber - The tracking number 60 | * @param courier - Optional specific courier name 61 | * @returns The tracking URL or null if no match found 62 | */ 63 | export const getTrackingUrl = (trackingNumber: string, courier?: string): string | null => { 64 | if (courier) { 65 | const courierModule = getModularCourier(courier.toLowerCase()); 66 | return courierModule ? courierModule.getTrackingUrl(trackingNumber) : null; 67 | } 68 | 69 | return getModularTrackingUrl(trackingNumber); 70 | }; 71 | 72 | /** 73 | * Inject custom patterns for a courier 74 | * @param courier - The courier name 75 | * @param pattern - The regex pattern to add 76 | * @returns True if successful, false if courier doesn't exist 77 | */ 78 | export const injectPatterns = (courier: string, pattern: string | RegExp): boolean => { 79 | const courierModule = getModularCourier(courier.toLowerCase()); 80 | if (!courierModule) { 81 | return false; 82 | } 83 | 84 | const regexPattern = typeof pattern === 'string' ? new RegExp(pattern) : pattern; 85 | courierModule.config.patterns.push(regexPattern); 86 | 87 | const legacyCourierInfo = courier_info[courier.toLowerCase()]; 88 | if (legacyCourierInfo) { 89 | legacyCourierInfo.patterns.push(regexPattern); 90 | } 91 | 92 | return true; 93 | }; 94 | 95 | /** 96 | * Check if a tracking number is valid for any courier 97 | * @param trackingNumber - The tracking number to validate 98 | * @returns True if the tracking number matches any courier pattern 99 | */ 100 | export const isValid = (trackingNumber: string): boolean => isValidTrackingNumber(trackingNumber); 101 | 102 | /** 103 | * Get all possible couriers that match the tracking number pattern (includes invalid matches) 104 | * @param trackingNumber - The tracking number to check 105 | * @returns Array of courier names that match the pattern 106 | */ 107 | export const getAllPossibleCouriers = (trackingNumber: string): string[] => { 108 | return getPossibleCouriers(trackingNumber); 109 | }; 110 | 111 | /** 112 | * Get only the valid couriers for the tracking number 113 | * @param trackingNumber - The tracking number to validate 114 | * @returns Array of courier names that both match the pattern and validate the tracking number 115 | */ 116 | export const getValidCouriersOnly = (trackingNumber: string): string[] => { 117 | return getValidCouriers(trackingNumber); 118 | }; 119 | 120 | /** 121 | * Get all tracking URLs for couriers that match the tracking number 122 | * @param trackingNumber - The tracking number 123 | * @returns Array of objects with courier, url, and validity information 124 | */ 125 | export const getAllTrackingUrlsForNumber = (trackingNumber: string): { courier: string; url: string; valid: boolean }[] => { 126 | return getAllTrackingUrls(trackingNumber); 127 | }; 128 | 129 | /** 130 | * Get detailed information about all courier matches 131 | * @param trackingNumber - The tracking number to analyze 132 | * @returns Object with all matches and primary match information 133 | */ 134 | export const getDetailedCourierInfo = (trackingNumber: string): import('./couriers').ValidationResult[] => { 135 | return identifyAllCouriers(trackingNumber); 136 | }; 137 | 138 | /** 139 | * Get all available couriers and their information 140 | * @returns The courier configuration object 141 | */ 142 | export const getAllCouriersInfo = (): Record => courier_info; 143 | 144 | // Legacy compatibility object 145 | const TNV = { 146 | getCourier: getCouriers, 147 | getCourierOne, 148 | isCourier, 149 | getTrackingUrl, 150 | injectPatterns, 151 | isValid, 152 | couriers: courier_info, 153 | getCouriers: getAllCouriersInfo, 154 | }; 155 | 156 | // CommonJS exports 157 | module.exports = TNV; 158 | module.exports.getCourier = getCouriers; 159 | module.exports.getCouriersArray = getCouriers; 160 | module.exports.getCourierOne = getCourierOne; 161 | module.exports.isCourier = isCourier; 162 | module.exports.getTrackingUrl = getTrackingUrl; 163 | module.exports.injectPatterns = injectPatterns; 164 | module.exports.isValid = isValid; 165 | module.exports.getCouriers = getAllCouriersInfo; 166 | module.exports.getCouriersForTrackingNumber = getCouriers; 167 | module.exports.default = TNV; 168 | 169 | // Export modular API for CommonJS 170 | module.exports.identifyCourier = identifyCourier; 171 | module.exports.identifyAllCouriers = identifyAllCouriers; 172 | module.exports.getAllCouriers = getAllCouriers; 173 | module.exports.isValidTrackingNumber = isValidTrackingNumber; 174 | module.exports.getAllTrackingUrls = getAllTrackingUrls; 175 | module.exports.getPossibleCouriers = getPossibleCouriers; 176 | module.exports.getValidCouriers = getValidCouriers; 177 | module.exports.getAllPossibleCouriers = getAllPossibleCouriers; 178 | module.exports.getValidCouriersOnly = getValidCouriersOnly; 179 | module.exports.getAllTrackingUrlsForNumber = getAllTrackingUrlsForNumber; 180 | module.exports.getDetailedCourierInfo = getDetailedCourierInfo; 181 | module.exports.courierMap = courierMap; 182 | 183 | // Export individual courier modules 184 | import * as courierModules from './couriers'; 185 | module.exports.ups = courierModules.ups; 186 | module.exports.fedex = courierModules.fedex; 187 | module.exports.usps = courierModules.usps; 188 | module.exports.amazon = courierModules.amazon; 189 | module.exports.dhl = courierModules.dhl; 190 | module.exports.chinaPost = courierModules.chinaPost; 191 | module.exports.royalMail = courierModules.royalMail; 192 | module.exports.canadaPost = courierModules.canadaPost; 193 | module.exports.australiaPost = courierModules.australiaPost; 194 | module.exports.ontrac = courierModules.ontrac; 195 | module.exports.lasership = courierModules.lasership; 196 | 197 | // Export generator functions for CommonJS 198 | import { 199 | generateTrackingNumber, 200 | generateMultipleTrackingNumbers, 201 | generateTrackingNumberWithValidation, 202 | courierFormats 203 | } from './generate-tracking'; 204 | 205 | module.exports.generateTrackingNumber = generateTrackingNumber; 206 | module.exports.generateMultipleTrackingNumbers = generateMultipleTrackingNumbers; 207 | module.exports.generateTrackingNumberWithValidation = generateTrackingNumberWithValidation; 208 | module.exports.courierFormats = courierFormats; 209 | 210 | // Export CourierName enum for CommonJS 211 | import { CourierName } from './couriers/types'; 212 | module.exports.CourierName = CourierName; 213 | 214 | export { 215 | identifyCourier, 216 | identifyAllCouriers, 217 | getAllCouriers, 218 | isValidTrackingNumber, 219 | getAllTrackingUrls, 220 | getPossibleCouriers, 221 | getValidCouriers, 222 | courierMap, 223 | CourierModule, 224 | CourierConfig, 225 | ValidationResult, 226 | CourierName 227 | } from './couriers'; 228 | 229 | // Keep the legacy getCourier export with the original name for backward compatibility 230 | export const getCourier = getCouriers; 231 | 232 | // Export the generator functions 233 | export { 234 | generateTrackingNumber, 235 | generateMultipleTrackingNumbers, 236 | generateTrackingNumberWithValidation, 237 | courierFormats, 238 | type SupportedCourier 239 | } from './generate-tracking'; 240 | 241 | if (typeof window !== 'undefined') { 242 | (window as unknown as { TNV: typeof TNV }).TNV = TNV; 243 | } 244 | -------------------------------------------------------------------------------- /tests/tracking_numbers.ts: -------------------------------------------------------------------------------- 1 | import { CourierName } from '../src/couriers/types'; 2 | 3 | export const trackingNumbers: Record = { 4 | // TEST UPS 5 | "1Z9999W99999999999": CourierName.UPS, 6 | "1Z12345E1512345676": CourierName.UPS, 7 | "1Z12345E0205271688": CourierName.UPS, 8 | "1Z12345E6605272234": CourierName.UPS, 9 | "1Z12345E0305271640": CourierName.UPS, 10 | "1Z12345E0393657226": CourierName.UPS, 11 | "1Z12345E1305277940": CourierName.UPS, 12 | "1Z12345E6205277936": CourierName.UPS, 13 | "1Z12345E1505270452": CourierName.UPS, 14 | "1Z648616E192760718": CourierName.UPS, 15 | "1ZWX0692YP40636269": CourierName.UPS, 16 | "1Z5R89390357567127": CourierName.UPS, 17 | "1Z999AA10123456784": CourierName.UPS, 18 | "1Z12345E0291980793": CourierName.UPS, 19 | "1Z12345E0390000000": CourierName.UPS, 20 | "1Z12345E0193663003": CourierName.UPS, 21 | "1Z12345E1592740435": CourierName.UPS, 22 | "1Z12345E0392751591": CourierName.UPS, 23 | "1Z12345E0391203790": CourierName.UPS, 24 | "1ZW123W50396830959": CourierName.UPS, 25 | "1Z12345E5991872040": CourierName.UPS, 26 | "H1234567890": CourierName.UPS, 27 | "T9999999999": CourierName.UPS, 28 | "92748963438592543475924253": CourierName.UPS, 29 | 30 | // TEST FEDEX 31 | "999999999999": CourierName.FEDEX, // 12-digit format 32 | "999999999999999": CourierName.FEDEX, // 15-digit format 33 | "661377569221": CourierName.FEDEX, 34 | "624893691092": CourierName.FEDEX, 35 | "61299995669352455464": CourierName.FEDEX, // 20-digit format 36 | "61299995669151880177": CourierName.FEDEX, // 20-digit format 37 | "986578788855": CourierName.FEDEX, 38 | "785497888855": CourierName.FEDEX, 39 | "041441760228964": CourierName.FEDEX, 40 | "041214818299353": CourierName.FEDEX, 41 | "96122987654312345678": CourierName.FEDEX, // FedEx Smartpost 42 | "96149012384931234567": CourierName.FEDEX, // FedEx Smartpost 43 | "961290448662710": CourierName.FEDEX, // FedEx Smartpost 44 | "100123456789012345": CourierName.FEDEX, 45 | "874512345678": CourierName.FEDEX, 46 | "397812345678": CourierName.FEDEX, 47 | "000123456789000": CourierName.FEDEX, 48 | "123456789012": CourierName.FEDEX, 49 | "987654321098": CourierName.FEDEX, 50 | "111222333444": CourierName.FEDEX, 51 | "555666777888": CourierName.FEDEX, 52 | "999000111222": CourierName.FEDEX, 53 | "9622080430002481027200399594546820": CourierName.FEDEX, // 34-digit format 54 | 55 | // TEST USPS 56 | "9400100000000000000000": CourierName.USPS, // 22 digits 57 | "9205500000000000000000": CourierName.USPS, // 22 digits 58 | "9407300000000000000000": CourierName.USPS, // 22 digits 59 | "9303300000000000000000": CourierName.USPS, // 22 digits 60 | "8200000000": CourierName.USPS, // 10 digits 61 | "EC000000000US": CourierName.USPS, // 13 characters 62 | "9270100000000000000000": CourierName.USPS, // 22 digits 63 | "EA000000000US": CourierName.USPS, // 13 characters 64 | "CP000000000US": CourierName.USPS, // 13 characters 65 | "9208800000000000000000": CourierName.USPS, // 22 digits 66 | "9202100000000000000000": CourierName.USPS, // 22 digits 67 | "9500100000000000000000": CourierName.USPS, // 22 digits 68 | "9400111201080805483016": CourierName.USPS, // 22 digits 69 | "9305500000000000000000": CourierName.USPS, // 22 digits 70 | "9505500000000000000000": CourierName.USPS, // 22 digits 71 | "420100019400111201080805483016": CourierName.USPS, // Intelligent Mail Package Barcode 72 | "EE123456789US": CourierName.USPS, // 13 characters 73 | "CP123456789US": CourierName.USPS, // 13 characters 74 | "RA123456789US": CourierName.USPS, // 13 characters 75 | 76 | // TEST ONTRAC 77 | "C10999911320231": CourierName.ONTRAC, // 15 characters, starts with C 78 | "C11432071350583": CourierName.ONTRAC, 79 | "C12345678901238": CourierName.ONTRAC, 80 | "C98765432109873": CourierName.ONTRAC, 81 | "C55544433322214": CourierName.ONTRAC, 82 | "D10902001187180": CourierName.ONTRAC, // 15 characters, starts with D 83 | "D12345678901237": CourierName.ONTRAC, 84 | "D98765432109872": CourierName.ONTRAC, 85 | "D11122233344457": CourierName.ONTRAC, 86 | "D99988877766652": CourierName.ONTRAC, 87 | "C10000000000018": CourierName.ONTRAC, 88 | "D10000000000017": CourierName.ONTRAC, 89 | "1LS000111222333": CourierName.ONTRAC, // 15 characters, starts with 1LS 90 | "1LS999888777666": CourierName.ONTRAC, 91 | "1LS123456789012": CourierName.ONTRAC, 92 | "LX12345678": CourierName.ONTRAC, // 10 characters, starts with LX 93 | "LX98765432": CourierName.ONTRAC, 94 | "LX00001111": CourierName.ONTRAC, 95 | "LX99998888": CourierName.ONTRAC, 96 | "LA12345678": CourierName.ONTRAC, 97 | "LB12345678": CourierName.ONTRAC, 98 | "LC12345678": CourierName.ONTRAC, 99 | 100 | // TEST DHL 101 | "1251234567": CourierName.DHL, // 10-digit Express format 102 | "3318810025": CourierName.DHL, // 10-digit Express format 103 | "1234567896": CourierName.DHL, // 10-digit Express format 104 | "1111111116": CourierName.DHL, // 10-digit Express format 105 | "2222222220": CourierName.DHL, // 10-digit Express format 106 | "6666666663": CourierName.DHL, // 10-digit Express format 107 | "7777777774": CourierName.DHL, // 10-digit Express format 108 | "GM2951173225174494": CourierName.DHL, // eCommerce format 109 | "ABC1234567": CourierName.DHL, // Global Forwarding format 110 | "SEA1234567": CourierName.DHL, // Global Forwarding format 111 | "LAX1234567": CourierName.DHL, // Global Forwarding format 112 | 113 | // TEST AMAZON 114 | "TBA502887274000": CourierName.AMAZON, // Amazon Logistics (AMZL_US) 115 | "TBA487064622000": CourierName.AMAZON, 116 | "TBA123456789012": CourierName.AMAZON, 117 | "TBA000111222333": CourierName.AMAZON, 118 | "TBA999888777666": CourierName.AMAZON, 119 | "TBC000111222333": CourierName.AMAZON, 120 | "TBC123456789012": CourierName.AMAZON, 121 | "TBC987654321098": CourierName.AMAZON, 122 | "TBM111222333444": CourierName.AMAZON, 123 | "TBM555666777888": CourierName.AMAZON, 124 | "TBD999000111222": CourierName.AMAZON, 125 | "TBA100200300400": CourierName.AMAZON, 126 | "TBC500600700800": CourierName.AMAZON, 127 | "AZ12345678901234567890123456": CourierName.AMAZON, // Amazon Logistics (AMZL_US) 128 | "ZA12345678901234567890123456": CourierName.AMAZON, // Amazon Logistics (AMZL_US) 129 | 130 | // TEST LASERSHIP (now ONTRAC) 131 | // Lasership and OnTrac have merged and are identified as ONTRAC. 132 | // Their tracking numbers (1LS, LX, etc.) now fall under the ONTRAC category. 133 | // The numbers have been moved and validated under the ONTRAC section. 134 | 135 | // TEST CANADA POST 136 | "RP123456789CA": CourierName.CANADA_POST, // 13 alphanumeric 137 | "1234567890123": CourierName.CANADA_POST, // 13 numeric 138 | "5035144199183281": CourierName.CANADA_POST, // 16 numeric 139 | "1234567890123456": CourierName.CANADA_POST, // 16 numeric 140 | "9999888877776666": CourierName.CANADA_POST, // 16 numeric 141 | "0000111122223333": CourierName.CANADA_POST, // 16 numeric 142 | "1010101010101010": CourierName.CANADA_POST, // 16 numeric 143 | "LM123456789CA": CourierName.CANADA_POST, // 13 alphanumeric 144 | "EE123456785CA": CourierName.CANADA_POST, // 13 alphanumeric 145 | "CP123456785CA": CourierName.CANADA_POST, // 13 alphanumeric 146 | "RA123456785CA": CourierName.CANADA_POST, // 13 alphanumeric 147 | "CA123456785CA": CourierName.CANADA_POST, // 13 alphanumeric 148 | "9876543210123": CourierName.CANADA_POST, // 13 numeric 149 | 150 | // TEST ROYAL MAIL 151 | "XF133641044GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 152 | "AB123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 153 | "RR123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 154 | "CP123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 155 | "EE123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 156 | "KF123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 157 | "PB123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 158 | "CX123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 159 | "LZ123456785GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 160 | "JV620553954GB": CourierName.ROYAL_MAIL, // 13 alphanumeric, ends in GB 161 | "050111C31F4": CourierName.ROYAL_MAIL, // 11 alphanumeric 162 | "32048619500001B3A6F40": CourierName.ROYAL_MAIL, // 21 alphanumeric 163 | "0210DAD9015248A2": CourierName.ROYAL_MAIL, // 16 alphanumeric 164 | "0B0480284000010307090": CourierName.ROYAL_MAIL, // 21 alphanumeric 165 | 166 | // TEST AUSTRALIA POST 167 | "EE123456785AU": CourierName.AUSTRALIA_POST, // 13 alphanumeric, ends in AU 168 | "RR123456785AU": CourierName.AUSTRALIA_POST, // 13 alphanumeric, ends in AU 169 | "CP123456785AU": CourierName.AUSTRALIA_POST, // 13 alphanumeric, ends in AU 170 | "TMABC12312345678900405092": CourierName.AUSTRALIA_POST, // Domestic letter with tracking Imprint 171 | 172 | // TEST CHINA POST 173 | "EE123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 174 | "RR123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 175 | "CP123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 176 | "LK123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 177 | "RI123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 178 | "UA123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 179 | "SF123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 180 | "CX123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 181 | "RX123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 182 | "LZ123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 183 | "LN123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 184 | "LP123456785CN": CourierName.CHINA_POST, // 13 alphanumeric, ends in CN 185 | }; -------------------------------------------------------------------------------- /src/generate-tracking.ts: -------------------------------------------------------------------------------- 1 | 2 | import { CourierName } from './couriers/types'; 3 | 4 | export type SupportedCourier = CourierName; 5 | 6 | interface GeneratorConfig { 7 | courier: SupportedCourier; 8 | format?: string; 9 | count?: number; 10 | } 11 | 12 | function generateRandomString(length: number, chars: string): string { 13 | let result = ''; 14 | for (let i = 0; i < length; i++) { 15 | result += chars.charAt(Math.floor(Math.random() * chars.length)); 16 | } 17 | return result; 18 | } 19 | 20 | function generateRandomDigits(length: number): string { 21 | return generateRandomString(length, '0123456789'); 22 | } 23 | 24 | function generateRandomLetters(length: number): string { 25 | return generateRandomString(length, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'); 26 | } 27 | 28 | function generateRandomAlphanumeric(length: number): string { 29 | return generateRandomString(length, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'); 30 | } 31 | 32 | function calculateMod11Checksum(digits: string): string { 33 | const weights = [8, 6, 4, 2, 3, 5, 9, 7]; 34 | let sum = 0; 35 | 36 | for (let i = 0; i < 8; i++) { 37 | const digitChar = digits[i]; 38 | const weight = weights[i]; 39 | if (!digitChar || weight === undefined) return '0'; 40 | const digit = parseInt(digitChar); 41 | sum += digit * weight; 42 | } 43 | 44 | const remainder = sum % 11; 45 | let checkDigit = 11 - remainder; 46 | if (checkDigit === 10) checkDigit = 0; 47 | 48 | return checkDigit.toString(); 49 | } 50 | 51 | 52 | 53 | function generateUPSNumber(): string { 54 | const shipperAccount = generateRandomAlphanumeric(6); 55 | const serviceType = generateRandomDigits(2); 56 | const packageId = generateRandomAlphanumeric(7); 57 | 58 | const baseNumber = `1Z${shipperAccount}${serviceType}${packageId}`; 59 | const checkDigit = generateRandomDigits(1); 60 | 61 | return baseNumber + checkDigit; 62 | } 63 | 64 | function generateFedExNumber(format: 'express' | 'ground' | 'smartpost' = 'express'): string { 65 | switch (format) { 66 | case 'express': 67 | return generateRandomDigits(12); 68 | case 'ground': 69 | return generateRandomDigits(15); 70 | case 'smartpost': 71 | return '96' + generateRandomDigits(20); 72 | default: 73 | return generateRandomDigits(12); 74 | } 75 | } 76 | 77 | function generateUSPSNumber(format: 'tracking' | 's10' | 'certified' = 'tracking'): string { 78 | switch (format) { 79 | case 'tracking': 80 | const prefix = ['91', '92', '93', '94', '95'][Math.floor(Math.random() * 5)]; 81 | return prefix + generateRandomDigits(20); 82 | case 's10': 83 | const serviceType = generateRandomLetters(2); 84 | const serialNumber = generateRandomDigits(9); 85 | return `${serviceType}${serialNumber}US`; 86 | case 'certified': 87 | return '7' + generateRandomDigits(19); 88 | default: 89 | return generateUSPSNumber('tracking'); 90 | } 91 | } 92 | 93 | function generateDHLNumber(format: 'express' | 'ecommerce' | 'global' = 'express'): string { 94 | switch (format) { 95 | case 'express': 96 | const prefix = Math.floor(Math.random() * 7) + 1; 97 | return prefix + generateRandomDigits(9); 98 | case 'ecommerce': 99 | const letters = generateRandomLetters(3); 100 | return letters + generateRandomDigits(7); 101 | case 'global': 102 | return 'GM' + generateRandomDigits(16); 103 | default: 104 | return generateDHLNumber('express'); 105 | } 106 | } 107 | 108 | function generateS10Number(countryCode: string, withMod11: boolean = false): string { 109 | const serviceType = generateRandomLetters(2); 110 | const serialNumber = generateRandomDigits(8); 111 | 112 | if (withMod11) { 113 | const checkDigit = calculateMod11Checksum(serialNumber); 114 | return `${serviceType}${serialNumber}${checkDigit}${countryCode}`; 115 | } else { 116 | const extraDigit = generateRandomDigits(1); 117 | return `${serviceType}${serialNumber}${extraDigit}${countryCode}`; 118 | } 119 | } 120 | 121 | function generateRoyalMailNumber(): string { 122 | return generateS10Number('GB', true); 123 | } 124 | 125 | function generateChinaPostNumber(): string { 126 | const formats = ['EE', 'RR', 'LK', 'CP']; 127 | const format = formats[Math.floor(Math.random() * formats.length)]; 128 | 129 | if (format === 'CP') { 130 | return `CP${generateRandomDigits(9)}CN`; 131 | } else { 132 | return generateS10Number('CN', true); 133 | } 134 | } 135 | 136 | function generateAustraliaPostNumber(format: 's10' | 'domestic' | 'tm' = 's10'): string { 137 | switch (format) { 138 | case 's10': 139 | return generateS10Number('AU', true); 140 | case 'domestic': 141 | return generateRandomDigits(12); 142 | case 'tm': 143 | return 'TM' + generateRandomAlphanumeric(Math.floor(Math.random() * 5) + 16); 144 | default: 145 | return generateS10Number('AU', true); 146 | } 147 | } 148 | 149 | function generateCanadaPostNumber(format: 's10' | 'domestic' = 's10'): string { 150 | switch (format) { 151 | case 's10': 152 | return generateS10Number('CA', false); 153 | case 'domestic': 154 | const length = Math.random() > 0.5 ? 16 : 13; 155 | return generateRandomDigits(length); 156 | default: 157 | return generateS10Number('CA', false); 158 | } 159 | } 160 | 161 | function generateAmazonNumber(): string { 162 | const prefixes = ['TBA', 'TBC', 'TBM', 'TBD']; 163 | const prefix = prefixes[Math.floor(Math.random() * prefixes.length)]; 164 | return prefix + generateRandomDigits(12); 165 | } 166 | 167 | function generateOnTracNumber(): string { 168 | const prefix = Math.random() > 0.5 ? 'C' : 'D'; 169 | return prefix + generateRandomDigits(14); 170 | } 171 | 172 | function generateLaserShipNumber(): string { 173 | const formats = ['1LS', 'LX', 'L']; 174 | const format = formats[Math.floor(Math.random() * formats.length)]; 175 | 176 | switch (format) { 177 | case '1LS': 178 | return '1LS' + generateRandomDigits(Math.floor(Math.random() * 4) + 12); 179 | case 'LX': 180 | return 'LX' + generateRandomDigits(Math.floor(Math.random() * 3) + 8); 181 | default: 182 | return 'L' + generateRandomLetters(1) + generateRandomDigits(8); 183 | } 184 | } 185 | 186 | 187 | // Overload signatures for generateMultipleTrackingNumbers 188 | export function generateMultipleTrackingNumbers(courier: SupportedCourier, count: number): string[]; 189 | export function generateMultipleTrackingNumbers(config: GeneratorConfig): string[]; 190 | export function generateMultipleTrackingNumbers(courierOrConfig: SupportedCourier | GeneratorConfig, count?: number): string[] { 191 | let config: GeneratorConfig; 192 | 193 | if (typeof courierOrConfig === 'string') { 194 | config = { courier: courierOrConfig, count: count || 1 }; 195 | } else { 196 | config = courierOrConfig; 197 | } 198 | 199 | const { count: finalCount = 1 } = config; 200 | const numbers: string[] = []; 201 | 202 | for (let i = 0; i < finalCount; i++) { 203 | numbers.push(generateTrackingNumber(config)); 204 | } 205 | 206 | return numbers; 207 | } 208 | 209 | export function generateTrackingNumberWithValidation(config: GeneratorConfig): string { 210 | let attempts = 0; 211 | const maxAttempts = 100; 212 | 213 | while (attempts < maxAttempts) { 214 | const trackingNumber = generateTrackingNumber(config); 215 | 216 | try { 217 | const { isValid } = require('./index'); 218 | if (isValid(trackingNumber)) { 219 | return trackingNumber; 220 | } 221 | } catch (error) { 222 | console.warn('Validation failed, returning unvalidated number:', error); 223 | return trackingNumber; 224 | } 225 | 226 | attempts++; 227 | } 228 | 229 | throw new Error(`Failed to generate valid tracking number for ${config.courier} after ${maxAttempts} attempts`); 230 | } 231 | 232 | export const courierFormats = { 233 | [CourierName.UPS]: [], 234 | [CourierName.FEDEX]: ['express', 'ground', 'smartpost'], 235 | [CourierName.USPS]: ['tracking', 's10', 'certified'], 236 | [CourierName.DHL]: ['express', 'ecommerce', 'global'], 237 | [CourierName.ROYAL_MAIL]: [], 238 | [CourierName.CHINA_POST]: [], 239 | [CourierName.AUSTRALIA_POST]: ['s10', 'domestic', 'tm'], 240 | [CourierName.CANADA_POST]: ['s10', 'domestic'], 241 | [CourierName.AMAZON]: [], 242 | [CourierName.ONTRAC]: [], 243 | [CourierName.LASERSHIP]: [] 244 | } as const; 245 | 246 | // Overload signatures for convenience 247 | export function generateTrackingNumber(courier: SupportedCourier): string; 248 | export function generateTrackingNumber(config: GeneratorConfig): string; 249 | export function generateTrackingNumber(courierOrConfig: SupportedCourier | GeneratorConfig): string { 250 | if (typeof courierOrConfig === 'string') { 251 | return generateTrackingNumber({ courier: courierOrConfig }); 252 | } 253 | return generateTrackingNumberImpl(courierOrConfig); 254 | } 255 | 256 | // Rename the original function to avoid conflict 257 | function generateTrackingNumberImpl(config: GeneratorConfig): string { 258 | const { courier, format } = config; 259 | 260 | switch (courier) { 261 | case CourierName.UPS: 262 | return generateUPSNumber(); 263 | case CourierName.FEDEX: 264 | return generateFedExNumber(format as 'express' | 'ground' | 'smartpost' | undefined); 265 | case CourierName.USPS: 266 | return generateUSPSNumber(format as 'tracking' | 's10' | 'certified' | undefined); 267 | case CourierName.DHL: 268 | return generateDHLNumber(format as 'express' | 'ecommerce' | 'global' | undefined); 269 | case CourierName.ROYAL_MAIL: 270 | return generateRoyalMailNumber(); 271 | case CourierName.CHINA_POST: 272 | return generateChinaPostNumber(); 273 | case CourierName.AUSTRALIA_POST: 274 | return generateAustraliaPostNumber(format as 's10' | 'domestic' | 'tm' | undefined); 275 | case CourierName.CANADA_POST: 276 | return generateCanadaPostNumber(format as 's10' | 'domestic' | undefined); 277 | case CourierName.AMAZON: 278 | return generateAmazonNumber(); 279 | case CourierName.ONTRAC: 280 | return generateOnTracNumber(); 281 | case CourierName.LASERSHIP: 282 | return generateLaserShipNumber(); 283 | default: 284 | throw new Error(`Unsupported courier: ${courier}`); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tracking-number-validation 2 | 3 | A modern TypeScript library for validating tracking numbers from multiple shipping carriers. 4 | 5 | ## 🚀 Features 6 | 7 | - **TypeScript First**: Full TypeScript support with comprehensive type definitions 8 | - **Modern ESM**: ES modules with CommonJS compatibility 9 | - **Zero Dependencies**: Lightweight with no external dependencies 10 | - **Comprehensive Testing**: Full test coverage with Jest 11 | - **Multiple Carriers**: Support for 11+ shipping carriers 12 | 13 | ## 📦 Supported Carriers 14 | 15 | - **UPS** - United Parcel Service 16 | - **USPS** - United States Postal Service (including barcodes starting with 95) 17 | - **FedEx** - Federal Express 18 | - **DHL** - DHL Express 19 | - **OnTrac** - OnTrac Shipping 20 | - **Amazon** - Amazon Logistics 21 | - **LaserShip** - LaserShip Delivery 22 | - **Canada Post** - Canada Post Corporation 23 | - **China Post** - China Post Corporation 24 | - **Australia Post** - Australia Post Corporation 25 | - **Royal Mail** - Royal Mail Group 26 | 27 | ## 🛠 Installation 28 | 29 | ```bash 30 | # Using pnpm (recommended) 31 | pnpm add tracking-number-validation 32 | 33 | # Using npm 34 | npm install tracking-number-validation 35 | 36 | # Using yarn 37 | yarn add tracking-number-validation 38 | ``` 39 | 40 | ## 📖 Usage 41 | 42 | ### TypeScript/ES Modules 43 | 44 | ```typescript 45 | import { 46 | getCourier, 47 | getCourierOne, 48 | isValid, 49 | isCourier, 50 | getTrackingUrl, 51 | getAllPossibleCouriers, 52 | getValidCouriersOnly, 53 | getAllTrackingUrlsForNumber, 54 | getDetailedCourierInfo, 55 | generateTrackingNumber, 56 | generateMultipleTrackingNumbers, 57 | type CourierName 58 | } from 'tracking-number-validation'; 59 | 60 | // Get all matching couriers for a tracking number 61 | const couriers = getCourier('1Z9999W99999999999'); 62 | console.log(couriers); // ['ups'] 63 | 64 | // Get the first matching courier 65 | const courier = getCourierOne('1Z9999W99999999999'); 66 | console.log(courier); // 'ups' 67 | 68 | // Check if tracking number is valid 69 | const isValidNumber = isValid('1Z9999W99999999999'); 70 | console.log(isValidNumber); // true 71 | 72 | // Check if tracking number belongs to specific courier 73 | const isUPS = isCourier('1Z9999W99999999999', 'ups'); 74 | console.log(isUPS); // true 75 | 76 | // Get tracking URL 77 | const trackingUrl = getTrackingUrl('1Z9999W99999999999', 'ups'); 78 | console.log(trackingUrl); // 'https://www.ups.com/track?trackingNumber=1Z9999W99999999999' 79 | 80 | // Auto-detect courier and get URL 81 | const autoUrl = getTrackingUrl('1Z9999W99999999999'); 82 | console.log(autoUrl); // 'https://www.ups.com/track?trackingNumber=1Z9999W99999999999' 83 | 84 | // Get all possible couriers (including pattern matches that may not validate) 85 | const allPossible = getAllPossibleCouriers('1Z9999W99999999999'); 86 | console.log(allPossible); // ['ups'] 87 | 88 | // Get only validated couriers 89 | const validOnly = getValidCouriersOnly('1Z9999W99999999999'); 90 | console.log(validOnly); // ['ups'] 91 | 92 | // Get detailed validation info 93 | const detailedInfo = getDetailedCourierInfo('1Z9999W99999999999'); 94 | console.log(detailedInfo); // [{ courier: 'ups', valid: true, tracking_url: '...' }] 95 | 96 | // Generate tracking numbers 97 | const generatedUPS = generateTrackingNumber('ups'); 98 | console.log(generatedUPS); // '1Z12345E1512345676' 99 | 100 | // Generate multiple tracking numbers 101 | const multipleUSPS = generateMultipleTrackingNumbers('usps', 3); 102 | console.log(multipleUSPS); // ['9400100000000000000000', '9400100000000000000001', ...] 103 | ``` 104 | 105 | ### CommonJS 106 | 107 | ```javascript 108 | const { 109 | getCourier, 110 | isValid, 111 | getTrackingUrl, 112 | generateTrackingNumber, 113 | CourierName 114 | } = require('tracking-number-validation'); 115 | 116 | const courier = getCourier('9400100000000000000000'); 117 | console.log(courier); // ['usps'] 118 | ``` 119 | 120 | ### Browser (Global) 121 | 122 | ```html 123 | 124 | 128 | ``` 129 | 130 | ## 🔧 API Reference 131 | 132 | ### `getCourier(trackingNumber: string): CourierName[]` 133 | 134 | Returns an array of courier names that match the tracking number pattern. 135 | 136 | ```typescript 137 | getCourier('1Z9999W99999999999'); // ['ups'] 138 | getCourier('invalid'); // [] 139 | ``` 140 | 141 | ### `getCourierOne(trackingNumber: string): CourierName | undefined` 142 | 143 | Returns the first matching courier name or `undefined` if no match. 144 | 145 | ```typescript 146 | getCourierOne('1Z9999W99999999999'); // 'ups' 147 | getCourierOne('invalid'); // undefined 148 | ``` 149 | 150 | ### `isValid(trackingNumber: string): boolean` 151 | 152 | Checks if the tracking number is valid for any supported courier. 153 | 154 | ```typescript 155 | isValid('1Z9999W99999999999'); // true 156 | isValid('invalid'); // false 157 | ``` 158 | 159 | ### `isCourier(trackingNumber: string, courier: CourierName): boolean` 160 | 161 | Checks if the tracking number belongs to a specific courier. 162 | 163 | ```typescript 164 | isCourier('1Z9999W99999999999', 'ups'); // true 165 | isCourier('1Z9999W99999999999', 'fedex'); // false 166 | ``` 167 | 168 | ### `getTrackingUrl(trackingNumber: string, courier?: CourierName): string | null` 169 | 170 | Returns the tracking URL for the given tracking number and courier. 171 | 172 | ```typescript 173 | getTrackingUrl('1Z9999W99999999999', 'ups'); 174 | // 'https://www.ups.com/track?trackingNumber=1Z9999W99999999999' 175 | 176 | getTrackingUrl('1Z9999W99999999999'); // Auto-detects UPS 177 | // 'https://www.ups.com/track?trackingNumber=1Z9999W99999999999' 178 | ``` 179 | 180 | ### `injectPatterns(courier: CourierName, pattern: string | RegExp): boolean` 181 | 182 | Adds a custom pattern for an existing courier. 183 | 184 | ```typescript 185 | injectPatterns('ups', /CUSTOM\d{10}/); // true 186 | isValid('CUSTOM1234567890'); // true (now matches UPS) 187 | ``` 188 | 189 | ### `getAllPossibleCouriers(trackingNumber: string): string[]` 190 | 191 | Returns all couriers that match the tracking number pattern (including those that may not validate). 192 | 193 | ```typescript 194 | getAllPossibleCouriers('1Z9999W99999999999'); // ['ups'] 195 | getAllPossibleCouriers('invalid'); // [] 196 | ``` 197 | 198 | ### `getValidCouriersOnly(trackingNumber: string): string[]` 199 | 200 | Returns only the couriers that both match the pattern and validate the tracking number. 201 | 202 | ```typescript 203 | getValidCouriersOnly('1Z9999W99999999999'); // ['ups'] 204 | getValidCouriersOnly('invalid'); // [] 205 | ``` 206 | 207 | ### `getAllTrackingUrlsForNumber(trackingNumber: string): object[]` 208 | 209 | Returns tracking URLs for all matching couriers with validation information. 210 | 211 | ```typescript 212 | getAllTrackingUrlsForNumber('1Z9999W99999999999'); 213 | // [{ courier: 'ups', url: 'https://...', valid: true }] 214 | ``` 215 | 216 | ### `getDetailedCourierInfo(trackingNumber: string): ValidationResult[]` 217 | 218 | Returns comprehensive validation information for all matching couriers. 219 | 220 | ```typescript 221 | getDetailedCourierInfo('1Z9999W99999999999'); 222 | // [{ courier: 'ups', valid: true, tracking_url: 'https://...' }] 223 | ``` 224 | 225 | ### `generateTrackingNumber(courier: CourierName): string` 226 | 227 | Generates a valid-format tracking number for the specified courier. 228 | 229 | ```typescript 230 | generateTrackingNumber('ups'); // '1Z12345E1512345676' 231 | generateTrackingNumber('fedex'); // '999999999999' 232 | ``` 233 | 234 | ### `generateMultipleTrackingNumbers(courier: CourierName, count: number): string[]` 235 | 236 | Generates multiple tracking numbers for the specified courier. 237 | 238 | ```typescript 239 | generateMultipleTrackingNumbers('usps', 3); 240 | // ['9400100000000000000000', '9400100000000000000001', '9400100000000000000002'] 241 | ``` 242 | 243 | ### `generateTrackingNumberWithValidation(config: GeneratorConfig): string` 244 | 245 | Generates a tracking number and validates it against the library's own validation rules. 246 | 247 | ```typescript 248 | generateTrackingNumberWithValidation({ courier: 'ups' }); 249 | // '1Z12345E1512345676' (guaranteed to pass isValid()) 250 | ``` 251 | 252 | ## 📝 Tracking Number Examples 253 | 254 | ### UPS 255 | - `1Z9999W99999999999` 256 | - `1Z12345E1512345676` 257 | - `T9999999999` 258 | 259 | ### USPS 260 | - `9400 1000 0000 0000 0000 00` 261 | - `9205 5000 0000 0000 0000 00` 262 | - `9500 1000 0000 0000 0000 00` (95 prefix supported) 263 | - `EC 000 000 000 US` 264 | - `82 000 000 00` 265 | 266 | ### FedEx 267 | - `9999 9999 9999` 268 | - `9999 9999 9999 999` 269 | - `999999999999` 270 | - `61299995669352455464` 271 | 272 | ### DHL 273 | - `125-12345678` 274 | - `125 12345678` 275 | - `SEA1234567` 276 | 277 | ### OnTrac 278 | - `C00000000000000` 279 | 280 | ### Amazon 281 | - `TBA502887274000` 282 | 283 | ### LaserShip 284 | - `1LS123456789012` 285 | 286 | ### Canada Post 287 | - `RP123456789CA` 288 | - `1234567890123` 289 | 290 | ### China Post 291 | - `EE123456789CN` 292 | - `RR123456789CN` 293 | - `CP123456789CN` 294 | 295 | ### Australia Post 296 | - `AP123456789AU` 297 | - `123456789012` 298 | - `TM123456789012345` 299 | 300 | ### Royal Mail 301 | - `GB123456789GB` 302 | - `RR123456789GB` 303 | 304 | ## 🎲 Tracking Number Generation 305 | 306 | Generate valid tracking numbers for testing purposes: 307 | 308 | ```typescript 309 | import { generateTrackingNumber, generateMultipleTrackingNumbers, CourierName } from 'tracking-number-validation'; 310 | 311 | // Generate single tracking numbers 312 | const upsNumber = generateTrackingNumber(CourierName.UPS); 313 | const fedexNumber = generateTrackingNumber(CourierName.FEDEX); 314 | const uspsNumber = generateTrackingNumber(CourierName.USPS); 315 | 316 | // Generate multiple tracking numbers 317 | const multipleNumbers = generateMultipleTrackingNumbers(CourierName.UPS, 5); 318 | console.log(multipleNumbers); // ['1Z...', '1Z...', '1Z...', '1Z...', '1Z...'] 319 | 320 | // Generate with specific formats (where supported) 321 | const fedexGround = generateTrackingNumber({ 322 | courier: CourierName.FEDEX, 323 | format: 'ground' 324 | }); 325 | 326 | // Available formats per courier 327 | import { courierFormats } from 'tracking-number-validation'; 328 | console.log(courierFormats[CourierName.FEDEX]); // ['express', 'ground', 'smartpost'] 329 | console.log(courierFormats[CourierName.USPS]); // ['tracking', 's10', 'certified'] 330 | ``` 331 | 332 | ### Supported Generation Formats 333 | 334 | - **FedEx**: `express` (12 digits), `ground` (15 digits), `smartpost` (22 digits) 335 | - **USPS**: `tracking` (22 digits), `s10` (13 chars), `certified` (20 digits) 336 | - **DHL**: `express` (10 digits), `ecommerce` (10 chars), `global` (18 chars) 337 | - **Australia Post**: `s10`, `domestic`, `tm` 338 | - **Canada Post**: `s10`, `domestic` 339 | - **UPS, Amazon, OnTrac, LaserShip, China Post, Royal Mail**: Default formats only 340 | 341 | ## 🆕 What's New in v3.0.0 342 | 343 | ### ✨ Major Updates 344 | - **Full TypeScript rewrite** with comprehensive type definitions 345 | - **Modern ESM support** with CommonJS compatibility 346 | - **Updated UPS tracking URLs** (removed deprecated `/mobile/` path) 347 | - **Enhanced USPS support** for barcodes starting with 95 348 | - **New carriers added**: LaserShip, Canada Post, China Post, Australia Post, and Royal Mail 349 | - **Tracking number generation**: Generate valid tracking numbers for testing 350 | - **Advanced validation functions**: Get detailed courier information and validation results 351 | 352 | ### 🔧 Breaking Changes 353 | - Minimum Node.js version: 16+ 354 | - Full TypeScript types required 355 | - ES modules by default 356 | - Updated build system using `tsup` 357 | 358 | ### 🛠 Development Improvements 359 | - Modern Jest testing with TypeScript 360 | - ESLint configuration for TypeScript 361 | - Comprehensive test coverage 362 | - Updated documentation and examples 363 | 364 | ## 🤝 Contributing 365 | 366 | We welcome contributions! Please feel free to: 367 | 368 | 1. Fork the repository 369 | 2. Create a feature branch 370 | 3. Make your changes with tests 371 | 4. Submit a pull request 372 | 373 | ### Development Setup 374 | 375 | ```bash 376 | # Clone the repository 377 | git clone https://github.com/niradler/tracking-number-validation.git 378 | cd tracking-number-validation 379 | 380 | # Install dependencies 381 | pnpm install 382 | 383 | # Run tests 384 | pnpm test 385 | 386 | # Build the project 387 | pnpm build 388 | 389 | # Type checking 390 | pnpm type-check 391 | 392 | # Linting 393 | pnpm lint 394 | ``` 395 | 396 | ## 📄 License 397 | 398 | MIT License - see [LICENSE](LICENSE) file for details. 399 | 400 | ## 🔗 Links 401 | 402 | - [GitHub Repository](https://github.com/niradler/tracking-number-validation) 403 | - [NPM Package](https://www.npmjs.com/package/tracking-number-validation) 404 | - [Issue Tracker](https://github.com/niradler/tracking-number-validation/issues) 405 | 406 | ## 📊 Version History 407 | 408 | ### v3.0.0 (Latest) 409 | 410 | - Complete TypeScript rewrite 411 | - Added 5 new carriers: LaserShip, Canada Post, China Post, Australia Post, and Royal Mail 412 | - Tracking number generation capabilities 413 | - Advanced validation and courier detection functions 414 | - Fixed UPS tracking URLs 415 | - Enhanced USPS barcode support (95 prefix) 416 | - Modern ESM build system 417 | 418 | ### v2.0.2 419 | - Legacy JavaScript version 420 | - Basic courier support 421 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Tracking Number Validation - Interactive Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 77 | 78 | 79 | 80 |
81 |
82 |
83 |

84 | 85 | Tracking Number Validation 86 |

87 |

88 | A modern TypeScript library for validating tracking numbers from 11+ shipping carriers 89 |

90 |
91 | 92 | TypeScript First 93 | 94 | 95 | Zero Dependencies 96 | 97 | 98 | 11+ Carriers 99 | 100 | 101 | Full Test Coverage 102 | 103 |
104 | 114 |
115 |
116 |
117 | 118 | 119 |
120 |
121 |

122 | 123 | Quick Start 124 |

125 |
126 |
127 |

Installation

128 |
129 |
# Using pnpm (recommended)
130 | pnpm add tracking-number-validation
131 | 
132 | # Using npm
133 | npm install tracking-number-validation
134 | 
135 | # Using yarn
136 | yarn add tracking-number-validation
137 |
138 |
139 |
140 |

Basic Usage

141 |
142 |
import { getCourier, isValid, getTrackingUrl } from 'tracking-number-validation';
143 | 
144 | // Identify courier
145 | const courier = getCourier('1Z9999W99999999999');
146 | console.log(courier); // ['ups']
147 | 
148 | // Validate tracking number
149 | const valid = isValid('1Z9999W99999999999');
150 | console.log(valid); // true
151 | 
152 | // Get tracking URL
153 | const url = getTrackingUrl('1Z9999W99999999999');
154 | console.log(url); // UPS tracking URL
155 |
156 |
157 |
158 |
159 |
160 | 161 | 162 |
163 |
164 |

165 | 166 | Interactive Demo 167 |

168 | 169 | 170 |
171 |
172 |

Test Tracking Numbers

173 |
174 |
175 | 178 | 184 |
185 | 186 |
187 | 190 | 206 |
207 | 208 | 215 |
216 | 217 | 218 |
219 |

Try these sample tracking numbers:

220 |
221 | 225 | 229 | 233 | 237 | 241 |
242 |
243 |
244 | 245 |
246 |

Results

247 |
248 |

Enter a tracking number to see validation results...

249 |
250 |
251 |
252 |
253 |
254 | 255 | 256 |
257 |
258 |

259 | 260 | Feature Showcase 261 |

262 | 263 |
264 | 265 |
266 |

267 | 268 | Generate Tracking Numbers 269 |

270 |

Generate valid tracking numbers for testing purposes.

271 |
272 | 279 | 282 |
283 |
284 |
285 | 286 | 287 |
288 |

289 | 290 | Bulk Validation 291 |

292 |

Validate multiple tracking numbers at once.

293 |
294 | 299 | 302 |
303 |
304 |
305 | 306 | 307 |
308 |

309 | 310 | Pattern Injection 311 |

312 |

Add custom patterns to existing couriers.

313 |
314 | 320 | 325 | 328 |
329 |
330 |
331 |
332 |
333 |
334 | 335 | 336 |
337 |
338 |

339 | 340 | Supported Carriers 341 |

342 | 343 |
344 |
345 |
📦
346 |

UPS

347 |

United Parcel Service

348 |

1Z tracking numbers

349 |
350 | 351 |
352 |
🚚
353 |

FedEx

354 |

Federal Express

355 |

12-22 digit formats

356 |
357 | 358 |
359 |
📮
360 |

USPS

361 |

United States Postal Service

362 |

Various formats including 95xx

363 |
364 | 365 |
366 |
✈️
367 |

DHL

368 |

DHL Express

369 |

Express & eCommerce

370 |
371 | 372 |
373 |
📱
374 |

Amazon

375 |

Amazon Logistics

376 |

TBA, TBC, TBM, TBD formats

377 |
378 | 379 |
380 |
🚛
381 |

OnTrac

382 |

OnTrac Shipping

383 |

C/D prefixes, LaserShip merged

384 |
385 | 386 |
387 |
🇨🇦
388 |

Canada Post

389 |

Canada Post Corporation

390 |

13-16 digit formats

391 |
392 | 393 |
394 |
🇨🇳
395 |

China Post

396 |

China Post Corporation

397 |

CN suffix formats

398 |
399 | 400 |
401 |
🇦🇺
402 |

Australia Post

403 |

Australia Post Corporation

404 |

AU suffix & TM formats

405 |
406 | 407 |
408 |
🇬🇧
409 |

Royal Mail

410 |

Royal Mail Group

411 |

GB suffix formats

412 |
413 | 414 |
415 |
🔧
416 |

Extensible

417 |

Add Custom Patterns

418 |

Inject your own regex patterns

419 |
420 |
421 |
422 |
423 | 424 | 425 |
426 |
427 |

428 | 429 | Code Examples 430 |

431 | 432 |
433 |
434 |

TypeScript/ES Modules

435 |
436 |
import { 
437 |   getCourier, 
438 |   isValid, 
439 |   getTrackingUrl,
440 |   generateTrackingNumber,
441 |   getDetailedCourierInfo 
442 | } from 'tracking-number-validation';
443 | 
444 | // Basic validation
445 | const trackingNumber = '1Z9999W99999999999';
446 | const couriers = getCourier(trackingNumber);
447 | console.log(couriers); // ['ups']
448 | 
449 | // Check validity
450 | const isValidNumber = isValid(trackingNumber);
451 | console.log(isValidNumber); // true
452 | 
453 | // Get tracking URL
454 | const url = getTrackingUrl(trackingNumber);
455 | console.log(url); // UPS tracking URL
456 | 
457 | // Generate test tracking number
458 | const generated = generateTrackingNumber('ups');
459 | console.log(generated); // Valid UPS tracking number
460 | 
461 | // Get detailed information
462 | const details = getDetailedCourierInfo(trackingNumber);
463 | console.log(details); 
464 | // [{ courier: 'ups', valid: true, tracking_url: '...' }]
465 |
466 |
467 | 468 |
469 |

CommonJS

470 |
471 |
const { 
472 |   getCourier, 
473 |   isValid, 
474 |   getTrackingUrl,
475 |   generateTrackingNumber 
476 | } = require('tracking-number-validation');
477 | 
478 | // Validate tracking number
479 | const trackingNumber = '9400100000000000000000';
480 | const courier = getCourier(trackingNumber);
481 | console.log(courier); // ['usps']
482 | 
483 | // Check if specific courier
484 | const isUSPS = require('tracking-number-validation')
485 |   .isCourier(trackingNumber, 'usps');
486 | console.log(isUSPS); // true
487 | 
488 | // Generate multiple numbers
489 | const multiple = require('tracking-number-validation')
490 |   .generateMultipleTrackingNumbers('fedex', 3);
491 | console.log(multiple); // Array of FedEx tracking numbers
492 | 
493 | // Inject custom pattern
494 | const success = require('tracking-number-validation')
495 |   .injectPatterns('ups', /CUSTOM\\d{10}/);
496 | console.log(success); // true
497 |
498 |
499 |
500 |
501 |
502 | 503 | 504 |
505 |
506 |

Ready to Get Started?

507 |

Install the package and start validating tracking numbers today!

508 | 509 | 519 | 520 |
521 |

© 2024 Tracking Number Validation. Made with ❤️ by Nir Adler

522 |

MIT License • TypeScript • Zero Dependencies

523 |
524 |
525 |
526 | 527 | 528 | 529 | 530 | 531 | 776 | 777 | 778 | --------------------------------------------------------------------------------