From c65195d26d14618ce1c57336b67e548fc43f042f Mon Sep 17 00:00:00 2001 From: Duhan BALCI Date: Thu, 1 Jan 2026 18:30:21 +0300 Subject: [PATCH] init --- .gitignore | 9 + README.md | 80 + package-lock.json | 2465 +++++++++++++++++++++++ package.json | 51 + src/auth.ts | 52 + src/client.ts | 88 + src/errors.ts | 47 + src/http.ts | 190 ++ src/index.ts | 22 + src/services/bin-check.ts | 31 + src/services/cancel-refund.ts | 76 + src/services/card-storage.ts | 79 + src/services/installment.ts | 31 + src/services/payment.ts | 117 ++ src/services/reporting.ts | 81 + src/services/subscription.ts | 537 +++++ src/types/bin-check.ts | 83 + src/types/cancel-refund.ts | 97 + src/types/card-storage.ts | 112 + src/types/common.ts | 165 ++ src/types/index.ts | 25 + src/types/payment.ts | 148 ++ src/types/reporting.ts | 98 + src/types/subscription.ts | 511 +++++ src/utils.ts | 28 + tests/fixtures/card.ts | 49 + tests/fixtures/payment.ts | 206 ++ tests/fixtures/subscription.ts | 193 ++ tests/integration/bin-check.test.ts | 224 ++ tests/integration/cancel-refund.test.ts | 367 ++++ tests/integration/card-storage.test.ts | 391 ++++ tests/integration/payment.test.ts | 373 ++++ tests/integration/reporting.test.ts | 338 ++++ tests/integration/subscription.test.ts | 556 +++++ tests/setup.ts | 85 + tests/unit/auth.test.ts | 62 + tests/unit/client.test.ts | 97 + tests/unit/errors.test.ts | 76 + tests/unit/http.test.ts | 310 +++ tests/unit/index.test.ts | 40 + tests/unit/utils.test.ts | 53 + tsconfig.json | 27 + tsdown.config.ts | 14 + vitest.config.ts | 31 + 44 files changed, 8715 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/auth.ts create mode 100644 src/client.ts create mode 100644 src/errors.ts create mode 100644 src/http.ts create mode 100644 src/index.ts create mode 100644 src/services/bin-check.ts create mode 100644 src/services/cancel-refund.ts create mode 100644 src/services/card-storage.ts create mode 100644 src/services/installment.ts create mode 100644 src/services/payment.ts create mode 100644 src/services/reporting.ts create mode 100644 src/services/subscription.ts create mode 100644 src/types/bin-check.ts create mode 100644 src/types/cancel-refund.ts create mode 100644 src/types/card-storage.ts create mode 100644 src/types/common.ts create mode 100644 src/types/index.ts create mode 100644 src/types/payment.ts create mode 100644 src/types/reporting.ts create mode 100644 src/types/subscription.ts create mode 100644 src/utils.ts create mode 100644 tests/fixtures/card.ts create mode 100644 tests/fixtures/payment.ts create mode 100644 tests/fixtures/subscription.ts create mode 100644 tests/integration/bin-check.test.ts create mode 100644 tests/integration/cancel-refund.test.ts create mode 100644 tests/integration/card-storage.test.ts create mode 100644 tests/integration/payment.test.ts create mode 100644 tests/integration/reporting.test.ts create mode 100644 tests/integration/subscription.test.ts create mode 100644 tests/setup.ts create mode 100644 tests/unit/auth.test.ts create mode 100644 tests/unit/client.test.ts create mode 100644 tests/unit/errors.test.ts create mode 100644 tests/unit/http.test.ts create mode 100644 tests/unit/index.test.ts create mode 100644 tests/unit/utils.test.ts create mode 100644 tsconfig.json create mode 100644 tsdown.config.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..735ef47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +coverage/ +dist/ +.env +.DS_Store +.vscode/ +.idea/ +npm-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..648cf58 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# @iyzico/payment + +TypeScript SDK for iyzico payment gateway. + +## Installation + +```bash +npm install @iyzico/payment +``` + +## Usage + +```typescript +import { IyzicoClient } from '@iyzico/payment'; + +const client = new IyzicoClient({ + apiKey: 'your-api-key', + secretKey: 'your-secret-key', + baseUrl: 'https://sandbox-api.iyzipay.com', // optional, defaults to sandbox +}); + +// Create a payment +const payment = await client.payment.create({ + price: '100.00', + paidPrice: '100.00', + currency: 'TRY', + installment: 1, + basketId: 'B12345', + paymentCard: { + cardHolderName: 'John Doe', + cardNumber: '5528790000000008', + expireMonth: '12', + expireYear: '2030', + cvc: '123', + }, + buyer: { /* ... */ }, + shippingAddress: { /* ... */ }, + billingAddress: { /* ... */ }, + basketItems: [ /* ... */ ], +}); + +// Check BIN +const binInfo = await client.binCheck.check({ binNumber: '552879' }); + +// Get installment options +const installments = await client.installment.check({ + binNumber: '552879', + price: '100.00', +}); +``` + +## Services + +- `payment` - 3DS and Non-3DS payments +- `binCheck` - BIN number lookup +- `installment` - Installment options +- `cardStorage` - Save and manage cards +- `cancelRefund` - Cancellations and refunds +- `reporting` - Transaction reports +- `subscription` - Subscription management + +## Configuration + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| apiKey | string | - | API key (required) | +| secretKey | string | - | Secret key (required) | +| baseUrl | string | sandbox | API base URL | +| locale | 'tr' \| 'en' | 'tr' | Response locale | +| retryOnRateLimit | boolean | false | Retry on 429 | +| maxRetries | number | 3 | Max retry attempts | + +## Requirements + +- Node.js 18+ +- TypeScript 5.0+ (recommended) + +## License + +MIT diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ae63235 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2465 @@ +{ + "name": "@iyzico/payment", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@iyzico/payment", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@faker-js/faker": "^10.1.0", + "@types/node": "^25.0.3", + "@vitest/coverage-v8": "^4.0.16", + "dotenv": "^17.2.3", + "tsdown": "^0.18.4", + "typescript": "^5.9.3", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@faker-js/faker": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz", + "integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.0.tgz", + "integrity": "sha512-Fq6DJW+Bb5jaWE69/qOE0D1TUN9+6uWhCeZpdnSBk14pjLcCWR7Q8n49PTSPHazM37JqrsdpEthXy2xn6jWWiA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.103.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.103.0.tgz", + "integrity": "sha512-bkiYX5kaXWwUessFRSoXFkGIQTmc6dLGdxuRTrC+h8PSnIdZyuXHHlLAeTmOue5Br/a0/a7dHH0Gca6eXn9MKg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.57.tgz", + "integrity": "sha512-GoOVDy8bjw9z1K30Oo803nSzXJS/vWhFijFsW3kzvZCO8IZwFnNa6pGctmbbJstKl3Fv6UBwyjJQN6msejW0IQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.57.tgz", + "integrity": "sha512-9c4FOhRGpl+PX7zBK5p17c5efpF9aSpTPgyigv57hXf5NjQUaJOOiejPLAtFiKNBIfm5Uu6yFkvLKzOafNvlTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.57.tgz", + "integrity": "sha512-6RsB8Qy4LnGqNGJJC/8uWeLWGOvbRL/KG5aJ8XXpSEupg/KQtlBEiFaYU/Ma5Usj1s+bt3ItkqZYAI50kSplBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.57.tgz", + "integrity": "sha512-uA9kG7+MYkHTbqwv67Tx+5GV5YcKd33HCJIi0311iYBd25yuwyIqvJfBdt1VVB8tdOlyTb9cPAgfCki8nhwTQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.57.tgz", + "integrity": "sha512-3KkS0cHsllT2T+Te+VZMKHNw6FPQihYsQh+8J4jkzwgvAQpbsbXmrqhkw3YU/QGRrD8qgcOvBr6z5y6Jid+rmw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.57.tgz", + "integrity": "sha512-A3/wu1RgsHhqP3rVH2+sM81bpk+Qd2XaHTl8LtX5/1LNR7QVBFBCpAoiXwjTdGnI5cMdBVi7Z1pi52euW760Fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.57.tgz", + "integrity": "sha512-d0kIVezTQtazpyWjiJIn5to8JlwfKITDqwsFv0Xc6s31N16CD2PC/Pl2OtKgS7n8WLOJbfqgIp5ixYzTAxCqMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.57.tgz", + "integrity": "sha512-E199LPijo98yrLjPCmETx8EF43sZf9t3guSrLee/ej1rCCc3zDVTR4xFfN9BRAapGVl7/8hYqbbiQPTkv73kUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.57.tgz", + "integrity": "sha512-++EQDpk/UJ33kY/BNsh7A7/P1sr/jbMuQ8cE554ZIy+tCUWCivo9zfyjDUoiMdnxqX6HLJEqqGnbGQOvzm2OMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.57.tgz", + "integrity": "sha512-voDEBcNqxbUv/GeXKFtxXVWA+H45P/8Dec4Ii/SbyJyGvCqV1j+nNHfnFUIiRQ2Q40DwPe/djvgYBs9PpETiMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.57.tgz", + "integrity": "sha512-bRhcF7NLlCnpkzLVlVhrDEd0KH22VbTPkPTbMjlYvqhSmarxNIq5vtlQS8qmV7LkPKHrNLWyJW/V/sOyFba26Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.57.tgz", + "integrity": "sha512-rnDVGRks2FQ2hgJ2g15pHtfxqkGFGjJQUDWzYznEkE8Ra2+Vag9OffxdbJMZqBWXHVM0iS4dv8qSiEn7bO+n1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.57.tgz", + "integrity": "sha512-OqIUyNid1M4xTj6VRXp/Lht/qIP8fo25QyAZlCP+p6D2ATCEhyW4ZIFLnC9zAGN/HMbXoCzvwfa8Jjg/8J4YEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.57.tgz", + "integrity": "sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz", + "integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.16", + "ast-v8-to-istanbul": "^0.3.8", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.16", + "vitest": "4.0.16" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz", + "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz", + "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.16", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz", + "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz", + "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.16", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz", + "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz", + "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz", + "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.16", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-kit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-2.2.0.tgz", + "integrity": "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.10.tgz", + "integrity": "sha512-p4K7vMz2ZSk3wN8l5o3y2bJAoZXT3VuJI5OLTATY/01CYWumWvwkUw0SqDBnNq6IiTO3qDa1eSQDibAV8g7XOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dts-resolver": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-2.1.3.tgz", + "integrity": "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hookable": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.0.1.tgz", + "integrity": "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-without-cache": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.2.5.tgz", + "integrity": "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.1.tgz", + "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-beta.57", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.57.tgz", + "integrity": "sha512-lMMxcNN71GMsSko8RyeTaFoATHkCh4IWU7pYF73ziMYjhHZWfVesC6GQ+iaJCvZmVjvgSks9Ks1aaqEkBd8udg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.103.0", + "@rolldown/pluginutils": "1.0.0-beta.57" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-beta.57", + "@rolldown/binding-darwin-arm64": "1.0.0-beta.57", + "@rolldown/binding-darwin-x64": "1.0.0-beta.57", + "@rolldown/binding-freebsd-x64": "1.0.0-beta.57", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.57", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.57", + "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.57", + "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.57", + "@rolldown/binding-linux-x64-musl": "1.0.0-beta.57", + "@rolldown/binding-openharmony-arm64": "1.0.0-beta.57", + "@rolldown/binding-wasm32-wasi": "1.0.0-beta.57", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.57", + "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.57" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.20.0.tgz", + "integrity": "sha512-cLAY1kN2ilTYMfZcFlGWbXnu6Nb+8uwUBsi+Mjbh4uIx7IN8uMOmJ7RxrrRgPsO4H7eSz3E+JwGoL1gyugiyUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.28.5", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "ast-kit": "^2.2.0", + "birpc": "^4.0.0", + "dts-resolver": "^2.1.3", + "get-tsconfig": "^4.13.0", + "obug": "^2.1.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", + "@typescript/native-preview": ">=7.0.0-dev.20250601.1", + "rolldown": "^1.0.0-beta.57", + "typescript": "^5.0.0", + "vue-tsc": "~3.2.0" + }, + "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsdown": { + "version": "0.18.4", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.18.4.tgz", + "integrity": "sha512-J/tRS6hsZTkvqmt4+xdELUCkQYDuUCXgBv0fw3ImV09WPGbEKfsPD65E+WUjSu3E7Z6tji9XZ1iWs8rbGqB/ZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.2.0", + "cac": "^6.7.14", + "defu": "^6.1.4", + "empathic": "^2.0.0", + "hookable": "^6.0.1", + "import-without-cache": "^0.2.5", + "obug": "^2.1.1", + "picomatch": "^4.0.3", + "rolldown": "1.0.0-beta.57", + "rolldown-plugin-dts": "^0.20.0", + "semver": "^7.7.3", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.4.2", + "unrun": "^0.2.21" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@vitejs/devtools": "*", + "publint": "^0.3.0", + "typescript": "^5.0.0", + "unplugin-lightningcss": "^0.4.0", + "unplugin-unused": "^0.5.0" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-lightningcss": { + "optional": true + }, + "unplugin-unused": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unconfig-core": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.4.2.tgz", + "integrity": "sha512-VgPCvLWugINbXvMQDf8Jh0mlbvNjNC6eSUziHsBCMpxR05OPrNrvDnyatdMjRgcHaaNsCqz+wjNXxNw1kRLHUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrun": { + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.21.tgz", + "integrity": "sha512-VuwI4YKtwBpDvM7hCEop2Im/ezS82dliqJpkh9pvS6ve8HcUsBDvESHxMmUfImXR03GkmfdDynyrh/pUJnlguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rolldown": "1.0.0-beta.57" + }, + "bin": { + "unrun": "dist/cli.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/Gugustinette" + }, + "peerDependencies": { + "synckit": "^0.11.11" + }, + "peerDependenciesMeta": { + "synckit": { + "optional": true + } + } + }, + "node_modules/vite": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz", + "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz", + "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.16", + "@vitest/mocker": "4.0.16", + "@vitest/pretty-format": "4.0.16", + "@vitest/runner": "4.0.16", + "@vitest/snapshot": "4.0.16", + "@vitest/spy": "4.0.16", + "@vitest/utils": "4.0.16", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.16", + "@vitest/browser-preview": "4.0.16", + "@vitest/browser-webdriverio": "4.0.16", + "@vitest/ui": "4.0.16", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1b0eb1b --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "@iyzico/payment", + "version": "1.0.0", + "description": "Iyzico payment system TypeScript library", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.cts", + "exports": { + ".": { + "types": { + "import": "./dist/index.d.mts", + "require": "./dist/index.d.cts" + }, + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "watch": "tsdown --watch", + "dev": "tsdown --watch", + "prepublishOnly": "npm run build", + "test": "vitest run", + "test:unit": "vitest run tests/unit", + "test:integration": "vitest run tests/integration", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" + }, + "keywords": [ + "iyzico", + "payment", + "typescript" + ], + "author": "", + "license": "MIT", + "devDependencies": { + "@faker-js/faker": "^10.1.0", + "@types/node": "^25.0.3", + "@vitest/coverage-v8": "^4.0.16", + "dotenv": "^17.2.3", + "tsdown": "^0.18.4", + "typescript": "^5.9.3", + "vitest": "^4.0.16" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/src/auth.ts b/src/auth.ts new file mode 100644 index 0000000..6dcf694 --- /dev/null +++ b/src/auth.ts @@ -0,0 +1,52 @@ +/** + * HMAC SHA256 authentication utilities for İyzico API + */ + +import { createHmac } from 'crypto'; +import { generateRandomKey } from './utils'; + +/** + * Generates authorization header for İyzico API requests + * @param apiKey - API key + * @param secretKey - Secret key + * @param uriPath - Request URI path (e.g., '/payment/bin/check') + * @param requestBody - Request body object (will be JSON stringified) + * @param randomKey - Optional random key (if not provided, will be generated) + * @returns Authorization header value: 'IYZWSv2 {base64EncodedAuthorization}' + */ +export function generateAuthorization( + apiKey: string, + secretKey: string, + uriPath: string, + requestBody?: unknown, + randomKey?: string +): { authorization: string; randomKey: string } { + // Generate random key if not provided + const rndKey = randomKey || generateRandomKey(); + + // Create payload: randomKey + uriPath + requestBody + let payload = rndKey + uriPath; + if (requestBody !== undefined && requestBody !== null) { + // JSON stringify with no spaces (as per İyzico documentation) + const bodyString = JSON.stringify(requestBody); + payload += bodyString; + } + + // Generate HMAC SHA256 hash + const encryptedData = createHmac('sha256', secretKey) + .update(payload) + .digest('hex'); + + // Create authorization string + const authorizationString = `apiKey:${apiKey}&randomKey:${rndKey}&signature:${encryptedData}`; + + // Base64 encode + const base64EncodedAuthorization = Buffer.from(authorizationString).toString('base64'); + + // Return with IYZWSv2 prefix (note: space between prefix and encoded string) + return { + authorization: `IYZWSv2 ${base64EncodedAuthorization}`, + randomKey: rndKey, + }; +} + diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..babae76 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,88 @@ +/** + * İyzico Client - Main client class + */ + +import type { Locale } from './types'; +import { PaymentService } from './services/payment'; +import { BinCheckService } from './services/bin-check'; +import { CancelRefundService } from './services/cancel-refund'; +import { CardStorageService } from './services/card-storage'; +import { ReportingService } from './services/reporting'; +import { InstallmentService } from './services/installment'; +import { SubscriptionService } from './services/subscription'; + +export interface IyzicoClientConfig { + apiKey: string; + secretKey: string; + baseUrl?: string; + locale?: Locale; + retryOnRateLimit?: boolean; + maxRetries?: number; + retryDelay?: number; +} + +const DEFAULT_SANDBOX_URL = 'https://sandbox-api.iyzipay.com'; +const DEFAULT_PRODUCTION_URL = 'https://api.iyzipay.com'; + +/** + * İyzico API Client + */ +export class IyzicoClient { + public readonly apiKey: string; + public readonly secretKey: string; + public readonly baseUrl: string; + private readonly locale: Locale; + public readonly retryOnRateLimit: boolean; + public readonly maxRetries: number; + public readonly retryDelay: number; + + // Services + public readonly payment: PaymentService; + public readonly binCheck: BinCheckService; + public readonly cancelRefund: CancelRefundService; + public readonly cardStorage: CardStorageService; + public readonly reporting: ReportingService; + public readonly installment: InstallmentService; + public readonly subscription: SubscriptionService; + + constructor(config: IyzicoClientConfig) { + if (!config.apiKey) { + throw new Error('API key is required'); + } + if (!config.secretKey) { + throw new Error('Secret key is required'); + } + + this.apiKey = config.apiKey; + this.secretKey = config.secretKey; + this.baseUrl = config.baseUrl || DEFAULT_SANDBOX_URL; + this.locale = config.locale || 'tr'; + this.retryOnRateLimit = config.retryOnRateLimit ?? false; + this.maxRetries = config.maxRetries ?? 3; + this.retryDelay = config.retryDelay ?? 1000; + + // Initialize services + this.payment = new PaymentService(this); + this.binCheck = new BinCheckService(this); + this.cancelRefund = new CancelRefundService(this); + this.cardStorage = new CardStorageService(this); + this.reporting = new ReportingService(this); + this.installment = new InstallmentService(this); + this.subscription = new SubscriptionService(this); + } + + /** + * Gets the current locale + */ + getLocale(): Locale { + return this.locale; + } + + /** + * Gets the base URL + */ + getBaseUrl(): string { + return this.baseUrl; + } +} + diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..1e57897 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,47 @@ +/** + * Custom error classes for İyzico API + */ + +import type { ErrorResponse } from './types'; + +/** + * Base error class for all İyzico errors + */ +export class IyzicoError extends Error { + constructor( + message: string, + public readonly code?: string, + public readonly originalError?: unknown + ) { + super(message); + this.name = 'IyzicoError'; + Object.setPrototypeOf(this, IyzicoError.prototype); + } +} + +/** + * Request error - thrown when request validation fails + */ +export class IyzicoRequestError extends IyzicoError { + constructor(message: string, originalError?: unknown) { + super(message, 'REQUEST_ERROR', originalError); + this.name = 'IyzicoRequestError'; + Object.setPrototypeOf(this, IyzicoRequestError.prototype); + } +} + +/** + * Response error - thrown when API returns an error response + */ +export class IyzicoResponseError extends IyzicoError { + constructor( + message: string, + public readonly errorResponse: ErrorResponse, + originalError?: unknown + ) { + super(message, errorResponse.errorCode, originalError); + this.name = 'IyzicoResponseError'; + Object.setPrototypeOf(this, IyzicoResponseError.prototype); + } +} + diff --git a/src/http.ts b/src/http.ts new file mode 100644 index 0000000..13b1594 --- /dev/null +++ b/src/http.ts @@ -0,0 +1,190 @@ +/** + * HTTP request handler for İyzico API + */ + +import type { BaseResponse, ErrorResponse } from './types'; +import { + IyzicoError, + IyzicoRequestError, + IyzicoResponseError, +} from './errors'; +import { generateAuthorization } from './auth'; + +export interface HttpRequestOptions { + method: 'GET' | 'POST' | 'DELETE'; + path: string; + body?: unknown; + apiKey: string; + secretKey: string; + baseUrl: string; + retryOnRateLimit?: boolean; + maxRetries?: number; + retryDelay?: number; +} + +/** + * Sleep utility for retry delays + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Checks if an error is a rate limit error + */ +function isRateLimitError(error: unknown): boolean { + if (error instanceof IyzicoResponseError) { + // Check for rate limit error codes or messages + const errorCode = error.errorResponse?.errorCode?.toLowerCase() || ''; + const errorMessage = error.errorResponse?.errorMessage?.toLowerCase() || ''; + return ( + errorCode.includes('rate') || + errorCode.includes('limit') || + errorCode === '429' || + errorMessage.includes('rate') || + errorMessage.includes('limit') || + errorMessage.includes('too many requests') + ); + } + return false; +} + +/** + * Makes an HTTP request to İyzico API with optional retry on rate limit + * @param options - Request options + * @returns Parsed JSON response + * @throws IyzicoError on failure + */ +export async function makeRequest(options: HttpRequestOptions): Promise { + const { + method, + path, + body, + apiKey, + secretKey, + baseUrl, + retryOnRateLimit = false, + maxRetries = 3, + retryDelay = 1000, + } = options; + + let lastError: unknown; + let attempts = 0; + + while (attempts <= maxRetries) { + try { + // For GET requests, extract base path without query string for signature calculation + // The query string should be part of the URL but not the signature + let signaturePath = path; + if (method === 'GET') { + // Extract path without query string + const queryIndex = path.indexOf('?'); + if (queryIndex !== -1) { + signaturePath = path.substring(0, queryIndex); + } + } + + // Generate authorization header using base path (without query string for GET) + const { authorization, randomKey } = generateAuthorization( + apiKey, + secretKey, + signaturePath, + body + ); + + // Build URL (full path with query string for GET requests) + const url = `${baseUrl}${path}`; + + // Prepare headers + const headers: Record = { + 'Authorization': authorization, + 'Content-Type': 'application/json', + 'x-iyzi-rnd': randomKey, + }; + + // Prepare request options + const requestOptions: RequestInit = { + method, + headers, + }; + + // Add body for POST and DELETE requests + if ((method === 'POST' || method === 'DELETE') && body !== undefined) { + requestOptions.body = JSON.stringify(body); + } + + // Make request + const response = await fetch(url, requestOptions); + + // Parse response + const data = (await response.json()) as unknown; + + // Check if response is an error + const baseResponse = data as BaseResponse | null; + if ( + (baseResponse?.status === 'failure') || + !response.ok + ) { + const errorResponse = data as ErrorResponse; + const error = new IyzicoResponseError( + errorResponse.errorMessage || 'API request failed', + errorResponse + ); + + // Retry on rate limit if enabled + if (retryOnRateLimit && isRateLimitError(error) && attempts < maxRetries) { + attempts++; + const delay = retryDelay * attempts; // Exponential backoff + await sleep(delay); + lastError = error; + continue; + } + + throw error; + } + + return data as T; + } catch (error) { + lastError = error; + + // Retry on rate limit if enabled + if (retryOnRateLimit && isRateLimitError(error) && attempts < maxRetries) { + attempts++; + const delay = retryDelay * attempts; // Exponential backoff + await sleep(delay); + continue; + } + + // Handle network errors + if (error instanceof TypeError && error.message.includes('fetch')) { + throw new IyzicoRequestError('Network error: Failed to connect to İyzico API', error); + } + + // Handle response errors + if (error instanceof IyzicoResponseError) { + throw error; + } + + // Handle request errors + if (error instanceof IyzicoRequestError) { + throw error; + } + + // Handle other errors + if (error instanceof IyzicoError) { + throw error; + } + + // Wrap unknown errors + throw new IyzicoError( + error instanceof Error ? error.message : 'Unknown error occurred', + undefined, + error + ); + } + } + + // If we exhausted retries, throw the last error + throw lastError; +} + diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5e50fc5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,22 @@ +/** + * İyzico Payment Library + * Main exports + */ + +// Client +export { IyzicoClient } from './client'; +export type { IyzicoClientConfig } from './client'; + +// Errors +export { + IyzicoError, + IyzicoRequestError, + IyzicoResponseError, +} from './errors'; + +// Types +export * from './types'; + +// Utils (for backward compatibility if needed) +export { generateRandomNumber, generateRandomKey } from './utils'; + diff --git a/src/services/bin-check.ts b/src/services/bin-check.ts new file mode 100644 index 0000000..54f6b4d --- /dev/null +++ b/src/services/bin-check.ts @@ -0,0 +1,31 @@ +/** + * BIN Check Service + */ + +import type { BinCheckRequest, BinCheckResponse } from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class BinCheckService { + constructor(private client: IyzicoClient) {} + + /** + * Checks BIN (Bank Identification Number) information + * @param request - BIN check request + * @returns BIN check response + */ + async check(request: BinCheckRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/bin/check', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} + diff --git a/src/services/cancel-refund.ts b/src/services/cancel-refund.ts new file mode 100644 index 0000000..f9f3668 --- /dev/null +++ b/src/services/cancel-refund.ts @@ -0,0 +1,76 @@ +/** + * Cancel & Refund Service + */ + +import type { + CancelRequest, + CancelResponse, + RefundRequest, + RefundResponse, + RefundV2Request, + RefundV2Response, +} from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class CancelRefundService { + constructor(private client: IyzicoClient) {} + + /** + * Cancels a payment + * @param request - Cancel request + * @returns Cancel response + */ + async cancel(request: CancelRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/cancel', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Refunds a payment (v1 - using paymentTransactionId) + * @param request - Refund request + * @returns Refund response + */ + async refund(request: RefundRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/refund', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Refunds a payment (v2 - using paymentId) + * @param request - Refund v2 request + * @returns Refund v2 response + */ + async refundV2(request: RefundV2Request): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/payment/refund', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} + diff --git a/src/services/card-storage.ts b/src/services/card-storage.ts new file mode 100644 index 0000000..fa960d4 --- /dev/null +++ b/src/services/card-storage.ts @@ -0,0 +1,79 @@ +/** + * Card Storage Service + */ + +import type { + CardCreateRequest, + CardCreateWithUserKeyRequest, + CardCreateResponse, + CardListRequest, + CardListResponse, + CardDeleteRequest, + CardDeleteResponse, +} from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class CardStorageService { + constructor(private client: IyzicoClient) {} + + /** + * Creates a card (for new user or existing user) + * @param request - Card create request + * @returns Card create response + */ + async createCard( + request: CardCreateRequest | CardCreateWithUserKeyRequest + ): Promise { + return makeRequest({ + method: 'POST', + path: '/cardstorage/card', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Lists cards for a user + * @param request - Card list request + * @returns Card list response + */ + async listCards(request: CardListRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/cardstorage/cards', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Deletes a card + * @param request - Card delete request + * @returns Card delete response + */ + async deleteCard(request: CardDeleteRequest): Promise { + return makeRequest({ + method: 'DELETE', + path: '/cardstorage/card', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} + diff --git a/src/services/installment.ts b/src/services/installment.ts new file mode 100644 index 0000000..1acb559 --- /dev/null +++ b/src/services/installment.ts @@ -0,0 +1,31 @@ +/** + * Installment Service + */ + +import type { InstallmentRequest, InstallmentResponse } from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class InstallmentService { + constructor(private client: IyzicoClient) {} + + /** + * Queries installment options for a BIN number and price + * @param request - Installment query request + * @returns Installment options response + */ + async query(request: InstallmentRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/iyzipos/installment', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} + diff --git a/src/services/payment.ts b/src/services/payment.ts new file mode 100644 index 0000000..a0c858f --- /dev/null +++ b/src/services/payment.ts @@ -0,0 +1,117 @@ +/** + * Payment Service + */ + +import type { + ThreeDSInitializeRequest, + ThreeDSInitializeResponse, + ThreeDSAuthRequest, + ThreeDSV2AuthRequest, + Non3DPaymentRequest, + PaymentResponse, + ThreeDSV2PaymentResponse, + PaymentDetailRequest, + PaymentDetailResponse, +} from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class PaymentService { + constructor(private client: IyzicoClient) {} + + /** + * Initializes 3DS payment + * @param request - 3DS initialize request + * @returns 3DS initialize response with HTML content + */ + async initialize3DS(request: ThreeDSInitializeRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/3dsecure/initialize', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Completes 3DS v1 payment + * @param request - 3DS v1 auth request + * @returns Payment response + */ + async complete3DS(request: ThreeDSAuthRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/3dsecure/auth', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Completes 3DS v2 payment + * @param request - 3DS v2 auth request + * @returns 3DS v2 payment response + */ + async complete3DSV2(request: ThreeDSV2AuthRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/v2/3dsecure/auth', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Creates non-3DS payment + * @param request - Non-3DS payment request + * @returns Payment response + */ + async createNon3DS(request: Non3DPaymentRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/auth', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Gets payment detail + * @param request - Payment detail request + * @returns Payment detail response + */ + async getDetail(request: PaymentDetailRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/payment/detail', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} + diff --git a/src/services/reporting.ts b/src/services/reporting.ts new file mode 100644 index 0000000..9a89598 --- /dev/null +++ b/src/services/reporting.ts @@ -0,0 +1,81 @@ +/** + * Reporting Service + */ + +import type { + PaymentTransactionsRequest, + PaymentTransactionsResponse, + PaymentDetailsRequest, + PaymentDetailsResponse, +} from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class ReportingService { + constructor(private client: IyzicoClient) {} + + /** + * Gets payment transactions for a specific date + * @param request - Payment transactions request + * @returns Payment transactions response + */ + async getPaymentTransactions( + request: PaymentTransactionsRequest + ): Promise { + // Build query string from request + const queryParams = new URLSearchParams(); + queryParams.append('page', request.page.toString()); + queryParams.append('transactionDate', request.transactionDate); + if (request.locale) { + queryParams.append('locale', request.locale); + } + if (request.conversationId) { + queryParams.append('conversationId', request.conversationId); + } + + return makeRequest({ + method: 'GET', + path: `/v2/reporting/payment/transactions?${queryParams.toString()}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Gets payment details + * @param request - Payment details request + * @returns Payment details response + */ + async getPaymentDetails(request: PaymentDetailsRequest): Promise { + // Build query string from request + const queryParams = new URLSearchParams(); + if (request.paymentId) { + queryParams.append('paymentId', request.paymentId); + } + if (request.paymentConversationId) { + queryParams.append('paymentConversationId', request.paymentConversationId); + } + if (request.locale) { + queryParams.append('locale', request.locale); + } + if (request.conversationId) { + queryParams.append('conversationId', request.conversationId); + } + + return makeRequest({ + method: 'GET', + path: `/v2/reporting/payment/details?${queryParams.toString()}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} + diff --git a/src/services/subscription.ts b/src/services/subscription.ts new file mode 100644 index 0000000..3cd867c --- /dev/null +++ b/src/services/subscription.ts @@ -0,0 +1,537 @@ +/** + * Subscription Service + */ + +import type { + // Product types + ProductCreateRequest, + ProductCreateResponse, + ProductListRequest, + ProductListResponse, + ProductGetResponse, + ProductUpdateRequest, + ProductUpdateResponse, + ProductDeleteResponse, + // Pricing Plan types + PricingPlanCreateRequest, + PricingPlanCreateResponse, + PricingPlanListRequest, + PricingPlanListResponse, + PricingPlanGetResponse, + PricingPlanUpdateRequest, + PricingPlanUpdateResponse, + PricingPlanDeleteResponse, + // Customer types + CustomerListRequest, + CustomerListResponse, + CustomerGetResponse, + CustomerUpdateRequest, + CustomerUpdateResponse, + // Subscription types + SubscriptionInitializeRequest, + SubscriptionInitializeResponse, + SubscriptionInitializeWithCustomerRequest, + SubscriptionCheckoutFormInitializeRequest, + SubscriptionCheckoutFormInitializeResponse, + SubscriptionListRequest, + SubscriptionListResponse, + SubscriptionGetResponse, + SubscriptionCancelResponse, + SubscriptionActivateResponse, + SubscriptionUpgradeRequest, + SubscriptionUpgradeResponse, + SubscriptionRetryRequest, + SubscriptionRetryResponse, + CardUpdateCheckoutFormInitializeRequest, + CardUpdateCheckoutFormInitializeResponse, +} from '../types'; +import { makeRequest } from '../http'; +import type { IyzicoClient } from '../client'; + +export class SubscriptionService { + constructor(private client: IyzicoClient) {} + + // ============================================================================ + // Product Operations + // ============================================================================ + + /** + * Creates a new subscription product + * @param request - Product create request + * @returns Product create response + */ + async createProduct(request: ProductCreateRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/subscription/products', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Lists subscription products with pagination + * @param request - Optional pagination parameters + * @returns Paginated product list response + */ + async listProducts(request?: ProductListRequest): Promise { + const queryParams = new URLSearchParams(); + if (request?.page !== undefined) queryParams.set('page', String(request.page)); + if (request?.count !== undefined) queryParams.set('count', String(request.count)); + + const queryString = queryParams.toString(); + const path = `/v2/subscription/products${queryString ? `?${queryString}` : ''}`; + + return makeRequest({ + method: 'GET', + path, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Gets a subscription product by reference code + * @param productReferenceCode - Product reference code + * @returns Product get response + */ + async getProduct(productReferenceCode: string): Promise { + return makeRequest({ + method: 'GET', + path: `/v2/subscription/products/${productReferenceCode}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Updates a subscription product + * @param productReferenceCode - Product reference code + * @param request - Product update request + * @returns Product update response + */ + async updateProduct(productReferenceCode: string, request: ProductUpdateRequest): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/products/${productReferenceCode}`, + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Deletes a subscription product + * @param productReferenceCode - Product reference code + * @returns Product delete response + */ + async deleteProduct(productReferenceCode: string): Promise { + return makeRequest({ + method: 'DELETE', + path: `/v2/subscription/products/${productReferenceCode}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + // ============================================================================ + // Pricing Plan Operations + // ============================================================================ + + /** + * Creates a new pricing plan for a product + * @param productReferenceCode - Product reference code + * @param request - Pricing plan create request + * @returns Pricing plan create response + */ + async createPricingPlan(productReferenceCode: string, request: PricingPlanCreateRequest): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/products/${productReferenceCode}/pricing-plans`, + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Lists pricing plans for a product with pagination + * @param productReferenceCode - Product reference code + * @param request - Optional pagination parameters + * @returns Paginated pricing plan list response + */ + async listPricingPlans(productReferenceCode: string, request?: PricingPlanListRequest): Promise { + const queryParams = new URLSearchParams(); + if (request?.page !== undefined) queryParams.set('page', String(request.page)); + if (request?.count !== undefined) queryParams.set('count', String(request.count)); + + const queryString = queryParams.toString(); + const path = `/v2/subscription/products/${productReferenceCode}/pricing-plans${queryString ? `?${queryString}` : ''}`; + + return makeRequest({ + method: 'GET', + path, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Gets a pricing plan by reference code + * @param pricingPlanReferenceCode - Pricing plan reference code + * @returns Pricing plan get response + */ + async getPricingPlan(pricingPlanReferenceCode: string): Promise { + return makeRequest({ + method: 'GET', + path: `/v2/subscription/pricing-plans/${pricingPlanReferenceCode}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Updates a pricing plan + * @param pricingPlanReferenceCode - Pricing plan reference code + * @param request - Pricing plan update request + * @returns Pricing plan update response + */ + async updatePricingPlan(pricingPlanReferenceCode: string, request: PricingPlanUpdateRequest): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/pricing-plans/${pricingPlanReferenceCode}`, + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Deletes a pricing plan + * @param pricingPlanReferenceCode - Pricing plan reference code + * @returns Pricing plan delete response + */ + async deletePricingPlan(pricingPlanReferenceCode: string): Promise { + return makeRequest({ + method: 'DELETE', + path: `/v2/subscription/pricing-plans/${pricingPlanReferenceCode}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + // ============================================================================ + // Customer Operations + // ============================================================================ + + /** + * Lists subscription customers with pagination + * @param request - Optional pagination parameters + * @returns Paginated customer list response + */ + async listCustomers(request?: CustomerListRequest): Promise { + const queryParams = new URLSearchParams(); + if (request?.page !== undefined) queryParams.set('page', String(request.page)); + if (request?.count !== undefined) queryParams.set('count', String(request.count)); + + const queryString = queryParams.toString(); + const path = `/v2/subscription/customers${queryString ? `?${queryString}` : ''}`; + + return makeRequest({ + method: 'GET', + path, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Gets a subscription customer by reference code + * @param customerReferenceCode - Customer reference code + * @returns Customer get response + */ + async getCustomer(customerReferenceCode: string): Promise { + return makeRequest({ + method: 'GET', + path: `/v2/subscription/customers/${customerReferenceCode}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Updates a subscription customer + * @param customerReferenceCode - Customer reference code + * @param request - Customer update request + * @returns Customer update response + */ + async updateCustomer(customerReferenceCode: string, request: CustomerUpdateRequest): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/customers/${customerReferenceCode}`, + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + // ============================================================================ + // Subscription Operations + // ============================================================================ + + /** + * Initializes a new subscription (NON3D) + * @param request - Subscription initialize request + * @returns Subscription initialize response + */ + async initialize(request: SubscriptionInitializeRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/subscription/initialize', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Initializes a subscription for an existing customer + * @param request - Subscription initialize with customer request + * @returns Subscription initialize response + */ + async initializeWithCustomer(request: SubscriptionInitializeWithCustomerRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/subscription/initialize/with-customer', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Initializes a subscription using checkout form + * @param request - Subscription checkout form initialize request + * @returns Subscription checkout form initialize response + */ + async initializeCheckoutForm(request: SubscriptionCheckoutFormInitializeRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/subscription/checkoutform/initialize', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Lists subscriptions with filtering and pagination + * @param request - Optional filter and pagination parameters + * @returns Paginated subscription list response + */ + async list(request?: SubscriptionListRequest): Promise { + const queryParams = new URLSearchParams(); + if (request?.subscriptionReferenceCode) queryParams.set('subscriptionReferenceCode', request.subscriptionReferenceCode); + if (request?.customerReferenceCode) queryParams.set('customerReferenceCode', request.customerReferenceCode); + if (request?.pricingPlanReferenceCode) queryParams.set('pricingPlanReferenceCode', request.pricingPlanReferenceCode); + if (request?.parentReferenceCode) queryParams.set('parent', request.parentReferenceCode); + if (request?.subscriptionStatus) queryParams.set('subscriptionStatus', request.subscriptionStatus); + if (request?.startDate !== undefined) queryParams.set('startDate', String(request.startDate)); + if (request?.endDate !== undefined) queryParams.set('endDate', String(request.endDate)); + if (request?.page !== undefined) queryParams.set('page', String(request.page)); + if (request?.count !== undefined) queryParams.set('count', String(request.count)); + + const queryString = queryParams.toString(); + const path = `/v2/subscription/subscriptions${queryString ? `?${queryString}` : ''}`; + + return makeRequest({ + method: 'GET', + path, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Gets a subscription by reference code + * @param subscriptionReferenceCode - Subscription reference code + * @returns Subscription get response + */ + async get(subscriptionReferenceCode: string): Promise { + return makeRequest({ + method: 'GET', + path: `/v2/subscription/subscriptions/${subscriptionReferenceCode}`, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Cancels an active subscription + * @param subscriptionReferenceCode - Subscription reference code + * @returns Subscription cancel response + */ + async cancel(subscriptionReferenceCode: string): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/subscriptions/${subscriptionReferenceCode}/cancel`, + body: {}, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Activates a pending subscription + * @param subscriptionReferenceCode - Subscription reference code + * @returns Subscription activate response + */ + async activate(subscriptionReferenceCode: string): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/subscriptions/${subscriptionReferenceCode}/activate`, + body: {}, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Upgrades a subscription to a new pricing plan + * @param subscriptionReferenceCode - Subscription reference code + * @param request - Subscription upgrade request + * @returns Subscription upgrade response + */ + async upgrade(subscriptionReferenceCode: string, request: SubscriptionUpgradeRequest): Promise { + return makeRequest({ + method: 'POST', + path: `/v2/subscription/subscriptions/${subscriptionReferenceCode}/upgrade`, + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Retries a failed subscription payment + * @param request - Retry payment request with order reference code + * @returns Subscription retry response + */ + async retryPayment(request: SubscriptionRetryRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/subscription/operation/retry', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } + + /** + * Initializes card update via checkout form + * @param request - Card update checkout form initialize request + * @returns Card update checkout form initialize response + */ + async initializeCardUpdate(request: CardUpdateCheckoutFormInitializeRequest): Promise { + return makeRequest({ + method: 'POST', + path: '/v2/subscription/card-update/checkoutform/initialize', + body: request, + apiKey: this.client.apiKey, + secretKey: this.client.secretKey, + baseUrl: this.client.baseUrl, + retryOnRateLimit: this.client.retryOnRateLimit, + maxRetries: this.client.maxRetries, + retryDelay: this.client.retryDelay, + }); + } +} diff --git a/src/types/bin-check.ts b/src/types/bin-check.ts new file mode 100644 index 0000000..7e108c4 --- /dev/null +++ b/src/types/bin-check.ts @@ -0,0 +1,83 @@ +/** + * BIN Sorgulama modelleri + */ + +import type { BaseResponse, Locale, Status } from './common'; + +/** + * BIN Sorgulama Request + */ +export interface BinCheckRequest { + locale?: Locale; + binNumber: string; + conversationId?: string; + price?: number; +} + +/** + * Taksit detayı modeli + */ +export interface InstallmentDetail { + installmentPrice: number; + totalPrice: number; + installmentNumber: number; +} + +/** + * Taksit Sorgulama Request + */ +export interface InstallmentRequest { + locale?: Locale; + conversationId?: string; + binNumber: string; + price: number; +} + +/** + * Taksit Detay Bilgileri + */ +export interface InstallmentDetailItem { + binNumber: string; + price: number; + cardType: string; + cardAssociation: string; + cardFamilyName: string; + force3ds: 0 | 1; + bankCode: number; + bankName: string; + forceCvc: 0 | 1; + commercial: 0 | 1; + dccEnabled: 0 | 1; + agricultureEnabled?: 0 | 1; + installmentPrices: InstallmentDetail[]; +} + +/** + * Taksit Sorgulama Response + */ +export interface InstallmentResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + installmentDetails?: InstallmentDetailItem[]; +} + +/** + * BIN Sorgulama Response + */ +export interface BinCheckResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + binNumber?: string; + cardType?: string; + cardAssociation?: string; + cardFamily?: string; + bankName?: string; + bankCode?: number; + commercial?: 0 | 1; + installmentDetails?: InstallmentDetailItem[]; +} + diff --git a/src/types/cancel-refund.ts b/src/types/cancel-refund.ts new file mode 100644 index 0000000..a9a86b7 --- /dev/null +++ b/src/types/cancel-refund.ts @@ -0,0 +1,97 @@ +/** + * İptal ve İade işlemleri için modeller + */ + +import type { BaseResponse, Locale, Currency, Status, CancelReason } from './common'; + +/** + * İptal Request + */ +export interface CancelRequest { + paymentId: string; + conversationId?: string; + ip?: string; + locale?: Locale; + reason?: CancelReason; + description?: string; +} + +/** + * İptal Response + */ +export interface CancelResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + paymentId?: string; + price?: number; + currency?: Currency; + authCode?: string; + hostReference?: string; + cancelHostReference?: string; +} + +/** + * İade Request (v1 - paymentTransactionId ile) + */ +export interface RefundRequest { + paymentTransactionId: string; + price: number; + conversationId?: string; + ip?: string; + locale?: Locale; + currency?: Currency; + reason?: CancelReason; + description?: string; +} + +/** + * İade Response (v1) + */ +export interface RefundResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + paymentId?: string; + paymentTransactionId?: string; + price?: number; + currency?: Currency; + authCode?: string; + hostReference?: string; + refundHostReference?: string; + retryable?: boolean; + signature?: string; +} + +/** + * İade Request (v2 - paymentId ile) + */ +export interface RefundV2Request { + paymentId: string; + price: number; + conversationId?: string; + ip?: string; + locale?: Locale; + currency?: Currency; +} + +/** + * İade Response (v2) + */ +export interface RefundV2Response extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + paymentId?: string; + price?: number; + currency?: Currency; + authCode?: string; + hostReference?: string; + refundHostReference?: string; + retryable?: boolean; + signature?: string; +} + diff --git a/src/types/card-storage.ts b/src/types/card-storage.ts new file mode 100644 index 0000000..7688448 --- /dev/null +++ b/src/types/card-storage.ts @@ -0,0 +1,112 @@ +/** + * Kart saklama işlemleri için modeller + */ + +import type { BaseResponse, Locale, Status, CardType, CardAssociation } from './common'; + +/** + * Kart modeli + */ +export interface Card { + cardAlias?: string; + cardNumber: string; + expireYear: string; + expireMonth: string; + cardHolderName: string; +} + +/** + * Kart oluşturma Request (yeni kullanıcı + ilk kart) + */ +export interface CardCreateRequest { + locale?: Locale; + conversationId?: string; + externalId?: string; + email: string; + card: Card; +} + +/** + * Kart oluşturma Request (mevcut kullanıcıya kart ekleme) + */ +export interface CardCreateWithUserKeyRequest { + locale?: Locale; + conversationId?: string; + cardUserKey: string; + card: Card; +} + +/** + * Kart oluşturma Response + */ +export interface CardCreateResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + externalId?: string; + email?: string; + cardUserKey?: string; + cardToken?: string; + binNumber?: string; + lastFourDigits?: string; + cardType?: CardType; + cardAssociation?: CardAssociation; + cardFamily?: string; + cardAlias?: string; + cardBankCode?: number; + cardBankName?: string; +} + +/** + * Kart listeleme Request + */ +export interface CardListRequest { + locale?: Locale; + conversationId?: string; + cardUserKey: string; +} + +/** + * Kart detayı modeli + */ +export interface CardDetail { + cardFamily?: string; + cardType?: CardType; + cardAssociation?: CardAssociation; + lastFourDigits?: string; + binNumber?: string; +} + +/** + * Kart listeleme Response + */ +export interface CardListResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + cardUserKey?: string; + cardDetails?: CardDetail[]; +} + +/** + * Kart silme Request + */ +export interface CardDeleteRequest { + locale?: Locale; + conversationId?: string; + cardUserKey: string; + cardToken: string; +} + +/** + * Kart silme Response + */ +export interface CardDeleteResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; +} + diff --git a/src/types/common.ts b/src/types/common.ts new file mode 100644 index 0000000..2562717 --- /dev/null +++ b/src/types/common.ts @@ -0,0 +1,165 @@ +/** + * Ortak kullanılan modeller + */ + +export type Locale = 'tr' | 'en'; +export type Currency = 'TRY' | 'USD' | 'EUR' | 'GBP' | 'NOK' | 'CHF'; +export type PaymentChannel = + | 'WEB' + | 'MOBILE' + | 'MOBILE_WEB' + | 'MOBILE_IOS' + | 'MOBILE_ANDROID' + | 'MOBILE_WINDOWS' + | 'MOBILE_TABLET' + | 'MOBILE_PHONE'; +export type PaymentGroup = 'PRODUCT' | 'LISTING' | 'SUBSCRIPTION' | 'OTHER'; +export type ItemType = 'PHYSICAL' | 'VIRTUAL'; +export type CardType = 'CREDIT_CARD' | 'DEBIT_CARD' | 'PREPAID_CARD'; +export type CardAssociation = 'VISA' | 'MASTER_CARD' | 'AMERICAN_EXPRESS' | 'TROY'; +export type Status = 'success' | 'failure'; +export type CancelReason = 'OTHER' | 'FRAUD' | 'BUYER_REQUEST' | 'DOUBLE_PAYMENT'; + +/** + * Tüm response'lar için temel model + */ +export interface BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; +} + +/** + * Hata response modeli + */ +export interface ErrorResponse extends BaseResponse { + status: 'failure'; + errorCode?: string; + errorMessage?: string; + errorGroup?: string; + errorCategory?: string; + paymentId?: string; + paymentTransactionId?: string; + price?: number; + retryable?: boolean; +} + +/** + * Adres modeli + */ +export interface Address { + contactName: string; + name?: string; + surname?: string; + address: string; + city: string; + country: string; + zipCode?: string; + phone?: string; + mobilePhone?: string; + province?: string; + line1?: string; +} + +/** + * Fatura adresi modeli + */ +export interface BillingAddress extends Address {} + +/** + * Kargo adresi modeli + */ +export interface ShippingAddress extends Address {} + +/** + * Alıcı bilgileri modeli + */ +export interface Buyer { + id: string; + name: string; + surname: string; + identityNumber: string; + email: string; + gsmNumber: string; + registrationAddress: string; + city: string; + country: string; + registrationDate?: string; + lastLoginDate?: string; + zipCode?: string; + ip?: string; +} + +/** + * Ödeme kartı modeli + */ +export interface PaymentCard { + cardHolderName: string; + cardNumber: string; + expireYear: string; + expireMonth: string; + cvc: string; + registerCard?: 0 | 1; + cardUserKey?: string; + cardToken?: string; +} + +/** + * Sepet öğesi modeli + */ +export interface BasketItem { + id: string; + name: string; + price: number; + itemType: ItemType; + category1: string; + category2?: string; + quantity?: number; + itemDescription1?: string; + itemDescription2?: string; + subMerchantMemberId?: string; + subMerchantPrice?: number; +} + +/** + * İşlem kırılımı modeli + */ +export interface ItemTransaction { + paymentTransactionId: string; + itemId?: string; + price: number; + paidPrice: number; + transactionStatus?: number; + installment?: number; + merchantCommissionRate?: number; + merchantCommissionRateAmount?: number; + iyziCommissionRateAmount?: number; + iyziCommissionFee?: number; + blockageRate?: number; + blockageRateAmountMerchant?: number; + blockageRateAmountSubMerchant?: number; + blockageResolvedDate?: string; + subMerchantPrice?: number; + subMerchantPayoutRate?: number; + subMerchantPayoutAmount?: number; + merchantPayoutAmount?: number; + convertedPayout?: ConvertedPayout; +} + +/** + * Dönüştürülmüş ödeme modeli + */ +export interface ConvertedPayout { + paidPrice: number; + iyziCommissionRateAmount: number; + iyziCommissionFee: number; + blockageRateAmountMerchant: number; + blockageRateAmountSubMerchant: number; + subMerchantPayoutAmount: number; + merchantPayoutAmount: number; + iyziConversionRate?: number; + iyziConversionRateAmount?: number; + currency: Currency; +} + diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..325f97e --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,25 @@ +/** + * Tüm type export'ları + */ + +// Common types +export * from './common'; + +// Payment types +export * from './payment'; + +// Bin check types +export * from './bin-check'; + +// Cancel & Refund types +export * from './cancel-refund'; + +// Card storage types +export * from './card-storage'; + +// Reporting types +export * from './reporting'; + +// Subscription types +export * from './subscription'; + diff --git a/src/types/payment.ts b/src/types/payment.ts new file mode 100644 index 0000000..3edc4a0 --- /dev/null +++ b/src/types/payment.ts @@ -0,0 +1,148 @@ +/** + * Ödeme işlemleri için modeller + */ + +import type { + BaseResponse, + Locale, + Currency, + PaymentChannel, + PaymentGroup, + PaymentCard, + Buyer, + ShippingAddress, + BillingAddress, + BasketItem, + ItemTransaction, + Status, +} from './common'; + +/** + * 3DS Ödeme Başlatma Request + */ +export interface ThreeDSInitializeRequest { + locale?: Locale; + conversationId?: string; + price: number; + paidPrice: number; + currency?: Currency; + installment?: 1 | 2 | 3 | 4 | 6 | 9 | 12; + paymentChannel?: PaymentChannel; + basketId?: string; + paymentGroup?: PaymentGroup; + callbackUrl: string; + paymentCard: PaymentCard; + buyer: Buyer; + shippingAddress?: ShippingAddress; + billingAddress: BillingAddress; + basketItems: BasketItem[]; + paymentSource?: string; +} + +/** + * 3DS Ödeme Başlatma Response + */ +export interface ThreeDSInitializeResponse extends BaseResponse { + status: Status; + paymentId?: string; + htmlContent?: string; + threeDSHtmlContent?: string; + conversationId?: string; +} + +/** + * 3DS v1 Ödeme Tamamlama Request + */ +export interface ThreeDSAuthRequest { + locale?: Locale; + paymentId: string; + conversationId?: string; + conversationData?: string; +} + +/** + * 3DS v2 Ödeme Tamamlama Request + */ +export interface ThreeDSV2AuthRequest { + locale?: Locale; + paymentId: string; + conversationId?: string; + paidPrice: number; + basketId: string; + currency: Currency; +} + +/** + * Non-3DS Ödeme Request + */ +export interface Non3DPaymentRequest { + locale?: Locale; + conversationId?: string; + price: number; + paidPrice: number; + currency?: Currency; + installment?: 1 | 2 | 3 | 6 | 9 | 12; + paymentChannel?: PaymentChannel; + basketId?: string; + paymentGroup?: PaymentGroup; + paymentCard: PaymentCard; + buyer: Buyer; + shippingAddress?: ShippingAddress; + billingAddress: BillingAddress; + basketItems: BasketItem[]; + paymentSource?: string; +} + +/** + * Ödeme Response (3DS ve Non3DS için ortak) + */ +export interface PaymentResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + price?: number; + paidPrice?: number; + installment?: number; + paymentId?: string; + fraudStatus?: -1 | 0 | 1; + merchantCommissionRate?: number; + merchantCommissionRateAmount?: number; + iyziCommissionRateAmount?: number; + iyziCommissionFee?: number; + cardType?: string; + cardAssociation?: string; + cardFamily?: string; + binNumber?: string; + lastFourDigits?: string; + basketId?: string; + currency?: Currency; + itemTransactions?: ItemTransaction[]; + authCode?: string; + phase?: string; + hostReference?: string; + signature?: string; + paymentStatus?: string; + mdStatus?: string; + hash?: string; +} + +/** + * 3DS v2 Ödeme Response + */ +export interface ThreeDSV2PaymentResponse extends PaymentResponse {} + +/** + * Ödeme Sorgulama Request + */ +export interface PaymentDetailRequest { + locale?: Locale; + paymentId?: string; + paymentConversationId?: string; +} + +/** + * Ödeme Sorgulama Response + */ +export interface PaymentDetailResponse extends PaymentResponse {} + diff --git a/src/types/reporting.ts b/src/types/reporting.ts new file mode 100644 index 0000000..6297736 --- /dev/null +++ b/src/types/reporting.ts @@ -0,0 +1,98 @@ +/** + * Raporlama işlemleri için modeller + */ + +import type { BaseResponse, Locale, Currency, Status } from './common'; + +/** + * İşlem raporlama Request + */ +export interface PaymentTransactionsRequest { + page: number; + transactionDate: string; // YYYY-MM-DD formatında + locale?: Locale; + conversationId?: string; +} + +/** + * İşlem öğesi modeli + */ +export interface TransactionItem { + transactionType: 'PAYMENT' | 'CANCEL' | 'REFUND'; + transactionDate: string; + transactionId: string; + transactionStatus: number; + afterSettlement: 0 | 1; + paymentTxId?: string; + paymentId?: string; + conversationId?: string; + paymentPhase?: string; + price: number; + paidPrice: number; + transactionCurrency: Currency; + installment?: number; + threeDS: 0 | 1; + settlementCurrency?: Currency; + connectorType?: string; + posOrderId?: string; + authCode?: string; + hostReference?: string; + basketId?: string; + iyzicoCommission?: number; + iyzicoFee?: number; +} + +/** + * İşlem raporlama Response + */ +export interface PaymentTransactionsResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + transactions?: TransactionItem[]; + currentPage?: number; + totalPageCount?: number; +} + +/** + * Ödeme detay raporlama Request + */ +export interface PaymentDetailsRequest { + paymentId?: string; + paymentConversationId?: string; + locale?: Locale; + conversationId?: string; +} + +/** + * Ödeme detay modeli + */ +export interface PaymentDetail { + paymentId: string; + paymentStatus: 1 | 2 | 3; // 1: Success, 2: Failure / INIT_THREEDS, 3: CALLBACK_THREEDS + paymentRefundStatus: 'NOT_REFUNDED' | 'PARTIALLY_REFUNDED' | 'TOTALLY_REFUNDED'; + price: number; + paidPrice: number; + installment?: number; + merchantCommissionRate?: number; + merchantCommissionRateAmount?: number; + iyziCommissionRateAmount?: number; + iyziCommissionFee?: number; + paymentConversationId?: string; + fraudStatus?: -1 | 0 | 1; + cardType?: string; + cardAssociation?: string; +} + +/** + * Ödeme detay raporlama Response + */ +export interface PaymentDetailsResponse extends BaseResponse { + status: Status; + locale?: Locale; + systemTime?: number; + conversationId?: string; + payments?: PaymentDetail[]; +} + diff --git a/src/types/subscription.ts b/src/types/subscription.ts new file mode 100644 index 0000000..658ed23 --- /dev/null +++ b/src/types/subscription.ts @@ -0,0 +1,511 @@ +/** + * Subscription API type definitions + */ + +import type { BaseResponse, Locale, Currency } from './common'; + +// ============================================================================ +// Enums and Type Aliases +// ============================================================================ + +export type PaymentInterval = 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY'; +export type PlanPaymentType = 'RECURRING'; +export type SubscriptionStatus = 'ACTIVE' | 'PENDING' | 'UNPAID' | 'UPGRADED' | 'CANCELED' | 'EXPIRED'; +export type SubscriptionInitialStatus = 'ACTIVE' | 'PENDING'; +export type UpgradePeriod = 'NOW' | 'NEXT_PERIOD'; +export type SubscriptionProductStatus = 'ACTIVE' | 'INACTIVE'; +export type SubscriptionCustomerStatus = 'ACTIVE' | 'INACTIVE'; + +// ============================================================================ +// Common Subscription Types +// ============================================================================ + +/** + * Paginated result wrapper + */ +export interface PaginatedResult { + totalCount: number; + currentPage: number; + pageCount: number; + items: T[]; +} + +/** + * Subscription address model (different from payment Address) + */ +export interface SubscriptionAddress { + address: string; + zipCode?: string; + contactName: string; + city: string; + district?: string; + country: string; +} + +/** + * Subscription customer model + */ +export interface SubscriptionCustomer { + name: string; + surname: string; + email: string; + gsmNumber: string; + identityNumber: string; + billingAddress: SubscriptionAddress; + shippingAddress?: SubscriptionAddress; +} + +/** + * Subscription customer resource (response model) + */ +export interface SubscriptionCustomerResource { + referenceCode: string; + createdDate: number; + status: SubscriptionCustomerStatus; + name: string; + surname: string; + identityNumber: string; + email: string; + gsmNumber: string; + contactEmail?: string; + contactGsmNumber?: string; + billingAddress: SubscriptionAddress; + shippingAddress?: SubscriptionAddress; +} + +/** + * Subscription payment card model + */ +export interface SubscriptionPaymentCard { + cardHolderName: string; + cardNumber: string; + expireMonth: string; + expireYear: string; + cvc: string; +} + +/** + * Pricing plan model + */ +export interface PricingPlan { + referenceCode: string; + createdDate: string | number; + name: string; + price: number; + currencyCode: Currency; + paymentInterval: PaymentInterval; + paymentIntervalCount: number; + trialPeriodDays?: number; + productReferenceCode: string; + planPaymentType: PlanPaymentType; + status: string; + recurrenceCount?: number; +} + +/** + * Subscription product model + */ +export interface SubscriptionProduct { + referenceCode: string; + createdDate: string; + name: string; + description?: string; + status: SubscriptionProductStatus; + pricingPlans?: PricingPlan[]; +} + +/** + * Subscription order model + */ +export interface SubscriptionOrder { + referenceCode: string; + price: number; + currencyCode: Currency; + startPeriod: number; + endPeriod: number; + orderStatus: string; +} + +/** + * Subscription resource model + */ +export interface SubscriptionResource { + referenceCode: string; + parentReferenceCode?: string; + pricingPlanName?: string; + pricingPlanReferenceCode: string; + productName?: string; + productReferenceCode?: string; + customerEmail?: string; + customerGsmNumber?: string; + customerReferenceCode: string; + subscriptionStatus: SubscriptionStatus; + trialDays?: number; + trialStartDate?: number; + trialEndDate?: number; + createdDate?: number; + startDate?: number; + endDate?: number; + orders?: SubscriptionOrder[]; +} + +// ============================================================================ +// Product Request/Response Types +// ============================================================================ + +/** + * Create product request + */ +export interface ProductCreateRequest { + locale?: Locale; + conversationId?: string; + name: string; + description?: string; +} + +/** + * Create product response + */ +export interface ProductCreateResponse extends BaseResponse { + data?: SubscriptionProduct; +} + +/** + * List products request + */ +export interface ProductListRequest { + locale?: Locale; + conversationId?: string; + page?: number; + count?: number; +} + +/** + * List products response + */ +export interface ProductListResponse extends BaseResponse { + data?: PaginatedResult; +} + +/** + * Get product response + */ +export interface ProductGetResponse extends BaseResponse { + data?: SubscriptionProduct; +} + +/** + * Update product request + */ +export interface ProductUpdateRequest { + locale?: Locale; + conversationId?: string; + name: string; + description?: string; +} + +/** + * Update product response + */ +export interface ProductUpdateResponse extends BaseResponse { + data?: SubscriptionProduct; +} + +/** + * Delete product response + */ +export interface ProductDeleteResponse extends BaseResponse {} + +// ============================================================================ +// Pricing Plan Request/Response Types +// ============================================================================ + +/** + * Create pricing plan request + */ +export interface PricingPlanCreateRequest { + locale?: Locale; + conversationId?: string; + name: string; + price: number; + currencyCode: Currency; + paymentInterval: PaymentInterval; + paymentIntervalCount?: number; + trialPeriodDays?: number; + planPaymentType: PlanPaymentType; + recurrenceCount?: number; +} + +/** + * Create pricing plan response + */ +export interface PricingPlanCreateResponse extends BaseResponse { + data?: PricingPlan; +} + +/** + * List pricing plans request + */ +export interface PricingPlanListRequest { + locale?: Locale; + conversationId?: string; + page?: number; + count?: number; +} + +/** + * List pricing plans response + */ +export interface PricingPlanListResponse extends BaseResponse { + data?: PaginatedResult; +} + +/** + * Get pricing plan response + */ +export interface PricingPlanGetResponse extends BaseResponse { + data?: PricingPlan; +} + +/** + * Update pricing plan request + */ +export interface PricingPlanUpdateRequest { + locale?: Locale; + conversationId?: string; + name: string; + trialPeriodDays?: number; +} + +/** + * Update pricing plan response + */ +export interface PricingPlanUpdateResponse extends BaseResponse { + data?: PricingPlan; +} + +/** + * Delete pricing plan response + */ +export interface PricingPlanDeleteResponse extends BaseResponse {} + +// ============================================================================ +// Customer Request/Response Types +// ============================================================================ + +/** + * List customers request + */ +export interface CustomerListRequest { + locale?: Locale; + conversationId?: string; + page?: number; + count?: number; +} + +/** + * List customers response + */ +export interface CustomerListResponse extends BaseResponse { + data?: PaginatedResult; +} + +/** + * Get customer response + */ +export interface CustomerGetResponse extends BaseResponse { + data?: SubscriptionCustomerResource; +} + +/** + * Update customer request + */ +export interface CustomerUpdateRequest { + locale?: Locale; + conversationId?: string; + name?: string; + surname?: string; + email?: string; + gsmNumber?: string; + identityNumber?: string; + billingAddress?: SubscriptionAddress; + shippingAddress?: SubscriptionAddress; +} + +/** + * Update customer response + */ +export interface CustomerUpdateResponse extends BaseResponse { + data?: SubscriptionCustomerResource; +} + +// ============================================================================ +// Subscription Request/Response Types +// ============================================================================ + +/** + * Initialize subscription request (NON3D) + */ +export interface SubscriptionInitializeRequest { + locale?: Locale; + conversationId?: string; + pricingPlanReferenceCode: string; + subscriptionInitialStatus: SubscriptionInitialStatus; + customer: SubscriptionCustomer; + paymentCard: SubscriptionPaymentCard; +} + +/** + * Initialize subscription response + */ +export interface SubscriptionInitializeResponse extends BaseResponse { + data?: { + referenceCode: string; + parentReferenceCode?: string; + customerReferenceCode: string; + pricingPlanReferenceCode?: string; + subscriptionStatus?: SubscriptionStatus; + trialDays?: number; + trialStartDate?: number; + trialEndDate?: number; + createdDate?: number; + startDate?: number; + endDate?: number; + }; +} + +/** + * Initialize subscription with existing customer request + */ +export interface SubscriptionInitializeWithCustomerRequest { + locale?: Locale; + conversationId?: string; + pricingPlanReferenceCode: string; + subscriptionInitialStatus: SubscriptionInitialStatus; + customerReferenceCode: string; +} + +/** + * Initialize subscription checkout form request + */ +export interface SubscriptionCheckoutFormInitializeRequest { + locale?: Locale; + conversationId?: string; + callbackUrl: string; + pricingPlanReferenceCode: string; + subscriptionInitialStatus: SubscriptionInitialStatus; + customer: SubscriptionCustomer; +} + +/** + * Initialize subscription checkout form response + */ +export interface SubscriptionCheckoutFormInitializeResponse extends BaseResponse { + token?: string; + checkoutFormContent?: string; + tokenExpireTime?: number; +} + +/** + * List subscriptions request + */ +export interface SubscriptionListRequest { + locale?: Locale; + conversationId?: string; + subscriptionReferenceCode?: string; + customerReferenceCode?: string; + pricingPlanReferenceCode?: string; + parentReferenceCode?: string; + subscriptionStatus?: SubscriptionStatus; + startDate?: number; + endDate?: number; + page?: number; + count?: number; +} + +/** + * List subscriptions response + */ +export interface SubscriptionListResponse extends BaseResponse { + data?: PaginatedResult; +} + +/** + * Get subscription response + */ +export interface SubscriptionGetResponse extends BaseResponse { + data?: PaginatedResult; +} + +/** + * Cancel subscription response + */ +export interface SubscriptionCancelResponse extends BaseResponse {} + +/** + * Activate subscription response + */ +export interface SubscriptionActivateResponse extends BaseResponse {} + +/** + * Upgrade subscription request + */ +export interface SubscriptionUpgradeRequest { + locale?: Locale; + conversationId?: string; + upgradePeriod: UpgradePeriod; + newPricingPlanReferenceCode: string; + useTrial?: boolean; + resetRecurrenceCount?: boolean; +} + +/** + * Upgrade subscription response + */ +export interface SubscriptionUpgradeResponse extends BaseResponse { + data?: { + referenceCode: string; + parentReferenceCode?: string; + pricingPlanReferenceCode: string; + customerReferenceCode: string; + subscriptionStatus: SubscriptionStatus; + trialDays?: number; + createdDate?: number; + startDate?: number; + endDate?: number; + }; +} + +/** + * Retry failed payment request + */ +export interface SubscriptionRetryRequest { + locale?: Locale; + conversationId?: string; + referenceCode: string; +} + +/** + * Retry failed payment response + */ +export interface SubscriptionRetryResponse extends BaseResponse {} + +/** + * Card update checkout form initialize request + */ +export interface CardUpdateCheckoutFormInitializeRequest { + locale?: Locale; + conversationId?: string; + callbackUrl: string; + customerReferenceCode: string; + subscriptionReferenceCode?: string; +} + +/** + * Card update checkout form initialize response + */ +export interface CardUpdateCheckoutFormInitializeResponse extends BaseResponse { + token?: string; + checkoutFormContent?: string; + tokenExpireTime?: number; +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..c217bd1 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,28 @@ +/** + * Utility functions + */ + +/** + * Generates a random number between min and max (inclusive) + * @param min - Minimum value (default: 0) + * @param max - Maximum value (default: 100) + * @returns Random number between min and max + */ +export function generateRandomNumber(min: number = 0, max: number = 100): number { + if (min > max) { + throw new Error('Min value cannot be greater than max value'); + } + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Generates a random key for İyzico API requests + * Uses timestamp + random number combination + * @returns Random key string + */ +export function generateRandomKey(): string { + const timestamp = Date.now(); + const random = generateRandomNumber(100000, 999999); + return `${timestamp}${random}`; +} + diff --git a/tests/fixtures/card.ts b/tests/fixtures/card.ts new file mode 100644 index 0000000..7f50bc2 --- /dev/null +++ b/tests/fixtures/card.ts @@ -0,0 +1,49 @@ +/** + * Card storage test fixtures with randomized personal data + */ + +import { faker } from '@faker-js/faker'; +import type { Card, CardCreateRequest, CardCreateWithUserKeyRequest } from '../../src/types'; + +/** + * Generates a test card with randomized holder name + * Card number is fixed to iyzico sandbox test card + */ +export function createTestCard(): Card { + return { + cardAlias: faker.helpers.arrayElement(['My Card', 'Personal Card', 'Main Card', 'Default Card']), + cardNumber: '5528790000000008', // iyzico sandbox test card + expireYear: '2030', + expireMonth: '12', + cardHolderName: faker.person.fullName(), + }; +} + +/** + * Generates a test card create request with randomized data + */ +export function createTestCardCreateRequest(): CardCreateRequest { + return { + locale: 'tr', + conversationId: faker.string.uuid(), + email: faker.internet.email().toLowerCase(), + externalId: `external-${faker.string.alphanumeric(10)}`, + card: createTestCard(), + }; +} + +/** + * Generates a test card create request with user key + */ +export function createCardCreateWithUserKeyRequest(cardUserKey: string): CardCreateWithUserKeyRequest { + return { + locale: 'tr', + conversationId: faker.string.uuid(), + cardUserKey, + card: createTestCard(), + }; +} + +// Legacy exports for backward compatibility (static versions that generate once) +export const testCard = createTestCard(); +export const testCardCreateRequest = createTestCardCreateRequest(); diff --git a/tests/fixtures/payment.ts b/tests/fixtures/payment.ts new file mode 100644 index 0000000..de75be9 --- /dev/null +++ b/tests/fixtures/payment.ts @@ -0,0 +1,206 @@ +/** + * Payment test fixtures with randomized personal data + */ + +import { faker } from '@faker-js/faker'; +import type { + ThreeDSInitializeRequest, + Non3DPaymentRequest, + PaymentCard, + Buyer, + BillingAddress, + ShippingAddress, + BasketItem, +} from '../../src/types'; + +/** + * Generates a valid Turkish identity number (TC Kimlik No) + * Uses a simple algorithm that produces valid-looking numbers + */ +function generateTurkishIdentityNumber(): string { + const digits: number[] = []; + + // First digit cannot be 0 + digits.push(faker.number.int({ min: 1, max: 9 })); + + // Generate next 8 random digits + for (let i = 1; i < 9; i++) { + digits.push(faker.number.int({ min: 0, max: 9 })); + } + + // Calculate 10th digit + const oddSum = digits[0] + digits[2] + digits[4] + digits[6] + digits[8]; + const evenSum = digits[1] + digits[3] + digits[5] + digits[7]; + const digit10 = (oddSum * 7 - evenSum) % 10; + digits.push(digit10 < 0 ? digit10 + 10 : digit10); + + // Calculate 11th digit + const allSum = digits.reduce((sum, d) => sum + d, 0); + digits.push(allSum % 10); + + return digits.join(''); +} + +/** + * Generates a Turkish phone number in the format +905XXXXXXXXX + */ +function generateTurkishPhoneNumber(): string { + const areaCode = faker.helpers.arrayElement(['530', '531', '532', '533', '534', '535', '536', '537', '538', '539', '540', '541', '542', '543', '544', '545', '546', '547', '548', '549', '550', '551', '552', '553', '554', '555', '556', '557', '558', '559']); + const number = faker.string.numeric(7); + return `+90${areaCode}${number}`; +} + +/** + * Generates a random IP address + */ +function generateIpAddress(): string { + return faker.internet.ipv4(); +} + +/** + * Generates a test payment card with randomized holder name + * Card number is fixed to iyzico sandbox test card + */ +export function createTestCard(): PaymentCard { + return { + cardHolderName: faker.person.fullName(), + cardNumber: '5528790000000008', // iyzico sandbox test card + expireMonth: '12', + expireYear: '2030', + cvc: '123', + }; +} + +/** + * Generates a test buyer with randomized personal data + */ +export function createTestBuyer(): Buyer { + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + + return { + id: `BY${faker.string.alphanumeric(6).toUpperCase()}`, + name: firstName, + surname: lastName, + identityNumber: generateTurkishIdentityNumber(), + email: faker.internet.email({ firstName, lastName }).toLowerCase(), + gsmNumber: generateTurkishPhoneNumber(), + registrationAddress: `${faker.location.streetAddress()}, ${faker.location.city()}`, + city: faker.location.city(), + country: 'Turkey', + ip: generateIpAddress(), + zipCode: faker.location.zipCode('#####'), + }; +} + +/** + * Generates a test billing address with randomized data + */ +export function createTestBillingAddress(): BillingAddress { + return { + contactName: faker.person.fullName(), + city: faker.location.city(), + country: 'Turkey', + address: `${faker.location.streetAddress()}, ${faker.location.city()}`, + zipCode: faker.location.zipCode('#####'), + }; +} + +/** + * Generates a test shipping address with randomized data + */ +export function createTestShippingAddress(): ShippingAddress { + return { + contactName: faker.person.fullName(), + city: faker.location.city(), + country: 'Turkey', + address: `${faker.location.streetAddress()}, ${faker.location.city()}`, + zipCode: faker.location.zipCode('#####'), + }; +} + +/** + * Generates test basket items + */ +export function createTestBasketItems(): BasketItem[] { + return [ + { + id: `BI${faker.string.alphanumeric(3).toUpperCase()}`, + name: faker.commerce.productName(), + category1: faker.commerce.department(), + category2: faker.commerce.productAdjective(), + itemType: 'PHYSICAL', + price: 0.3, + }, + { + id: `BI${faker.string.alphanumeric(3).toUpperCase()}`, + name: faker.commerce.productName(), + category1: 'Game', + category2: 'Online Game Items', + itemType: 'VIRTUAL', + price: 0.5, + }, + { + id: `BI${faker.string.alphanumeric(3).toUpperCase()}`, + name: faker.commerce.productName(), + category1: 'Electronics', + category2: faker.commerce.productAdjective(), + itemType: 'PHYSICAL', + price: 0.2, + }, + ]; +} + +/** + * Generates a test 3DS initialize request with randomized data + */ +export function createTest3DSInitializeRequest(): ThreeDSInitializeRequest { + return { + locale: 'tr', + conversationId: faker.string.uuid(), + price: 1.0, + paidPrice: 1.1, + currency: 'TRY', + installment: 1, + paymentChannel: 'WEB', + basketId: `B${faker.string.alphanumeric(5).toUpperCase()}`, + paymentGroup: 'PRODUCT', + callbackUrl: 'https://www.merchant.com/callback', + paymentCard: createTestCard(), + buyer: createTestBuyer(), + billingAddress: createTestBillingAddress(), + shippingAddress: createTestShippingAddress(), + basketItems: createTestBasketItems(), + }; +} + +/** + * Generates a test non-3DS payment request with randomized data + */ +export function createTestNon3DPaymentRequest(): Non3DPaymentRequest { + return { + locale: 'tr', + conversationId: faker.string.uuid(), + price: 1.0, + paidPrice: 1.1, + currency: 'TRY', + installment: 1, + paymentChannel: 'WEB', + basketId: `B${faker.string.alphanumeric(5).toUpperCase()}`, + paymentGroup: 'PRODUCT', + paymentCard: createTestCard(), + buyer: createTestBuyer(), + billingAddress: createTestBillingAddress(), + shippingAddress: createTestShippingAddress(), + basketItems: createTestBasketItems(), + }; +} + +// Legacy exports for backward compatibility (static versions that generate once) +export const testCard = createTestCard(); +export const testBuyer = createTestBuyer(); +export const testBillingAddress = createTestBillingAddress(); +export const testShippingAddress = createTestShippingAddress(); +export const testBasketItems = createTestBasketItems(); +export const test3DSInitializeRequest = createTest3DSInitializeRequest(); +export const testNon3DPaymentRequest = createTestNon3DPaymentRequest(); diff --git a/tests/fixtures/subscription.ts b/tests/fixtures/subscription.ts new file mode 100644 index 0000000..6734135 --- /dev/null +++ b/tests/fixtures/subscription.ts @@ -0,0 +1,193 @@ +/** + * Subscription test fixtures with randomized personal data + */ + +import { faker } from '@faker-js/faker'; +import type { + ProductCreateRequest, + PricingPlanCreateRequest, + SubscriptionCustomer, + SubscriptionPaymentCard, + SubscriptionAddress, + SubscriptionInitializeRequest, + SubscriptionCheckoutFormInitializeRequest, +} from '../../src/types'; + +/** + * Generates a valid Turkish identity number (TC Kimlik No) + */ +function generateTurkishIdentityNumber(): string { + const digits: number[] = []; + + // First digit cannot be 0 + digits.push(faker.number.int({ min: 1, max: 9 })); + + // Generate next 8 random digits + for (let i = 1; i < 9; i++) { + digits.push(faker.number.int({ min: 0, max: 9 })); + } + + // Calculate 10th digit + const oddSum = digits[0] + digits[2] + digits[4] + digits[6] + digits[8]; + const evenSum = digits[1] + digits[3] + digits[5] + digits[7]; + const digit10 = (oddSum * 7 - evenSum) % 10; + digits.push(digit10 < 0 ? digit10 + 10 : digit10); + + // Calculate 11th digit + const allSum = digits.reduce((sum, d) => sum + d, 0); + digits.push(allSum % 10); + + return digits.join(''); +} + +/** + * Generates a Turkish phone number in the format +905XXXXXXXXX + */ +function generateTurkishPhoneNumber(): string { + const areaCode = faker.helpers.arrayElement(['530', '531', '532', '533', '534', '535', '536', '537', '538', '539', '540', '541', '542', '543', '544', '545', '546', '547', '548', '549', '550', '551', '552', '553', '554', '555', '556', '557', '558', '559']); + const number = faker.string.numeric(7); + return `+90${areaCode}${number}`; +} + +/** + * Generates a subscription billing address with randomized data + */ +export function createTestSubscriptionBillingAddress(): SubscriptionAddress { + return { + address: `${faker.location.streetAddress()}, ${faker.location.city()}`, + zipCode: faker.location.zipCode('#####'), + contactName: faker.person.fullName(), + city: faker.location.city(), + country: 'Turkey', + }; +} + +/** + * Generates a subscription shipping address with randomized data + */ +export function createTestSubscriptionShippingAddress(): SubscriptionAddress { + return { + address: `${faker.location.streetAddress()}, ${faker.location.city()}`, + zipCode: faker.location.zipCode('#####'), + contactName: faker.person.fullName(), + city: faker.location.city(), + country: 'Turkey', + }; +} + +/** + * Generates a subscription customer with randomized personal data + */ +export function createTestSubscriptionCustomer(): SubscriptionCustomer { + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + + return { + name: firstName, + surname: lastName, + email: faker.internet.email({ firstName, lastName }).toLowerCase(), + gsmNumber: generateTurkishPhoneNumber(), + identityNumber: generateTurkishIdentityNumber(), + billingAddress: createTestSubscriptionBillingAddress(), + shippingAddress: createTestSubscriptionShippingAddress(), + }; +} + +/** + * Generates a subscription payment card with randomized holder name + * Card number is fixed to iyzico sandbox test card + */ +export function createTestSubscriptionPaymentCard(): SubscriptionPaymentCard { + return { + cardHolderName: faker.person.fullName(), + cardNumber: '5528790000000008', // iyzico sandbox test card + expireMonth: '12', + expireYear: '2030', + cvc: '123', + }; +} + +/** + * Generates a product create request with unique name + */ +export function createTestProductCreateRequest(): ProductCreateRequest { + return { + locale: 'tr', + name: `Test ${faker.string.alphanumeric(6)}`, + description: faker.commerce.productDescription().substring(0,20), + }; +} + +/** + * Generates a pricing plan create request + */ +export function createTestPricingPlanCreateRequest(): PricingPlanCreateRequest { + return { + locale: 'tr', + name: `Monthly Test Plan ${faker.string.alphanumeric(6)}`, + price: 99.99, + currencyCode: 'TRY', + paymentInterval: 'MONTHLY', + paymentIntervalCount: 1, + trialPeriodDays: 7, + planPaymentType: 'RECURRING', + recurrenceCount: 12, + }; +} + +/** + * Generates a subscription initialize request with randomized data + */ +export function createTestSubscriptionInitializeRequest(pricingPlanReferenceCode: string = ''): SubscriptionInitializeRequest { + return { + locale: 'tr', + pricingPlanReferenceCode, + subscriptionInitialStatus: 'ACTIVE', + customer: createTestSubscriptionCustomer(), + paymentCard: createTestSubscriptionPaymentCard(), + }; +} + +/** + * Generates a subscription checkout form request with randomized data + */ +export function createTestSubscriptionCheckoutFormRequest(pricingPlanReferenceCode: string = ''): SubscriptionCheckoutFormInitializeRequest { + return { + locale: 'tr', + callbackUrl: 'https://www.merchant.com/callback', + pricingPlanReferenceCode, + subscriptionInitialStatus: 'ACTIVE', + customer: createTestSubscriptionCustomer(), + }; +} + +/** + * Generates a unique product name for testing + */ +export function generateTestProductName(): string { + return `Test Product ${Date.now()}-${faker.string.alphanumeric(4)}`; +} + +/** + * Generates a unique pricing plan name for testing + */ +export function generateTestPlanName(): string { + return `Test Plan ${Date.now()}-${faker.string.alphanumeric(4)}`; +} + +/** + * Generates a unique email for testing + */ +export function generateTestEmail(): string { + return faker.internet.email().toLowerCase(); +} + +// Legacy exports for backward compatibility (static versions that generate once) +export const testSubscriptionBillingAddress = createTestSubscriptionBillingAddress(); +export const testSubscriptionShippingAddress = createTestSubscriptionShippingAddress(); +export const testSubscriptionCustomer = createTestSubscriptionCustomer(); +export const testSubscriptionPaymentCard = createTestSubscriptionPaymentCard(); +export const testProductCreateRequest = createTestProductCreateRequest(); +export const testPricingPlanCreateRequest = createTestPricingPlanCreateRequest(); +export const testSubscriptionInitializeRequest = createTestSubscriptionInitializeRequest(); +export const testSubscriptionCheckoutFormRequest = createTestSubscriptionCheckoutFormRequest(); diff --git a/tests/integration/bin-check.test.ts b/tests/integration/bin-check.test.ts new file mode 100644 index 0000000..95e4789 --- /dev/null +++ b/tests/integration/bin-check.test.ts @@ -0,0 +1,224 @@ +/** + * BIN Check integration tests + */ + +import { describe, it, expect } from 'vitest'; +import { createTestClient, assertSuccessResponse } from '../setup'; +import type { ErrorResponse } from '../../src/types'; + +describe('BIN Check Integration Tests', () => { + const client = createTestClient(); + + it('should check BIN for a valid card number', async () => { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '535805', + conversationId: 'test-bin-check-1', + }); + + assertSuccessResponse(response); + expect(response.binNumber).toBe('535805'); + expect(response.cardType).toBeTruthy(); + expect(response.cardAssociation).toBeTruthy(); + }); + + it('should check BIN with price for installment details', async () => { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '535805', + price: 100.0, + conversationId: 'test-bin-check-2', + }); + + assertSuccessResponse(response); + expect(response.binNumber).toBe('535805'); + if (response.installmentDetails && response.installmentDetails.length > 0) { + expect(response.installmentDetails[0].installmentPrices).toBeTruthy(); + } + }); + + it('should handle invalid BIN number', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '000000', + conversationId: 'test-bin-check-invalid', + }); + // If API doesn't reject invalid BINs, verify response structure + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + // If it's a success response, verify it has expected structure + if (response.status === 'success') { + expect(response.binNumber).toBe('000000'); + } + } catch (error: any) { + // API might return an error for invalid BIN - verify it's an Iyzico error + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + // Verify error has proper structure + if (error.code) { + expect(typeof error.code).toBe('string'); + } + } + }); + + describe('Edge Cases - Invalid Fields', () => { + it('should handle missing binNumber', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: undefined as any, + conversationId: 'test-missing-bin', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle empty binNumber', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '', + conversationId: 'test-empty-bin', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle binNumber that is too short', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '12345', // 5 digits, should be 6 + conversationId: 'test-short-bin', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle binNumber that is too long', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '1234567', // 7 digits, should be 6 + conversationId: 'test-long-bin', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle binNumber with non-numeric characters', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: 'ABC123', + conversationId: 'test-nonnumeric-bin', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle negative price', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '535805', + price: -100.0, + conversationId: 'test-negative-price', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle zero price', async () => { + try { + const response = await client.binCheck.check({ + locale: 'tr', + binNumber: '535805', + price: 0, + conversationId: 'test-zero-price', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid locale', async () => { + try { + const response = await client.binCheck.check({ + locale: 'invalid' as any, + binNumber: '535805', + conversationId: 'test-invalid-locale', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing locale', async () => { + try { + const response = await client.binCheck.check({ + locale: undefined as any, + binNumber: '535805', + conversationId: 'test-missing-locale', + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + }); +}); + diff --git a/tests/integration/cancel-refund.test.ts b/tests/integration/cancel-refund.test.ts new file mode 100644 index 0000000..3cc967c --- /dev/null +++ b/tests/integration/cancel-refund.test.ts @@ -0,0 +1,367 @@ +/** + * Cancel & Refund integration tests + */ + +import { describe, it, expect } from 'vitest'; +import { createTestClient, assertSuccessResponse } from '../setup'; +import { testNon3DPaymentRequest } from '../fixtures/payment'; +import type { ErrorResponse } from '../../src/types'; + +describe('Cancel & Refund Integration Tests', () => { + const client = createTestClient(); + + it('should cancel a payment', async () => { + // First create a payment + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-cancel-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.paymentId) { + // Then cancel it + const cancelResponse = await client.cancelRefund.cancel({ + paymentId: paymentResponse.paymentId, + locale: 'tr', + conversationId: `test-cancel-${Date.now()}`, + }); + + assertSuccessResponse(cancelResponse); + expect(cancelResponse.paymentId).toBe(paymentResponse.paymentId); + } + }); + + it('should refund a payment using v1 endpoint', async () => { + // First create a payment + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-refund-v1-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.paymentId && paymentResponse.itemTransactions) { + // Get the first payment transaction ID + const firstTransaction = paymentResponse.itemTransactions[0]; + if (firstTransaction && firstTransaction.paymentTransactionId) { + // Then refund it using v1 + const refundResponse = await client.cancelRefund.refund({ + paymentTransactionId: firstTransaction.paymentTransactionId, + price: firstTransaction.price, + locale: 'tr', + conversationId: `test-refund-v1-${Date.now()}`, + }); + + assertSuccessResponse(refundResponse); + expect(refundResponse.paymentTransactionId).toBe( + firstTransaction.paymentTransactionId + ); + } + } + }); + + it('should refund a payment using v2 endpoint', async () => { + // First create a payment + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-refund-v2-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.paymentId && paymentResponse.paidPrice) { + // Then refund it using v2 + const refundResponse = await client.cancelRefund.refundV2({ + paymentId: paymentResponse.paymentId, + price: paymentResponse.paidPrice, + locale: 'tr', + conversationId: `test-refund-v2-${Date.now()}`, + }); + + assertSuccessResponse(refundResponse); + expect(refundResponse.paymentId).toBe(paymentResponse.paymentId); + } + }); + + describe('Edge Cases - Invalid Fields', () => { + it('should handle invalid paymentId for cancel', async () => { + try { + const response = await client.cancelRefund.cancel({ + paymentId: 'invalid-payment-id-99999', + locale: 'tr', + conversationId: `test-invalid-paymentid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle empty paymentId for cancel', async () => { + try { + const response = await client.cancelRefund.cancel({ + paymentId: '', + locale: 'tr', + conversationId: `test-empty-paymentid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing paymentId for cancel', async () => { + try { + const response = await client.cancelRefund.cancel({ + paymentId: undefined as any, + locale: 'tr', + conversationId: `test-missing-paymentid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid paymentTransactionId for refund', async () => { + try { + const response = await client.cancelRefund.refund({ + paymentTransactionId: 'invalid-transaction-id-99999', + price: 1.0, + locale: 'tr', + conversationId: `test-invalid-transactionid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle empty paymentTransactionId for refund', async () => { + try { + const response = await client.cancelRefund.refund({ + paymentTransactionId: '', + price: 1.0, + locale: 'tr', + conversationId: `test-empty-transactionid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing paymentTransactionId for refund', async () => { + try { + const response = await client.cancelRefund.refund({ + paymentTransactionId: undefined as any, + price: 1.0, + locale: 'tr', + conversationId: `test-missing-transactionid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle negative price for refund', async () => { + // First create a payment to get a transaction ID + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-refund-negative-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.itemTransactions && paymentResponse.itemTransactions[0]) { + const firstTransaction = paymentResponse.itemTransactions[0]; + if (firstTransaction.paymentTransactionId) { + try { + const response = await client.cancelRefund.refund({ + paymentTransactionId: firstTransaction.paymentTransactionId, + price: -100.0, + locale: 'tr', + conversationId: `test-refund-negative-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + } + } + }); + + it('should handle zero price for refund', async () => { + // First create a payment to get a transaction ID + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-refund-zero-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.itemTransactions && paymentResponse.itemTransactions[0]) { + const firstTransaction = paymentResponse.itemTransactions[0]; + if (firstTransaction.paymentTransactionId) { + try { + const response = await client.cancelRefund.refund({ + paymentTransactionId: firstTransaction.paymentTransactionId, + price: 0, + locale: 'tr', + conversationId: `test-refund-zero-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + } + } + }); + + it('should handle refund amount greater than original payment', async () => { + // First create a payment + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-refund-excess-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.itemTransactions && paymentResponse.itemTransactions[0]) { + const firstTransaction = paymentResponse.itemTransactions[0]; + if (firstTransaction.paymentTransactionId) { + try { + // Try to refund more than the original amount + const excessAmount = (firstTransaction.price || 0) * 2; + const response = await client.cancelRefund.refund({ + paymentTransactionId: firstTransaction.paymentTransactionId, + price: excessAmount, + locale: 'tr', + conversationId: `test-refund-excess-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + } + } + }); + + it('should handle invalid paymentId for refundV2', async () => { + try { + const response = await client.cancelRefund.refundV2({ + paymentId: 'invalid-payment-id-99999', + price: 1.0, + locale: 'tr', + conversationId: `test-invalid-paymentid-v2-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing price for refund', async () => { + // First create a payment to get a transaction ID + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-refund-missing-price-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.itemTransactions && paymentResponse.itemTransactions[0]) { + const firstTransaction = paymentResponse.itemTransactions[0]; + if (firstTransaction.paymentTransactionId) { + try { + const response = await client.cancelRefund.refund({ + paymentTransactionId: firstTransaction.paymentTransactionId, + price: undefined as any, + locale: 'tr', + conversationId: `test-refund-missing-price-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + } + } + }); + }); +}); + diff --git a/tests/integration/card-storage.test.ts b/tests/integration/card-storage.test.ts new file mode 100644 index 0000000..6b81d74 --- /dev/null +++ b/tests/integration/card-storage.test.ts @@ -0,0 +1,391 @@ +/** + * Card Storage integration tests + * + * NOTE: These tests require the card storage feature to be enabled in the iyzico merchant panel. + * If the feature is not enabled, tests will be skipped automatically. + */ + +import { describe, it, expect } from 'vitest'; +import { createTestClient, assertSuccessResponse } from '../setup'; +import { testCardCreateRequest } from '../fixtures/card'; +import { IyzicoResponseError } from '../../src/errors'; +import type { ErrorResponse } from '../../src/types'; + +describe('Card Storage Integration Tests', () => { + const client = createTestClient(); + + // Flag to track if card storage feature is available + let cardStorageFeatureAvailable = true; + + let cardUserKey: string | undefined; + let cardToken: string | undefined; + + // Helper to skip test if card storage feature is not available + const skipIfUnavailable = () => { + if (!cardStorageFeatureAvailable) { + console.log('Skipping: Card storage feature not enabled in merchant panel'); + return true; + } + return false; + }; + + it('should create a card for a new user', async () => { + const request = { + ...testCardCreateRequest, + conversationId: `test-card-create-${Date.now()}`, + externalId: `external-${Date.now()}`, + }; + + try { + const response = await client.cardStorage.createCard(request); + + assertSuccessResponse(response); + expect(response.cardUserKey).toBeTruthy(); + expect(response.cardToken).toBeTruthy(); + expect(response.lastFourDigits).toBeTruthy(); + + cardUserKey = response.cardUserKey; + cardToken = response.cardToken; + } catch (error) { + // Check if card storage feature is not enabled + if (error instanceof IyzicoResponseError) { + const errorCode = error.errorResponse?.errorCode; + const errorMessage = error.errorResponse?.errorMessage || ''; + // Error code 100001 = system error, or specific message for card storage not enabled + if (errorCode === '100001' || errorMessage.includes('Kart saklama özelliği tanımlı değil')) { + cardStorageFeatureAvailable = false; + console.log('Card storage feature not enabled in merchant panel. Skipping card storage tests.'); + return; // Skip this test gracefully + } + } + throw error; + } + }); + + it('should list cards for a user', async () => { + if (skipIfUnavailable()) return; + + if (!cardUserKey) { + // Create a card first if we don't have one + const createResponse = await client.cardStorage.createCard({ + ...testCardCreateRequest, + conversationId: `test-list-${Date.now()}`, + externalId: `external-list-${Date.now()}`, + }); + cardUserKey = createResponse.cardUserKey; + } + + const response = await client.cardStorage.listCards({ + cardUserKey: cardUserKey!, + locale: 'tr', + conversationId: `test-list-${Date.now()}`, + }); + + assertSuccessResponse(response); + expect(response.cardUserKey).toBe(cardUserKey); + expect(response.cardDetails).toBeTruthy(); + }); + + it('should delete a card', async () => { + if (skipIfUnavailable()) return; + + if (!cardUserKey || !cardToken) { + // Create a card first if we don't have one + const createResponse = await client.cardStorage.createCard({ + ...testCardCreateRequest, + conversationId: `test-delete-${Date.now()}`, + externalId: `external-delete-${Date.now()}`, + }); + cardUserKey = createResponse.cardUserKey; + cardToken = createResponse.cardToken; + } + + const response = await client.cardStorage.deleteCard({ + cardUserKey: cardUserKey!, + cardToken: cardToken!, + locale: 'tr', + conversationId: `test-delete-${Date.now()}`, + }); + + assertSuccessResponse(response); + }); + + describe('Edge Cases - Invalid Fields', () => { + it('should handle missing card data', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-missing-card-${Date.now()}`, + email: 'email@email.com', + externalId: `external-${Date.now()}`, + card: undefined as any, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing email', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-missing-email-${Date.now()}`, + email: undefined as any, + externalId: `external-${Date.now()}`, + card: testCardCreateRequest.card, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid email format', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-invalid-email-${Date.now()}`, + email: 'invalid-email-format', + externalId: `external-${Date.now()}`, + card: testCardCreateRequest.card, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing externalId', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-missing-externalid-${Date.now()}`, + email: 'email@email.com', + externalId: undefined as any, + card: testCardCreateRequest.card, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid card number', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-invalid-card-number-${Date.now()}`, + email: 'email@email.com', + externalId: `external-${Date.now()}`, + card: { + ...testCardCreateRequest.card, + cardNumber: '1234', + }, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid expiry month', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-invalid-expmonth-${Date.now()}`, + email: 'email@email.com', + externalId: `external-${Date.now()}`, + card: { + ...testCardCreateRequest.card, + expireMonth: '13', + }, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid expiry year (past year)', async () => { + if (skipIfUnavailable()) return; + + const pastYear = new Date().getFullYear() - 1; + try { + const response = await client.cardStorage.createCard({ + locale: 'tr', + conversationId: `test-invalid-expyear-${Date.now()}`, + email: 'email@email.com', + externalId: `external-${Date.now()}`, + card: { + ...testCardCreateRequest.card, + expireYear: String(pastYear), + }, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid cardUserKey for listCards', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.listCards({ + cardUserKey: 'invalid-user-key-99999', + locale: 'tr', + conversationId: `test-invalid-userkey-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle empty cardUserKey for listCards', async () => { + if (skipIfUnavailable()) return; + + try { + const response = await client.cardStorage.listCards({ + cardUserKey: '', + locale: 'tr', + conversationId: `test-empty-userkey-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid cardToken for deleteCard', async () => { + if (skipIfUnavailable()) return; + + // First create a card to get a valid cardUserKey + const createResponse = await client.cardStorage.createCard({ + ...testCardCreateRequest, + conversationId: `test-delete-invalid-token-${Date.now()}`, + externalId: `external-${Date.now()}`, + }); + assertSuccessResponse(createResponse); + + if (createResponse.cardUserKey) { + try { + const response = await client.cardStorage.deleteCard({ + cardUserKey: createResponse.cardUserKey, + cardToken: 'invalid-card-token-99999', + locale: 'tr', + conversationId: `test-delete-invalid-token-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + } + }); + + it('should handle empty cardToken for deleteCard', async () => { + if (skipIfUnavailable()) return; + + // First create a card to get a valid cardUserKey + const createResponse = await client.cardStorage.createCard({ + ...testCardCreateRequest, + conversationId: `test-delete-empty-token-${Date.now()}`, + externalId: `external-${Date.now()}`, + }); + assertSuccessResponse(createResponse); + + if (createResponse.cardUserKey) { + try { + const response = await client.cardStorage.deleteCard({ + cardUserKey: createResponse.cardUserKey, + cardToken: '', + locale: 'tr', + conversationId: `test-delete-empty-token-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + } + }); + }); +}); diff --git a/tests/integration/payment.test.ts b/tests/integration/payment.test.ts new file mode 100644 index 0000000..960e096 --- /dev/null +++ b/tests/integration/payment.test.ts @@ -0,0 +1,373 @@ +/** + * Payment integration tests + */ + +import { describe, it, expect } from 'vitest'; +import { createTestClient, assertSuccessResponse } from '../setup'; +import { testNon3DPaymentRequest, test3DSInitializeRequest } from '../fixtures/payment'; +import { IyzicoResponseError } from '../../src/errors'; + +describe('Payment Integration Tests', () => { + const client = createTestClient(); + + it('should create a non-3DS payment', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-non3ds-${Date.now()}`, + }; + + const response = await client.payment.createNon3DS(request); + + assertSuccessResponse(response); + expect(response.paymentId).toBeTruthy(); + expect(response.price).toBe(request.price); + }); + + it('should get payment detail by paymentId', async () => { + // First create a payment + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-detail-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (!paymentResponse.paymentId) { + throw new Error('Payment ID is required for detail test'); + } + + // Then get payment detail + const detailResponse = await client.payment.getDetail({ + paymentId: paymentResponse.paymentId, + locale: 'tr', + }); + + assertSuccessResponse(detailResponse); + expect(detailResponse.paymentId).toBe(paymentResponse.paymentId); + }); + + it('should initialize 3DS payment', async () => { + const request = { + ...test3DSInitializeRequest, + conversationId: `test-3ds-init-${Date.now()}`, + }; + + const response = await client.payment.initialize3DS(request); + + assertSuccessResponse(response); + expect(response.paymentId).toBeTruthy(); + expect(response.htmlContent || response.threeDSHtmlContent).toBeTruthy(); + }); + + it('should complete 3DS v1 payment', async () => { + // First initialize a 3DS payment + const initRequest = { + ...test3DSInitializeRequest, + conversationId: `test-3ds-v1-${Date.now()}`, + }; + + const initResponse = await client.payment.initialize3DS(initRequest); + assertSuccessResponse(initResponse); + + if (!initResponse.paymentId) { + throw new Error('Payment ID is required for 3DS completion test'); + } + + // In sandbox, 3DS completion will fail without actual 3DS flow + // We expect an IyzicoResponseError to be thrown + await expect( + client.payment.complete3DS({ + paymentId: initResponse.paymentId, + locale: 'tr', + conversationId: `test-3ds-v1-complete-${Date.now()}`, + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should complete 3DS v2 payment', async () => { + // First initialize a 3DS payment + const initRequest = { + ...test3DSInitializeRequest, + conversationId: `test-3ds-v2-${Date.now()}`, + }; + + const initResponse = await client.payment.initialize3DS(initRequest); + assertSuccessResponse(initResponse); + + if (!initResponse.paymentId) { + throw new Error('Payment ID is required for 3DS completion test'); + } + + // In sandbox, 3DS completion will fail without actual 3DS flow + // We expect an IyzicoResponseError to be thrown + await expect( + client.payment.complete3DSV2({ + paymentId: initResponse.paymentId, + locale: 'tr', + conversationId: `test-3ds-v2-complete-${Date.now()}`, + paidPrice: initRequest.paidPrice, + basketId: initRequest.basketId!, + currency: initRequest.currency || 'TRY', + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + describe('Edge Cases - Invalid Fields', () => { + it('should handle missing required fields (price)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-missing-price-${Date.now()}`, + price: undefined as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing required fields (paidPrice)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-missing-paidprice-${Date.now()}`, + paidPrice: undefined as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing required fields (paymentCard)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-missing-card-${Date.now()}`, + paymentCard: undefined as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing required fields (buyer)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-missing-buyer-${Date.now()}`, + buyer: undefined as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing required fields (billingAddress)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-missing-billing-${Date.now()}`, + billingAddress: undefined as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing required fields (basketItems)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-missing-basket-${Date.now()}`, + basketItems: undefined as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle empty basketItems array', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-empty-basket-${Date.now()}`, + basketItems: [], + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid card number (too short)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-card-short-${Date.now()}`, + paymentCard: { + ...testNon3DPaymentRequest.paymentCard, + cardNumber: '1234', + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid card number (non-numeric)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-card-nonnumeric-${Date.now()}`, + paymentCard: { + ...testNon3DPaymentRequest.paymentCard, + cardNumber: 'ABCDEFGHIJKLMNOP', + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid expiry month (invalid format)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-expmonth-${Date.now()}`, + paymentCard: { + ...testNon3DPaymentRequest.paymentCard, + expireMonth: '13', + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid expiry year (invalid format)', async () => { + // Note: Sandbox doesn't validate past years, so we test invalid format instead + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-expyear-format-${Date.now()}`, + paymentCard: { + ...testNon3DPaymentRequest.paymentCard, + expireYear: 'AB', // Invalid format - non-numeric + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid CVV (too short)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-cvv-short-${Date.now()}`, + paymentCard: { + ...testNon3DPaymentRequest.paymentCard, + cvc: '12', + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid CVV (non-numeric)', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-cvv-nonnumeric-${Date.now()}`, + paymentCard: { + ...testNon3DPaymentRequest.paymentCard, + cvc: 'ABC', + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle negative price', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-negative-price-${Date.now()}`, + price: -100.0, + paidPrice: -100.0, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle zero price', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-zero-price-${Date.now()}`, + price: 0, + paidPrice: 0, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid currency', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-currency-${Date.now()}`, + currency: 'INVALID' as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid installment value', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-installment-${Date.now()}`, + installment: 99 as any, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid email format', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-invalid-email-${Date.now()}`, + buyer: { + ...testNon3DPaymentRequest.buyer, + email: 'invalid-email-format', + }, + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle basket item with negative price', async () => { + const request = { + ...testNon3DPaymentRequest, + conversationId: `test-negative-basket-price-${Date.now()}`, + basketItems: [ + { + ...testNon3DPaymentRequest.basketItems[0], + price: -10.0, + }, + ], + }; + + await expect(client.payment.createNon3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing callbackUrl for 3DS initialize', async () => { + const request = { + ...test3DSInitializeRequest, + conversationId: `test-missing-callback-${Date.now()}`, + callbackUrl: undefined as any, + }; + + await expect(client.payment.initialize3DS(request)).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid paymentId for getDetail', async () => { + await expect( + client.payment.getDetail({ + paymentId: 'invalid-payment-id-12345', + locale: 'tr', + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle empty paymentId for getDetail', async () => { + await expect( + client.payment.getDetail({ + paymentId: '', + locale: 'tr', + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing paymentId and paymentConversationId for getDetail', async () => { + await expect( + client.payment.getDetail({ + locale: 'tr', + }) + ).rejects.toThrow(IyzicoResponseError); + }); + }); +}); + diff --git a/tests/integration/reporting.test.ts b/tests/integration/reporting.test.ts new file mode 100644 index 0000000..7220daa --- /dev/null +++ b/tests/integration/reporting.test.ts @@ -0,0 +1,338 @@ +/** + * Reporting integration tests + */ + +import { describe, it, expect } from 'vitest'; +import { createTestClient, assertSuccessResponse } from '../setup'; +import type { ErrorResponse } from '../../src/types'; + +describe('Reporting Integration Tests', () => { + const client = createTestClient(); + + it('should get payment transactions for a date', async () => { + // Use today's date + const today = new Date(); + const dateString = today.toISOString().split('T')[0]; // YYYY-MM-DD format + + const response = await client.reporting.getPaymentTransactions({ + page: 1, + transactionDate: dateString, + locale: 'tr', + conversationId: `test-transactions-${Date.now()}`, + }); + + assertSuccessResponse(response); + expect(response.currentPage).toBe(1); + // Transactions might be empty if no transactions exist for the date + expect(response.transactions).toBeDefined(); + }); + + it('should get payment details by paymentId', async () => { + // First create a payment to get a paymentId + const { testNon3DPaymentRequest } = await import('../fixtures/payment'); + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId: `test-reporting-${Date.now()}`, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.paymentId) { + // Then get payment details + const detailsResponse = await client.reporting.getPaymentDetails({ + paymentId: paymentResponse.paymentId, + locale: 'tr', + conversationId: `test-details-${Date.now()}`, + }); + + assertSuccessResponse(detailsResponse); + expect(detailsResponse.payments).toBeTruthy(); + if (detailsResponse.payments && detailsResponse.payments.length > 0) { + // Convert to string for comparison as API may return number or string + const returnedPaymentId = String(detailsResponse.payments[0].paymentId); + expect(returnedPaymentId).toBe(String(paymentResponse.paymentId)); + } + } + }); + + it('should get payment details by paymentConversationId', async () => { + // First create a payment to get a conversationId + const { testNon3DPaymentRequest } = await import('../fixtures/payment'); + const conversationId = `test-reporting-conv-${Date.now()}`; + const paymentRequest = { + ...testNon3DPaymentRequest, + conversationId, + }; + + const paymentResponse = await client.payment.createNon3DS(paymentRequest); + assertSuccessResponse(paymentResponse); + + if (paymentResponse.conversationId) { + // Then get payment details using paymentConversationId + const detailsResponse = await client.reporting.getPaymentDetails({ + paymentConversationId: paymentResponse.conversationId, + locale: 'tr', + conversationId: `test-details-conv-${Date.now()}`, + }); + + assertSuccessResponse(detailsResponse); + expect(detailsResponse.payments).toBeTruthy(); + } + }); + + describe('Edge Cases - Invalid Fields', () => { + it('should handle invalid date format for getPaymentTransactions', async () => { + try { + const response = await client.reporting.getPaymentTransactions({ + page: 1, + transactionDate: 'invalid-date-format', + locale: 'tr', + conversationId: `test-invalid-date-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle future date for getPaymentTransactions', async () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + const dateString = futureDate.toISOString().split('T')[0]; + + try { + const response = await client.reporting.getPaymentTransactions({ + page: 1, + transactionDate: dateString, + locale: 'tr', + conversationId: `test-future-date-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + // Future dates might return empty results or error + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle negative page number', async () => { + const today = new Date(); + const dateString = today.toISOString().split('T')[0]; + + try { + const response = await client.reporting.getPaymentTransactions({ + page: -1, + transactionDate: dateString, + locale: 'tr', + conversationId: `test-negative-page-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle zero page number', async () => { + const today = new Date(); + const dateString = today.toISOString().split('T')[0]; + + try { + const response = await client.reporting.getPaymentTransactions({ + page: 0, + transactionDate: dateString, + locale: 'tr', + conversationId: `test-zero-page-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle very large page number', async () => { + const today = new Date(); + const dateString = today.toISOString().split('T')[0]; + + try { + const response = await client.reporting.getPaymentTransactions({ + page: 999999, + transactionDate: dateString, + locale: 'tr', + conversationId: `test-large-page-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + // Large page numbers might return empty results or error + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing transactionDate', async () => { + try { + const response = await client.reporting.getPaymentTransactions({ + page: 1, + transactionDate: undefined as any, + locale: 'tr', + conversationId: `test-missing-date-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid paymentId for getPaymentDetails', async () => { + try { + const response = await client.reporting.getPaymentDetails({ + paymentId: 'invalid-payment-id-99999', + locale: 'tr', + conversationId: `test-invalid-paymentid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle empty paymentId for getPaymentDetails', async () => { + try { + const response = await client.reporting.getPaymentDetails({ + paymentId: '', + locale: 'tr', + conversationId: `test-empty-paymentid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid paymentConversationId for getPaymentDetails', async () => { + try { + const response = await client.reporting.getPaymentDetails({ + paymentConversationId: 'invalid-conversation-id-99999', + locale: 'tr', + conversationId: `test-invalid-convid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle empty paymentConversationId for getPaymentDetails', async () => { + try { + const response = await client.reporting.getPaymentDetails({ + paymentConversationId: '', + locale: 'tr', + conversationId: `test-empty-convid-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle missing both paymentId and paymentConversationId', async () => { + try { + const response = await client.reporting.getPaymentDetails({ + locale: 'tr', + conversationId: `test-missing-ids-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + + it('should handle invalid locale', async () => { + const today = new Date(); + const dateString = today.toISOString().split('T')[0]; + + try { + const response = await client.reporting.getPaymentTransactions({ + page: 1, + transactionDate: dateString, + locale: 'invalid' as any, + conversationId: `test-invalid-locale-${Date.now()}`, + }); + expect(response).toBeDefined(); + expect(response.status).toBeDefined(); + if (response.status === 'failure') { + const errorResponse = response as unknown as ErrorResponse; + expect(errorResponse.errorCode || errorResponse.errorMessage).toBeTruthy(); + } + } catch (error: any) { + expect(error).toBeDefined(); + expect(error).toBeInstanceOf(Error); + } + }); + }); +}); + diff --git a/tests/integration/subscription.test.ts b/tests/integration/subscription.test.ts new file mode 100644 index 0000000..7eae08f --- /dev/null +++ b/tests/integration/subscription.test.ts @@ -0,0 +1,556 @@ +/** + * Subscription integration tests + * + * NOTE: These tests require the subscription feature to be enabled in the iyzico merchant panel. + * If the feature is not enabled, tests will be skipped automatically. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { createTestClient, assertSuccessResponse } from '../setup'; +import { + testProductCreateRequest, + testPricingPlanCreateRequest, + testSubscriptionCustomer, + testSubscriptionPaymentCard, + generateTestProductName, + generateTestPlanName, + generateTestEmail, +} from '../fixtures/subscription'; +import { IyzicoResponseError } from '../../src/errors'; + +describe('Subscription Integration Tests', () => { + const client = createTestClient(); + + // Flag to track if subscription feature is available + let subscriptionFeatureAvailable = true; + + // Shared references for chained tests + let createdProductReferenceCode: string | undefined; + let createdPricingPlanReferenceCode: string | undefined; + let createdSubscriptionReferenceCode: string | undefined; + let createdCustomerReferenceCode: string | undefined; + + // Helper to skip test if subscription feature is not available + const skipIfUnavailable = () => { + if (!subscriptionFeatureAvailable) { + console.log('Skipping: Subscription feature not enabled in merchant panel'); + return true; + } + return false; + }; + + // ============================================================================ + // Product CRUD Tests + // ============================================================================ + + describe('Product Operations', () => { + it('should create a subscription product', async () => { + const request = { + ...testProductCreateRequest, + // conversationId: `test-product-create-${Date.now()}`, + // name: generateTestProductName(), + }; + + try { + const response = await client.subscription.createProduct(request); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.referenceCode).toBeTruthy(); + expect(response.data?.name).toBe(request.name); + expect(response.data?.status).toBe('ACTIVE'); + + // Save for later tests + createdProductReferenceCode = response.data?.referenceCode; + + } catch (error) { + // Check if subscription feature is not enabled (error code 100001 = system error) + if (error instanceof IyzicoResponseError && error.errorResponse?.errorCode === '100001') { + subscriptionFeatureAvailable = false; + console.log('Subscription feature not enabled in merchant panel. Skipping subscription tests.'); + return; // Skip this test gracefully + } + throw error; + } + }); + + it('should get a subscription product', async () => { + if (skipIfUnavailable() || !createdProductReferenceCode) { + return; // Skip if feature unavailable or no product created + } + + const response = await client.subscription.getProduct(createdProductReferenceCode); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.referenceCode).toBe(createdProductReferenceCode); + + }); + + it('should update a subscription product', async () => { + if (skipIfUnavailable() || !createdProductReferenceCode) { + return; // Skip if feature unavailable or no product created + } + + const updatedName = `Updated Product ${Date.now()}`; + const response = await client.subscription.updateProduct(createdProductReferenceCode, { + name: updatedName, + description: 'Updated description', + conversationId: `test-product-update-${Date.now()}`, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.name).toBe(updatedName); + + }); + + it('should list subscription products', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + const response = await client.subscription.listProducts({ + page: 1, + count: 10, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.items).toBeInstanceOf(Array); + expect(response.data?.totalCount).toBeGreaterThanOrEqual(0); + + }); + }); + + // ============================================================================ + // Pricing Plan CRUD Tests + // ============================================================================ + + describe('Pricing Plan Operations', () => { + it('should create a pricing plan', async () => { + if (skipIfUnavailable() || !createdProductReferenceCode) { + return; // Skip if feature unavailable or no product created + } + + const request = { + ...testPricingPlanCreateRequest, + conversationId: `test-plan-create-${Date.now()}`, + name: generateTestPlanName(), + }; + + const response = await client.subscription.createPricingPlan(createdProductReferenceCode, request); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.referenceCode).toBeTruthy(); + expect(response.data?.name).toBe(request.name); + expect(response.data?.price).toBe(request.price); + expect(response.data?.paymentInterval).toBe(request.paymentInterval); + + // Save for later tests + createdPricingPlanReferenceCode = response.data?.referenceCode; + + }); + + it('should get a pricing plan', async () => { + if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { + return; // Skip if feature unavailable or no plan created + } + + const response = await client.subscription.getPricingPlan(createdPricingPlanReferenceCode); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.referenceCode).toBe(createdPricingPlanReferenceCode); + + }); + + it('should update a pricing plan', async () => { + if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { + return; // Skip if feature unavailable or no plan created + } + + const updatedName = `Updated Plan ${Date.now()}`; + const response = await client.subscription.updatePricingPlan(createdPricingPlanReferenceCode, { + name: updatedName, + trialPeriodDays: 14, + conversationId: `test-plan-update-${Date.now()}`, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.name).toBe(updatedName); + + }); + + it('should list pricing plans for a product', async () => { + if (skipIfUnavailable() || !createdProductReferenceCode) { + return; // Skip if feature unavailable or no product created + } + + const response = await client.subscription.listPricingPlans(createdProductReferenceCode, { + page: 1, + count: 10, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.items).toBeInstanceOf(Array); + + }); + }); + + // ============================================================================ + // Subscription Lifecycle Tests + // ============================================================================ + + describe('Subscription Operations', () => { + it('should initialize a subscription', async () => { + if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { + return; // Skip if feature unavailable or no plan created + } + + const uniqueEmail = generateTestEmail(); + const response = await client.subscription.initialize({ + locale: 'tr', + conversationId: `test-sub-init-${Date.now()}`, + pricingPlanReferenceCode: createdPricingPlanReferenceCode, + subscriptionInitialStatus: 'ACTIVE', + customer: { + ...testSubscriptionCustomer, + email: uniqueEmail, + }, + paymentCard: testSubscriptionPaymentCard, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.referenceCode).toBeTruthy(); + expect(response.data?.customerReferenceCode).toBeTruthy(); + + // Save for later tests + createdSubscriptionReferenceCode = response.data?.referenceCode; + createdCustomerReferenceCode = response.data?.customerReferenceCode; + + }); + + it('should get a subscription', async () => { + if (skipIfUnavailable() || !createdSubscriptionReferenceCode) { + return; // Skip if feature unavailable or no subscription created + } + + const response = await client.subscription.get(createdSubscriptionReferenceCode); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + + }); + + it('should list subscriptions', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + const response = await client.subscription.list({ + page: 1, + count: 10, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.items).toBeInstanceOf(Array); + + }); + + it('should list subscriptions with filters', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + const response = await client.subscription.list({ + subscriptionStatus: 'ACTIVE', + page: 1, + count: 10, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + + }); + + it('should initialize subscription with checkout form', async () => { + if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { + return; // Skip if feature unavailable or no plan created + } + + const uniqueEmail = generateTestEmail(); + const response = await client.subscription.initializeCheckoutForm({ + locale: 'tr', + conversationId: `test-sub-checkout-${Date.now()}`, + callbackUrl: 'https://www.merchant.com/callback', + pricingPlanReferenceCode: createdPricingPlanReferenceCode, + subscriptionInitialStatus: 'ACTIVE', + customer: { + ...testSubscriptionCustomer, + email: uniqueEmail, + }, + }); + + assertSuccessResponse(response); + expect(response.token).toBeTruthy(); + expect(response.checkoutFormContent).toBeTruthy(); + expect(response.tokenExpireTime).toBeGreaterThan(0); + + }); + + it('should cancel a subscription', async () => { + if (skipIfUnavailable() || !createdSubscriptionReferenceCode) { + return; // Skip if feature unavailable or no subscription created + } + + const response = await client.subscription.cancel(createdSubscriptionReferenceCode); + + assertSuccessResponse(response); + + }); + }); + + // ============================================================================ + // Customer Operations Tests + // ============================================================================ + + describe('Customer Operations', () => { + it('should list customers', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + const response = await client.subscription.listCustomers({ + page: 1, + count: 10, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.items).toBeInstanceOf(Array); + + }); + + it('should get a customer', async () => { + if (skipIfUnavailable() || !createdCustomerReferenceCode) { + return; // Skip if feature unavailable or no customer created + } + + const response = await client.subscription.getCustomer(createdCustomerReferenceCode); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.referenceCode).toBe(createdCustomerReferenceCode); + + }); + + it('should update a customer', async () => { + if (skipIfUnavailable() || !createdCustomerReferenceCode) { + return; // Skip if feature unavailable or no customer created + } + + const response = await client.subscription.updateCustomer(createdCustomerReferenceCode, { + name: 'Updated John', + surname: 'Updated Doe', + conversationId: `test-customer-update-${Date.now()}`, + }); + + assertSuccessResponse(response); + expect(response.data).toBeDefined(); + expect(response.data?.name).toBe('Updated John'); + + }); + }); + + // ============================================================================ + // Card Update Tests + // ============================================================================ + + describe('Card Update Operations', () => { + it('should initialize card update checkout form', async () => { + if (skipIfUnavailable() || !createdCustomerReferenceCode) { + return; // Skip if feature unavailable or no customer created + } + + try { + const response = await client.subscription.initializeCardUpdate({ + locale: 'tr', + conversationId: `test-card-update-${Date.now()}`, + callbackUrl: 'https://www.merchant.com/callback', + customerReferenceCode: createdCustomerReferenceCode, + }); + + assertSuccessResponse(response); + expect(response.token).toBeTruthy(); + expect(response.checkoutFormContent).toBeTruthy(); + + } catch (error) { + // Card update requires an active subscription - may fail if subscription was cancelled + if (error instanceof IyzicoResponseError) { + const errorMessage = error.errorResponse?.errorMessage || ''; + if (errorMessage.includes('aktif abonelik bulunamadı')) { + console.log('Skipping: No active subscription found for card update test'); + return; + } + } + throw error; + } + }); + }); + + // ============================================================================ + // Cleanup - Delete created resources + // ============================================================================ + + describe('Cleanup', () => { + it('should delete pricing plan', async () => { + if (skipIfUnavailable() || !createdPricingPlanReferenceCode) { + console.log('No pricing plan to delete'); + return; + } + + try { + const response = await client.subscription.deletePricingPlan(createdPricingPlanReferenceCode); + assertSuccessResponse(response); + } catch (error) { + // Deletion may fail if there are active subscriptions - this is expected + if (error instanceof IyzicoResponseError) { + console.log(`Could not delete pricing plan: ${error.errorResponse?.errorMessage || error.message}`); + return; // Don't fail the test for cleanup issues + } + throw error; + } + }); + + it('should delete product', async () => { + if (skipIfUnavailable() || !createdProductReferenceCode) { + console.log('No product to delete'); + return; + } + + try { + const response = await client.subscription.deleteProduct(createdProductReferenceCode); + assertSuccessResponse(response); + } catch (error) { + // Deletion may fail if there are active pricing plans/subscriptions - this is expected + if (error instanceof IyzicoResponseError) { + console.log(`Could not delete product: ${error.errorResponse?.errorMessage || error.message}`); + return; // Don't fail the test for cleanup issues + } + throw error; + } + }); + }); + + // ============================================================================ + // Edge Cases and Error Handling + // ============================================================================ + + describe('Edge Cases - Invalid Inputs', () => { + it('should handle non-existent product reference code', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.getProduct('invalid-product-ref-12345') + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle non-existent pricing plan reference code', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.getPricingPlan('invalid-plan-ref-12345') + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle non-existent subscription reference code', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.get('invalid-sub-ref-12345') + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle non-existent customer reference code', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.getCustomer('invalid-customer-ref-12345') + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle missing required fields in product creation', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.createProduct({ + name: '', + conversationId: `test-empty-name-${Date.now()}`, + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle invalid pricing plan creation without product', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.createPricingPlan('invalid-product-ref', { + ...testPricingPlanCreateRequest, + conversationId: `test-invalid-product-${Date.now()}`, + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle subscription initialization with invalid plan', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + const uniqueEmail = generateTestEmail(); + await expect( + client.subscription.initialize({ + locale: 'tr', + conversationId: `test-invalid-plan-${Date.now()}`, + pricingPlanReferenceCode: 'invalid-plan-ref', + subscriptionInitialStatus: 'ACTIVE', + customer: { + ...testSubscriptionCustomer, + email: uniqueEmail, + }, + paymentCard: testSubscriptionPaymentCard, + }) + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle retry payment with invalid reference', async () => { + if (skipIfUnavailable()) { + return; // Skip if feature unavailable + } + + await expect( + client.subscription.retryPayment({ + referenceCode: 'invalid-order-ref-12345', + conversationId: `test-invalid-retry-${Date.now()}`, + }) + ).rejects.toThrow(IyzicoResponseError); + }); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..50db42a --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,85 @@ +/** + * Test setup and utilities + */ + +import { config } from 'dotenv'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { IyzicoClient } from '../src/client'; + +// Get the directory of this file (for ESM compatibility) +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load environment variables from .env file +// Resolve path relative to project root (two levels up from tests/setup.ts) +const envPath = resolve(__dirname, '..', '.env'); +config({ path: envPath }); + +/** + * Creates an İyzico client for testing using environment variables + * @returns Configured IyzicoClient instance + */ +export function createTestClient(): IyzicoClient { + const apiKey = process.env.IYZICO_API_KEY; + const secretKey = process.env.IYZICO_API_SECRET; + const baseUrl = process.env.IYZICO_BASE_URL || 'https://sandbox-api.iyzipay.com'; + + if (!apiKey) { + throw new Error('IYZICO_API_KEY environment variable is required for tests'); + } + if (!secretKey) { + throw new Error('IYZICO_API_SECRET environment variable is required for tests'); + } + + // Enable retry on rate limit for tests + const retryOnRateLimit = process.env.IYZICO_RETRY_ON_RATE_LIMIT !== 'false'; + const maxRetries = Number.parseInt(process.env.IYZICO_MAX_RETRIES || '3', 10); + const retryDelay = Number.parseInt(process.env.IYZICO_RETRY_DELAY_MS || '10000', 10); + + return new IyzicoClient({ + apiKey, + secretKey, + baseUrl, + locale: 'tr', + retryOnRateLimit, + maxRetries, + retryDelay, + }); +} + +/** + * Asserts that a response has a success status + */ +export function assertSuccessResponse(response: { status: string }): void { + if (response.status !== 'success') { + throw new Error(`Expected success status, got: ${response.status}`); + } +} + +/** + * Asserts that a response has a failure status + */ +export function assertFailureResponse(response: { status: string }): void { + if (response.status !== 'failure') { + throw new Error(`Expected failure status, got: ${response.status}`); + } +} + +/** + * Delay utility to prevent rate limiting + * Adds a small delay between test requests + */ +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Default delay between API calls to avoid rate limiting + * Can be overridden via IYZICO_TEST_DELAY_MS environment variable + */ +export const DEFAULT_TEST_DELAY_MS = Number.parseInt( + process.env.IYZICO_TEST_DELAY_MS || '200', + 10 +); + diff --git a/tests/unit/auth.test.ts b/tests/unit/auth.test.ts new file mode 100644 index 0000000..cb82e6f --- /dev/null +++ b/tests/unit/auth.test.ts @@ -0,0 +1,62 @@ +/** + * Authentication unit tests + */ + +import { describe, it, expect } from 'vitest'; +import { generateAuthorization } from '../../src/auth'; + +describe('generateAuthorization', () => { + const apiKey = 'sandbox-api-key'; + const secretKey = 'sandbox-secret-key'; + const uriPath = '/payment/bin/check'; + + it('should generate authorization header with request body', () => { + const requestBody = { binNumber: '589004', locale: 'tr' }; + const result = generateAuthorization(apiKey, secretKey, uriPath, requestBody); + + expect(result.authorization).toMatch(/^IYZWSv2 /); + expect(result.randomKey).toBeTruthy(); + expect(result.randomKey.length).toBeGreaterThan(0); + }); + + it('should generate authorization header without request body', () => { + const result = generateAuthorization(apiKey, secretKey, uriPath); + + expect(result.authorization).toMatch(/^IYZWSv2 /); + expect(result.randomKey).toBeTruthy(); + }); + + it('should generate different random keys for each call', () => { + const result1 = generateAuthorization(apiKey, secretKey, uriPath); + const result2 = generateAuthorization(apiKey, secretKey, uriPath); + + expect(result1.randomKey).not.toBe(result2.randomKey); + }); + + it('should use provided random key when given', () => { + const customRandomKey = '123456789'; + const result = generateAuthorization(apiKey, secretKey, uriPath, undefined, customRandomKey); + + expect(result.randomKey).toBe(customRandomKey); + }); + + it('should generate valid base64 encoded authorization', () => { + const result = generateAuthorization(apiKey, secretKey, uriPath); + const base64Part = result.authorization.replace('IYZWSv2 ', ''); + + // Base64 should be valid (only contains A-Z, a-z, 0-9, +, /, =) + expect(base64Part).toMatch(/^[A-Za-z0-9+/=]+$/); + }); + + it('should include apiKey, randomKey, and signature in authorization string', () => { + const customRandomKey = '123456789'; + const result = generateAuthorization(apiKey, secretKey, uriPath, undefined, customRandomKey); + const base64Part = result.authorization.replace('IYZWSv2 ', ''); + const decoded = Buffer.from(base64Part, 'base64').toString('utf-8'); + + expect(decoded).toContain(`apiKey:${apiKey}`); + expect(decoded).toContain(`randomKey:${customRandomKey}`); + expect(decoded).toContain('signature:'); + }); +}); + diff --git a/tests/unit/client.test.ts b/tests/unit/client.test.ts new file mode 100644 index 0000000..6ba0821 --- /dev/null +++ b/tests/unit/client.test.ts @@ -0,0 +1,97 @@ +/** + * Client unit tests + */ + +import { describe, it, expect } from 'vitest'; +import { IyzicoClient } from '../../src/client'; + +describe('IyzicoClient', () => { + const apiKey = 'test-api-key'; + const secretKey = 'test-secret-key'; + + it('should create a client with required config', () => { + const client = new IyzicoClient({ + apiKey, + secretKey, + }); + + expect(client.apiKey).toBe(apiKey); + expect(client.secretKey).toBe(secretKey); + expect(client.baseUrl).toBe('https://sandbox-api.iyzipay.com'); + expect(client.locale).toBe('tr'); + }); + + it('should throw error when apiKey is missing', () => { + expect(() => { + new IyzicoClient({ + apiKey: '', + secretKey, + } as any); + }).toThrow('API key is required'); + }); + + it('should throw error when secretKey is missing', () => { + expect(() => { + new IyzicoClient({ + apiKey, + secretKey: '', + } as any); + }).toThrow('Secret key is required'); + }); + + it('should use custom baseUrl when provided', () => { + const customUrl = 'https://api.custom.com'; + const client = new IyzicoClient({ + apiKey, + secretKey, + baseUrl: customUrl, + }); + + expect(client.baseUrl).toBe(customUrl); + }); + + it('should use custom locale when provided', () => { + const client = new IyzicoClient({ + apiKey, + secretKey, + locale: 'en', + }); + + expect(client.locale).toBe('en'); + }); + + it('should initialize all services', () => { + const client = new IyzicoClient({ + apiKey, + secretKey, + }); + + expect(client.payment).toBeDefined(); + expect(client.binCheck).toBeDefined(); + expect(client.cancelRefund).toBeDefined(); + expect(client.cardStorage).toBeDefined(); + expect(client.reporting).toBeDefined(); + }); + + it('should return locale via getLocale()', () => { + const client = new IyzicoClient({ + apiKey, + secretKey, + locale: 'en', + }); + + expect(client.getLocale()).toBe('en'); + }); + + it('should return baseUrl via getBaseUrl()', () => { + const customUrl = 'https://api.custom.com'; + const client = new IyzicoClient({ + apiKey, + secretKey, + baseUrl: customUrl, + }); + + expect(client.getBaseUrl()).toBe(customUrl); + }); +}); + diff --git a/tests/unit/errors.test.ts b/tests/unit/errors.test.ts new file mode 100644 index 0000000..6fcbb5e --- /dev/null +++ b/tests/unit/errors.test.ts @@ -0,0 +1,76 @@ +/** + * Error classes unit tests + */ + +import { describe, it, expect } from 'vitest'; +import { + IyzicoError, + IyzicoRequestError, + IyzicoResponseError, +} from '../../src/errors'; +import type { ErrorResponse } from '../../src/types'; + +describe('IyzicoError', () => { + it('should create error with message', () => { + const error = new IyzicoError('Test error'); + expect(error.message).toBe('Test error'); + expect(error.name).toBe('IyzicoError'); + expect(error.code).toBeUndefined(); + expect(error.originalError).toBeUndefined(); + }); + + it('should create error with code and originalError', () => { + const originalError = new Error('Original error'); + const error = new IyzicoError('Test error', 'TEST_CODE', originalError); + expect(error.message).toBe('Test error'); + expect(error.code).toBe('TEST_CODE'); + expect(error.originalError).toBe(originalError); + }); +}); + +describe('IyzicoRequestError', () => { + it('should create request error', () => { + const error = new IyzicoRequestError('Request failed'); + expect(error.message).toBe('Request failed'); + expect(error.name).toBe('IyzicoRequestError'); + expect(error.code).toBe('REQUEST_ERROR'); + }); + + it('should create request error with originalError', () => { + const originalError = new Error('Validation error'); + const error = new IyzicoRequestError('Request failed', originalError); + expect(error.message).toBe('Request failed'); + expect(error.code).toBe('REQUEST_ERROR'); + expect(error.originalError).toBe(originalError); + }); +}); + +describe('IyzicoResponseError', () => { + it('should create response error', () => { + const errorResponse: ErrorResponse = { + status: 'failure', + errorCode: 'PAYMENT_FAILED', + errorMessage: 'Payment failed', + }; + const error = new IyzicoResponseError('Payment failed', errorResponse); + expect(error.message).toBe('Payment failed'); + expect(error.name).toBe('IyzicoResponseError'); + expect(error.code).toBe('PAYMENT_FAILED'); + expect(error.errorResponse).toEqual(errorResponse); + }); + + it('should create response error with originalError', () => { + const errorResponse: ErrorResponse = { + status: 'failure', + errorCode: 'PAYMENT_FAILED', + errorMessage: 'Payment failed', + }; + const originalError = new Error('Network error'); + const error = new IyzicoResponseError('Payment failed', errorResponse, originalError); + expect(error.message).toBe('Payment failed'); + expect(error.code).toBe('PAYMENT_FAILED'); + expect(error.errorResponse).toEqual(errorResponse); + expect(error.originalError).toBe(originalError); + }); +}); + diff --git a/tests/unit/http.test.ts b/tests/unit/http.test.ts new file mode 100644 index 0000000..97c88f7 --- /dev/null +++ b/tests/unit/http.test.ts @@ -0,0 +1,310 @@ +/** + * HTTP client unit tests + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { makeRequest } from '../../src/http'; +import { + IyzicoResponseError, + IyzicoRequestError, + IyzicoError, +} from '../../src/errors'; + +describe('makeRequest', () => { + const apiKey = 'test-api-key'; + const secretKey = 'test-secret-key'; + const baseUrl = 'https://api.test.com'; + + // Store original fetch to restore it later + const originalFetch = globalThis.fetch; + const mockFetch = vi.fn(); + + beforeEach(() => { + // Mock fetch before each test + globalThis.fetch = mockFetch as any; + mockFetch.mockClear(); + }); + + afterEach(() => { + // Restore original fetch after each test + globalThis.fetch = originalFetch; + vi.clearAllMocks(); + }); + + it('should make a successful POST request', async () => { + const mockResponse = { + status: 'success', + data: 'test data', + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + const result = await makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledTimes(1); + const callArgs = mockFetch.mock.calls[0]; + expect(callArgs[0]).toBe('https://api.test.com/test'); + expect(callArgs[1].method).toBe('POST'); + expect(callArgs[1].headers).toHaveProperty('Authorization'); + expect(callArgs[1].headers).toHaveProperty('Content-Type', 'application/json'); + expect(callArgs[1].headers).toHaveProperty('x-iyzi-rnd'); + }); + + it('should make a successful GET request without body', async () => { + const mockResponse = { + status: 'success', + data: 'test data', + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + const result = await makeRequest({ + method: 'GET', + path: '/test', + apiKey, + secretKey, + baseUrl, + }); + + expect(result).toEqual(mockResponse); + expect(mockFetch).toHaveBeenCalledTimes(1); + const callArgs = mockFetch.mock.calls[0]; + expect(callArgs[1].body).toBeUndefined(); + }); + + it('should throw IyzicoResponseError for failure responses', async () => { + const errorResponse = { + status: 'failure', + errorCode: 'TEST_ERROR', + errorMessage: 'Test error message', + }; + + mockFetch.mockResolvedValueOnce({ + ok: false, + json: async () => errorResponse, + }); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should throw IyzicoResponseError for failure status in response', async () => { + const errorResponse = { + status: 'failure', + errorCode: 'TEST_ERROR', + errorMessage: 'Test error message', + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => errorResponse, + }); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should use fallback message when errorMessage is missing', async () => { + const errorResponse = { + status: 'failure', + errorCode: 'TEST_ERROR', + // errorMessage is missing + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => errorResponse, + }); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should handle network errors with fetch in message', async () => { + const networkError = new TypeError('Failed to fetch'); + mockFetch.mockRejectedValueOnce(networkError); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoRequestError); + }); + + it('should rethrow IyzicoResponseError', async () => { + const errorResponse = { + status: 'failure', + errorCode: 'TEST_ERROR', + errorMessage: 'Test error', + } as const; + const responseError = new IyzicoResponseError('Test error', errorResponse); + mockFetch.mockRejectedValueOnce(responseError); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoResponseError); + }); + + it('should rethrow IyzicoRequestError', async () => { + const requestError = new IyzicoRequestError('Request failed'); + mockFetch.mockRejectedValueOnce(requestError); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoRequestError); + }); + + it('should rethrow IyzicoError', async () => { + const iyzicoError = new IyzicoError('Iyzico error'); + mockFetch.mockRejectedValueOnce(iyzicoError); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoError); + }); + + it('should wrap unknown errors in IyzicoError', async () => { + const unknownError = new Error('Unknown error'); + mockFetch.mockRejectedValueOnce(unknownError); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoError); + }); + + it('should wrap non-Error objects in IyzicoError', async () => { + mockFetch.mockRejectedValueOnce('String error'); + + await expect( + makeRequest({ + method: 'POST', + path: '/test', + body: { test: 'value' }, + apiKey, + secretKey, + baseUrl, + }), + ).rejects.toThrow(IyzicoError); + }); + + it('should handle DELETE request with body', async () => { + const mockResponse = { + status: 'success', + data: 'deleted', + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + const result = await makeRequest({ + method: 'DELETE', + path: '/test', + body: { id: '123' }, + apiKey, + secretKey, + baseUrl, + }); + + expect(result).toEqual(mockResponse); + const callArgs = mockFetch.mock.calls[0]; + expect(callArgs[1].method).toBe('DELETE'); + expect(callArgs[1].body).toBe(JSON.stringify({ id: '123' })); + }); + + it('should handle GET request with query string', async () => { + const mockResponse = { + status: 'success', + data: 'test data', + }; + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }); + + const result = await makeRequest({ + method: 'GET', + path: '/test?param=value', + apiKey, + secretKey, + baseUrl, + }); + + expect(result).toEqual(mockResponse); + const callArgs = mockFetch.mock.calls[0]; + expect(callArgs[0]).toBe('https://api.test.com/test?param=value'); + }); +}); diff --git a/tests/unit/index.test.ts b/tests/unit/index.test.ts new file mode 100644 index 0000000..8bf78d6 --- /dev/null +++ b/tests/unit/index.test.ts @@ -0,0 +1,40 @@ +/** + * Index exports test + * This test ensures all exports from index.ts are accessible + */ + +import { describe, it, expect } from 'vitest'; + +describe('Index exports', () => { + it('should export IyzicoClient', async () => { + const module = await import('../../src/index'); + expect(module.IyzicoClient).toBeDefined(); + expect(typeof module.IyzicoClient).toBe('function'); + }); + + it('should export all error classes', async () => { + const module = await import('../../src/index'); + expect(module.IyzicoError).toBeDefined(); + expect(module.IyzicoRequestError).toBeDefined(); + expect(module.IyzicoResponseError).toBeDefined(); + expect(typeof module.IyzicoError).toBe('function'); + expect(typeof module.IyzicoRequestError).toBe('function'); + expect(typeof module.IyzicoResponseError).toBe('function'); + }); + + it('should export utility functions', async () => { + const module = await import('../../src/index'); + expect(module.generateRandomNumber).toBeDefined(); + expect(module.generateRandomKey).toBeDefined(); + expect(typeof module.generateRandomNumber).toBe('function'); + expect(typeof module.generateRandomKey).toBe('function'); + }); + + it('should be importable', async () => { + // Just verify the module can be imported (this covers index.ts line 1) + const module = await import('../../src/index'); + expect(module).toBeDefined(); + expect(Object.keys(module).length).toBeGreaterThan(0); + }); +}); + diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts new file mode 100644 index 0000000..b336975 --- /dev/null +++ b/tests/unit/utils.test.ts @@ -0,0 +1,53 @@ +/** + * Utility functions unit tests + */ + +import { describe, it, expect } from 'vitest'; +import { generateRandomNumber, generateRandomKey } from '../../src/utils'; + +describe('generateRandomNumber', () => { + it('should generate a number within the specified range', () => { + const result = generateRandomNumber(1, 10); + expect(result).toBeGreaterThanOrEqual(1); + expect(result).toBeLessThanOrEqual(10); + }); + + it('should use default values when no arguments provided', () => { + const result = generateRandomNumber(); + expect(result).toBeGreaterThanOrEqual(0); + expect(result).toBeLessThanOrEqual(100); + }); + + it('should throw error when min is greater than max', () => { + expect(() => generateRandomNumber(10, 5)).toThrow('Min value cannot be greater than max value'); + }); + + it('should handle equal min and max', () => { + const result = generateRandomNumber(5, 5); + expect(result).toBe(5); + }); +}); + +describe('generateRandomKey', () => { + it('should generate a non-empty string', () => { + const result = generateRandomKey(); + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result.length).toBeGreaterThan(0); + }); + + it('should generate different keys on each call', () => { + const key1 = generateRandomKey(); + const key2 = generateRandomKey(); + expect(key1).not.toBe(key2); + }); + + it('should generate keys that start with timestamp', () => { + const result = generateRandomKey(); + const timestamp = Date.now(); + const keyTimestamp = parseInt(result.substring(0, 13)); + // Should be within 1 second of current timestamp + expect(Math.abs(timestamp - keyTimestamp)).toBeLessThan(1000); + }); +}); + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f6176c5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/tsdown.config.ts b/tsdown.config.ts new file mode 100644 index 0000000..3ab0f75 --- /dev/null +++ b/tsdown.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + treeshake: true, + minify: false, + target: 'es2022', + outDir: 'dist', +}); + diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8e5dbe1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + setupFiles: ['tests/setup.ts'], + testTimeout: 30000, // 30 seconds for integration tests + // Run tests sequentially to avoid rate limiting + pool: 'forks', + poolOptions: { + forks: { + singleFork: true, // Run all tests in a single process sequentially + }, + }, + // Alternative: use threads with maxConcurrency: 1 + // maxConcurrency: 1, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'dist/', + 'tests/', + '*.config.ts', + ], + }, + }, +}); +