From 4bb96fdf36f85f615924042b3e964e9ae795e271 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Mon, 1 Dec 2025 22:51:13 +0100 Subject: [PATCH 1/3] feat: implement Reports, Sessions, and enhanced Payment features Major features: - Add Report API for payment/fee report generation (CSV, PDF, XLSX) - Add Session API for POS integration with fund lock payments - Extend Payment API with meal voucher and fringe benefits parameters - Support euro-to-cent amount conversion in Payment.create() and Payment.update() - Accept Date objects in Payment.all() for starting_after_timestamp parameter API classes: - Report: create(), all(), get() for merchant-level reports - Session: open(), get(), update(), createEvent() for POS sessions - Enhanced Payment types with meal_voucher_max_amount_unit and meal_voucher_max_quantity TypeScript: - Add ReportType, ReportFormatType, ReportStatus types - Add SessionStatus, SessionEventType types - Extend PaymentCreateBody and PaymentUpdateBody with meal voucher support - Make amount/amount_unit mutually exclusive in payment types Testing: - Add 23 new unit tests for Report and Session classes (163 total passing) - Add E2E test suite for staging environment integration testing - Add setup helper functions for E2E test configuration - Improve test coverage with edge cases and error handling Documentation: - Update CHANGELOG.md with comprehensive feature list - Add examples for reports (reports.ts) and POS sessions (pos-session.ts) - Update README with Reports, Sessions, and Meal Voucher sections Fixes: - Change Environment type from 'test' to 'staging' for consistency - Fix Payment.all() return type from 'list' to 'data' (API compliance) - Add proper SSL verification disable for staging environment --- .env.example | 14 + .github/workflows/ci.yml | 86 ++ .github/workflows/release.yml | 61 ++ .gitignore | 2 +- CHANGELOG.md | 85 +- README.md | 175 +++- examples/create-payment-with-amount.ts | 96 +++ examples/create-payment.ts | 12 +- examples/get-payments.ts | 5 +- examples/payment-date-filtering.ts | 103 +++ examples/pos-session.ts | 152 ++++ examples/reports.ts | 101 +++ package.json | 25 +- pnpm-lock.yaml | 1060 ++++++------------------ src/Payment.ts | 87 +- src/Report.ts | 142 ++++ src/Request.ts | 6 +- src/Session.ts | 171 ++++ src/index.ts | 2 + src/types.ts | 127 ++- tests/Api.test.ts | 41 +- tests/Payment.test.ts | 124 ++- tests/Report.test.ts | 222 +++++ tests/Request.test.ts | 41 +- tests/Session.test.ts | 301 +++++++ tests/e2e/authentication.e2e.test.ts | 58 ++ tests/e2e/payment.e2e.test.ts | 230 +++++ tests/setup.ts | 63 ++ tests/utils.test.ts | 345 ++++++++ vitest.config.ts | 12 +- 30 files changed, 3069 insertions(+), 880 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 examples/create-payment-with-amount.ts create mode 100644 examples/payment-date-filtering.ts create mode 100644 examples/pos-session.ts create mode 100644 examples/reports.ts create mode 100644 src/Report.ts create mode 100644 src/Session.ts create mode 100644 tests/Report.test.ts create mode 100644 tests/Session.test.ts create mode 100644 tests/e2e/authentication.e2e.test.ts create mode 100644 tests/e2e/payment.e2e.test.ts create mode 100644 tests/setup.ts create mode 100644 tests/utils.test.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..75b718e --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Example .env file for local E2E tests +# Copy this file to .env.local and insert your real values + +# IMPORTANT: E2E tests require pre-configured keys +# The activation code must NOT be included because it can only be used once +# Generate the keys manually using: npx satispay-keygen + +# Satispay authentication keys (REQUIRED for E2E tests) +SATISPAY_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nyour_public_key_here\n-----END PUBLIC KEY-----" +SATISPAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nyour_private_key_here\n-----END PRIVATE KEY-----" +SATISPAY_KEY_ID=your_key_id_here + +# NOTE: The environment is forced to 'staging' for security +# It cannot be modified in E2E tests diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ac8e93b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,86 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test + env: + SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} + SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} + SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} + + - name: Test Coverage + run: pnpm test:coverage + env: + SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} + SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} + SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x' + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/coverage-final.json + flags: unittests + name: codecov-umbrella + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check formatting + run: pnpm format --check + + - name: Lint + run: pnpm lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..47a6a67 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test + env: + SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} + SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} + SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} + + - name: Test Coverage + run: pnpm test:coverage + env: + SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} + SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} + SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} + + - name: Publish to npm + run: pnpm publish --access public --no-git-checks + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + draft: false + prerelease: false diff --git a/.gitignore b/.gitignore index 15c4506..4812680 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ examples/authentication.json node_modules dist coverage -*.env.local \ No newline at end of file +*.local \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 37cbf6a..b41f327 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,67 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - -### Added -- Comprehensive test suite with 66 tests and 72% coverage -- GitHub Actions CI/CD workflows for automated testing and npm publishing -- Vitest test framework with UI and coverage reporting -- Support for multiple runtimes: Node.js 18+, Deno 1.30+, Bun 1.0+ - -### Changed -- Migrated build system from TypeScript compiler to Vite -- Improved TypeScript declaration generation with vite-plugin-dts -- Tests moved to dedicated `tests/` folder for better organization - -### Fixed -- TypeScript declaration files output to correct location in dist/ - -## [0.0.1] - TBD +## [0.0.1] - 2025-12-01 ### Added - Initial implementation of Satispay GBusiness Node.js SDK -- Api class for configuration and environment management -- ApiAuthentication for token-based authentication -- Payment operations (create, get, list, update) -- Consumer operations (get consumer details) -- DailyClosure operations (get daily closure reports) -- PreAuthorizedPaymentToken operations (create, get, list, accept, reject) -- Request class for HTTP operations with automatic signing -- RSA service for key generation and cryptographic operations -- Support for both Node.js crypto and Web Crypto API -- CLI tool `satispay-keygen` for generating RSA key pairs -- Comprehensive examples for common operations -- Zero runtime dependencies - -### API Methods - -#### Authentication -- `Api.authenticateWithToken(token: string)` - Generate keys and authenticate - -#### Configuration -- `Api.setEnv(env: Environment)` - Set environment (staging/production) -- `Api.setKeys(keyId: string, privateKey: string)` - Set authentication keys -- `Api.setPlatformHeader(value: string)` - Set platform identification header - -#### Payment Operations -- `Payment.create(options)` - Create a new payment -- `Payment.get(id: string)` - Get payment details -- `Payment.list(options)` - List payments with filters -- `Payment.update(id: string, options)` - Update payment metadata - -#### Consumer Operations -- `Consumer.get(id: string)` - Get consumer details - -#### Daily Closure Operations -- `DailyClosure.get(date: Date)` - Get daily closure report +- Zero runtime dependencies - uses only native APIs (fetch, crypto) +- Multi-runtime support: Node.js 18+, Deno 1.30+, Bun 1.0+ +- Complete TypeScript definitions with full type safety + +#### API Classes +- `Api` - Configuration and environment management +- `ApiAuthentication` - Token-based authentication and key generation +- `Payment` - Create, retrieve, list, and update payments (including meal vouchers and fringe benefits) +- `Consumer` - Retrieve consumer information +- `DailyClosure` - Get daily closure reports +- `PreAuthorizedPaymentToken` - Manage pre-authorized payment tokens +- `Report` - Generate and retrieve payment reports (CSV, PDF, XLSX) +- `Session` - POS integration for fund lock payments with incremental charging +- `Request` - HTTP operations with automatic RSA-SHA256 signing +- `RSAService` - Key generation and cryptographic operations + +#### Tools & Testing +- CLI tool `satispay-keygen` for RSA key pair generation +- Comprehensive test suite with 163 tests using Vitest +- E2E tests for integration testing with staging environment +- GitHub Actions CI/CD workflows for automated testing and npm publishing +- Vite-based build system with optimized TypeScript declaration generation -#### Pre-Authorized Payment Tokens -- `PreAuthorizedPaymentToken.create(options)` - Create token -- `PreAuthorizedPaymentToken.get(token: string)` - Get token details -- `PreAuthorizedPaymentToken.list()` - List tokens -- `PreAuthorizedPaymentToken.accept(token: string)` - Accept token -- `PreAuthorizedPaymentToken.reject(token: string)` - Reject token +#### Documentation & Examples +- Complete API documentation in README +- Example files for all major operations (payments, reports, sessions, webhooks, etc.) +- Runtime-specific examples for Node.js, Deno, and Bun -[Unreleased]: https://github.com/volverjs/satispay-node-sdk/compare/v0.0.1...HEAD [0.0.1]: https://github.com/volverjs/satispay-node-sdk/releases/tag/v0.0.1 diff --git a/README.md b/README.md index 545c4d5..ed50ff9 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Universal (but unofficial) TypeScript SDK for Satispay GBusiness API integration - **Type-safe** - Complete TypeScript definitions - **Modern** - Fetch API, async/await, ES Modules - **Secure** - Native RSA-SHA256 encryption +- **Developer-friendly** - Intuitive API with automatic conversions: + - šŸ’¶ Use `amount` (euros) instead of `amount_unit` (cents) + - šŸ“… Use `Date` objects instead of timestamp strings ## Installation @@ -102,7 +105,15 @@ SATISPAY_KEY_ID="your-key-id" ```typescript import { Payment } from '@volverjs/satispay-node-sdk'; +// Using amount in euros (recommended) const payment = await Payment.create({ + flow: 'MATCH_CODE', + amount: 1.99, // Amount in euros (automatically converted to cents) + currency: 'EUR', +}); + +// Or using amount_unit in cents (still supported) +const payment2 = await Payment.create({ flow: 'MATCH_CODE', amount_unit: 199, // Amount in cents (1.99 EUR) currency: 'EUR', @@ -119,9 +130,20 @@ console.log('Code:', payment.code_identifier); #### Create Payment ```typescript +// Using amount in euros (recommended) const payment = await Payment.create({ flow: 'MATCH_CODE', - amount_unit: 100, + amount: 1.00, // Automatically converted to 100 cents + currency: 'EUR', + callback_url: 'https://your-site.com/callback', + external_code: 'ORDER-123', + metadata: { order_id: '12345' }, +}); + +// Or using amount_unit in cents (still supported) +const payment2 = await Payment.create({ + flow: 'MATCH_CODE', + amount_unit: 100, // Amount in cents currency: 'EUR', callback_url: 'https://your-site.com/callback', external_code: 'ORDER-123', @@ -129,6 +151,10 @@ const payment = await Payment.create({ }); ``` +**šŸ’” Tip**: Use `amount` (euros) for more intuitive code. The SDK automatically converts it to cents. + +See [examples/create-payment-with-amount.ts](./examples/create-payment-with-amount.ts) for more examples. + #### Get Payment ```typescript @@ -145,16 +171,48 @@ const result = await Payment.all({ from_date: '2024-01-01', }); -result.list.forEach(payment => { +result.data.forEach(payment => { console.log(`${payment.id}: ${payment.status}`); }); ``` +##### Filter by Date + +You can filter payments using `Date` objects or timestamp strings: + +```typescript +// Using Date objects (recommended) +const yesterday = new Date(); +yesterday.setDate(yesterday.getDate() - 1); + +const recentPayments = await Payment.all({ + starting_after_timestamp: yesterday, // Date is automatically converted to milliseconds + limit: 10, +}); + +// Or using timestamp string (milliseconds) +const timestampString = new Date('2024-01-01').getTime().toString(); +const paymentsFromDate = await Payment.all({ + starting_after_timestamp: timestampString, + limit: 10, +}); +``` + +See [examples/payment-date-filtering.ts](./examples/payment-date-filtering.ts) for more examples. + #### Update Payment ```typescript +// Using amount in euros const payment = await Payment.update('PAYMENT_ID', { - metadata: { order_id: '67890' }, + action: 'ACCEPT', + amount: 5.50, // Automatically converted to 550 cents +}); + +// Or using amount_unit in cents +const payment2 = await Payment.update('PAYMENT_ID', { + action: 'ACCEPT', + amount_unit: 550, }); ``` @@ -203,6 +261,117 @@ const updatedToken = await PreAuthorizedPaymentToken.update(token.id, { }); ``` +### Reports + +> **āš ļø Special Authentication Required**: Report APIs require special authentication keys. Contact tech@satispay.com to enable access. + +```typescript +import { Report } from '@volverjs/satispay-node-sdk'; + +// Create a new report +const report = await Report.create({ + type: 'PAYMENT_FEE', + format: 'CSV', // or 'PDF', 'XLSX' + from_date: '2025-11-01', + to_date: '2025-11-30', + columns: ['transaction_id', 'transaction_date', 'total_amount'], // Optional +}); + +// Get list of reports +const reports = await Report.all({ + limit: 10, + starting_after: 'report-123', +}); + +// Get specific report +const reportDetails = await Report.get('report-123'); + +if (reportDetails.status === 'READY' && reportDetails.download_url) { + console.log('Download URL:', reportDetails.download_url); +} +``` + +**Important Notes:** +- Reports are extracted at merchant level (includes all shops) +- Reports for the previous day should be generated at least 4 hours after midnight +- Report status: `PENDING`, `READY`, or `FAILED` + +### Sessions (POS Integration) + +Sessions are used for POS/device integration to manage fund lock payments incrementally: + +```typescript +import { Session } from '@volverjs/satispay-node-sdk'; + +// Open a session from a fund lock +const session = await Session.open({ + fund_lock_id: 'payment-fund-lock-123', +}); + +console.log('Session ID:', session.id); +console.log('Available amount:', session.residual_amount_unit); + +// Add items to the session +await Session.createEvent(session.id, { + type: 'ADD_ITEM', + amount_unit: 500, + description: 'Coffee', + metadata: { sku: 'COFFEE-001' }, +}); + +// Remove items +await Session.createEvent(session.id, { + type: 'REMOVE_ITEM', + amount_unit: 200, + description: 'Discount', +}); + +// Update total +await Session.createEvent(session.id, { + type: 'UPDATE_TOTAL', + amount_unit: 300, +}); + +// Get session details +const details = await Session.get(session.id); +console.log('Residual amount:', details.residual_amount_unit); + +// Close the session +const closedSession = await Session.update(session.id, { + status: 'CLOSE', +}); +``` + +### Meal Voucher & Fringe Benefits + +Meal Voucher and Fringe Benefits payments use the same `Payment` API with additional parameters: + +```typescript +import { Payment } from '@volverjs/satispay-node-sdk'; + +// Create payment with Meal Voucher limits +const payment = await Payment.create({ + flow: 'MATCH_CODE', + amount: 50.00, + currency: 'EUR', + meal_voucher_max_amount_unit: 4000, // Max 40 EUR with meal vouchers + meal_voucher_max_quantity: 8, // Max 8 vouchers +}); + +// Update payment with Meal Voucher limits +const updated = await Payment.update(payment.id, { + action: 'ACCEPT', + meal_voucher_max_amount_unit: 3000, + meal_voucher_max_quantity: 6, +}); +``` + +**Important Notes:** +- Meal Vouchers and Fringe Benefits are mutually exclusive +- Meal Voucher refunds: Only on the same day, full amount only +- Fringe Benefits refunds: Only within the same month, full amount only +- Default limit: 8 meal vouchers per payment if not specified + ## Runtime-Specific Examples ### Node.js Server diff --git a/examples/create-payment-with-amount.ts b/examples/create-payment-with-amount.ts new file mode 100644 index 0000000..124943d --- /dev/null +++ b/examples/create-payment-with-amount.ts @@ -0,0 +1,96 @@ +import { Api, Payment } from '../src'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Example: Create payment using amount in euros + * + * This example demonstrates how to create payments using the 'amount' field + * which accepts floating-point numbers in euros instead of cents. + */ + +async function main() { + try { + // Enable sandbox mode + Api.setSandbox(true); + + // Load authentication keys + const authFilePath = path.join(__dirname, 'authentication.json'); + const authData = JSON.parse(fs.readFileSync(authFilePath, 'utf-8')); + + Api.setPublicKey(authData.public_key); + Api.setPrivateKey(authData.private_key); + Api.setKeyId(authData.key_id); + + console.log('Creating payments using amount field...\n'); + + // Example 1: Simple payment with amount in euros + console.log('1. Creating payment with 10.50 EUR:'); + const payment1 = await Payment.create({ + flow: 'MATCH_CODE', + amount: 10.50, // Automatically converted to 1050 cents + currency: 'EUR', + external_code: `EXAMPLE-${Date.now()}-1`, + }); + + console.log(` Payment ID: ${payment1.id}`); + console.log(` Code: ${payment1.code_identifier}`); + console.log(` Amount: ${payment1.amount_unit / 100} EUR (${payment1.amount_unit} cents)`); + console.log(` Status: ${payment1.status}\n`); + + // Example 2: Payment with decimal precision + console.log('2. Creating payment with 99.99 EUR:'); + const payment2 = await Payment.create({ + flow: 'MATCH_CODE', + amount: 99.99, // Handles decimal precision correctly + currency: 'EUR', + external_code: `EXAMPLE-${Date.now()}-2`, + metadata: { + description: 'Premium product', + category: 'electronics', + }, + }); + + console.log(` Payment ID: ${payment2.id}`); + console.log(` Code: ${payment2.code_identifier}`); + console.log(` Amount: ${payment2.amount_unit / 100} EUR (${payment2.amount_unit} cents)`); + console.log(` Status: ${payment2.status}\n`); + + // Example 3: Small amount (0.50 EUR) + console.log('3. Creating payment with 0.50 EUR:'); + const payment3 = await Payment.create({ + flow: 'MATCH_CODE', + amount: 0.50, // 50 cents + currency: 'EUR', + external_code: `EXAMPLE-${Date.now()}-3`, + }); + + console.log(` Payment ID: ${payment3.id}`); + console.log(` Code: ${payment3.code_identifier}`); + console.log(` Amount: ${payment3.amount_unit / 100} EUR (${payment3.amount_unit} cents)`); + console.log(` Status: ${payment3.status}\n`); + + // Example 4: Comparing with amount_unit (old way) + console.log('4. Using amount_unit (cents) - still supported:'); + const payment4 = await Payment.create({ + flow: 'MATCH_CODE', + amount_unit: 2500, // 25.00 EUR in cents + currency: 'EUR', + external_code: `EXAMPLE-${Date.now()}-4`, + }); + + console.log(` Payment ID: ${payment4.id}`); + console.log(` Code: ${payment4.code_identifier}`); + console.log(` Amount: ${payment4.amount_unit / 100} EUR (${payment4.amount_unit} cents)`); + console.log(` Status: ${payment4.status}\n`); + + console.log('āœ“ All payments created successfully!'); + console.log('\nšŸ’” Tip: Using "amount" (euros) is more intuitive than "amount_unit" (cents)'); + + } catch (error) { + console.error('Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +main(); diff --git a/examples/create-payment.ts b/examples/create-payment.ts index c687637..092c629 100644 --- a/examples/create-payment.ts +++ b/examples/create-payment.ts @@ -30,16 +30,24 @@ async function main() { console.log('Creating payment...'); - // Create a payment + // Create a payment using amount in euros (recommended) const payment = await Payment.create({ flow: 'MATCH_CODE', - amount_unit: 199, // Amount in cents (1.99 EUR) + amount: 1.99, // Amount in euros (automatically converted to cents) currency: 'EUR', }); + // Or using amount_unit in cents (still supported) + // const payment = await Payment.create({ + // flow: 'MATCH_CODE', + // amount_unit: 199, // Amount in cents (1.99 EUR) + // currency: 'EUR', + // }); + console.log('Payment created successfully!'); console.log('Payment ID:', payment.id); console.log('Payment Code:', payment.code_identifier); + console.log('Amount:', payment.amount_unit / 100, 'EUR'); console.log('Status:', payment.status); console.log('\nFull payment object:'); console.log(JSON.stringify(payment, null, 2)); diff --git a/examples/get-payments.ts b/examples/get-payments.ts index 95302a4..2a76ca2 100644 --- a/examples/get-payments.ts +++ b/examples/get-payments.ts @@ -28,12 +28,13 @@ async function main() { limit: 10, // status: 'ACCEPTED', // Optional: filter by status // from_date: '2024-01-01', // Optional: filter by date + // starting_after_timestamp: new Date('2024-01-01'), // Optional: filter by timestamp using Date object }); - console.log(`Found ${result.list.length} payments`); + console.log(`Found ${result.data.length} payments`); console.log('Has more:', result.has_more); console.log('\nPayments:'); - result.list.forEach((payment, index) => { + result.data.forEach((payment, index) => { console.log(`\n${index + 1}. Payment ${payment.id}`); console.log(` Status: ${payment.status}`); console.log(` Amount: ${payment.amount_unit / 100} ${payment.currency}`); diff --git a/examples/payment-date-filtering.ts b/examples/payment-date-filtering.ts new file mode 100644 index 0000000..2e2e473 --- /dev/null +++ b/examples/payment-date-filtering.ts @@ -0,0 +1,103 @@ +import { Api, Payment } from '../src'; +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Example: Filter payments by date + * + * This example demonstrates how to filter payments using Date objects + * with the starting_after_timestamp parameter. + */ + +async function main() { + try { + // Enable sandbox mode + Api.setSandbox(true); + + // Load authentication keys + const authFilePath = path.join(__dirname, 'authentication.json'); + const authData = JSON.parse(fs.readFileSync(authFilePath, 'utf-8')); + + Api.setPublicKey(authData.public_key); + Api.setPrivateKey(authData.private_key); + Api.setKeyId(authData.key_id); + + console.log('Filtering payments by date...\n'); + + // Example 1: Payments from yesterday + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + console.log('1. Payments from yesterday:'); + console.log(` Filter date: ${yesterday.toISOString()}`); + + const paymentsFromYesterday = await Payment.all({ + starting_after_timestamp: yesterday, // Date object is automatically converted + limit: 10, + }); + + console.log(` Found ${paymentsFromYesterday.data.length} payments`); + console.log(` Has more: ${paymentsFromYesterday.has_more}\n`); + + // Example 2: Payments from last week + const lastWeek = new Date(); + lastWeek.setDate(lastWeek.getDate() - 7); + + console.log('2. Payments from last week:'); + console.log(` Filter date: ${lastWeek.toISOString()}`); + + const paymentsFromLastWeek = await Payment.all({ + starting_after_timestamp: lastWeek, + limit: 20, + }); + + console.log(` Found ${paymentsFromLastWeek.data.length} payments`); + console.log(` Has more: ${paymentsFromLastWeek.has_more}\n`); + + // Example 3: Payments from a specific date + const specificDate = new Date('2024-01-01T00:00:00Z'); + + console.log('3. Payments from a specific date:'); + console.log(` Filter date: ${specificDate.toISOString()}`); + + const paymentsFromDate = await Payment.all({ + starting_after_timestamp: specificDate, + limit: 5, + }); + + console.log(` Found ${paymentsFromDate.data.length} payments`); + console.log(` Has more: ${paymentsFromDate.has_more}\n`); + + // Example 4: You can still use timestamp strings if needed + const timestampString = new Date('2024-06-01').getTime().toString(); + + console.log('4. Using timestamp string:'); + console.log(` Timestamp: ${timestampString}`); + + const paymentsWithString = await Payment.all({ + starting_after_timestamp: timestampString, + limit: 10, + }); + + console.log(` Found ${paymentsWithString.data.length} payments`); + console.log(` Has more: ${paymentsWithString.has_more}\n`); + + // Example 5: Combine with other filters + console.log('5. Date filter with status filter:'); + + const combinedFilters = await Payment.all({ + starting_after_timestamp: yesterday, + status: 'ACCEPTED', + limit: 10, + }); + + console.log(` Found ${combinedFilters.data.length} accepted payments from yesterday`); + console.log(` Has more: ${combinedFilters.has_more}`); + + } catch (error) { + console.error('Error:', error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} + +main(); diff --git a/examples/pos-session.ts b/examples/pos-session.ts new file mode 100644 index 0000000..cce4973 --- /dev/null +++ b/examples/pos-session.ts @@ -0,0 +1,152 @@ +import { Api, Payment, Session } from '../src/index.js' + +/** + * Example: POS Session Management + * + * Sessions allow managing fund lock payments incrementally, + * useful for POS/device integrations where items are added over time. + */ + +// Configure API +Api.setSandbox(true) +Api.setPublicKey(process.env.SATISPAY_PUBLIC_KEY || '') +Api.setPrivateKey(process.env.SATISPAY_PRIVATE_KEY || '') +Api.setKeyId(process.env.SATISPAY_KEY_ID || '') + +async function main() { + try { + console.log('šŸŖ POS Session Example\n') + + // Step 1: Create a fund lock payment + console.log('1ļøāƒ£ Creating fund lock payment...\n') + const fundLock = await Payment.create({ + flow: 'FUND_LOCK', + amount: 100.00, // Lock 100 EUR + currency: 'EUR', + external_code: `POS-SESSION-${Date.now()}`, + }) + + console.log('āœ… Fund lock created!') + console.log('Payment ID:', fundLock.id) + console.log('Amount locked:', fundLock.amount_unit / 100, 'EUR') + console.log('Status:', fundLock.status) + console.log('Code:', fundLock.code_identifier) + console.log('\nšŸ’” Customer needs to authorize this fund lock in their Satispay app.\n') + + // In a real scenario, wait for customer authorization via webhook or polling + console.log('ā³ Waiting for customer authorization...') + console.log(' (In production, use webhooks or poll Payment.get())\n') + + // For this example, we'll simulate that the payment is not yet authorized + // In production, you would check the status until it becomes 'AUTHORIZED' + const currentStatus = await Payment.get(fundLock.id) + + if (currentStatus.status !== 'AUTHORIZED') { + console.log('āš ļø Payment not authorized yet.') + console.log(' Status:', currentStatus.status) + console.log('\n To continue this example:') + console.log(' 1. Authorize the payment in Satispay app') + console.log(' 2. Wait for status to become AUTHORIZED') + console.log(' 3. Then run the session operations\n') + + console.log(' For this demo, we\'ll show how sessions work once authorized:\n') + } + + // Step 2: Open a session (only works if fund lock is AUTHORIZED) + console.log('2ļøāƒ£ Opening POS session...\n') + console.log(' (This will fail until payment is AUTHORIZED)\n') + + try { + const session = await Session.open({ + fund_lock_id: fundLock.id, + }) + + console.log('āœ… Session opened!') + console.log('Session ID:', session.id) + console.log('Total amount:', session.amount_unit / 100, 'EUR') + console.log('Residual amount:', session.residual_amount_unit / 100, 'EUR') + console.log('Status:', session.status) + + // Step 3: Add items to the session + console.log('\n3ļøāƒ£ Adding items to session...\n') + + // Add coffee + await Session.createEvent(session.id, { + type: 'ADD_ITEM', + amount_unit: 300, // 3.00 EUR + description: 'Espresso', + metadata: { sku: 'COFFEE-001', category: 'beverages' }, + }) + console.log('āœ… Added: Espresso (3.00 EUR)') + + // Add croissant + await Session.createEvent(session.id, { + type: 'ADD_ITEM', + amount_unit: 250, // 2.50 EUR + description: 'Croissant', + metadata: { sku: 'PASTRY-042', category: 'food' }, + }) + console.log('āœ… Added: Croissant (2.50 EUR)') + + // Add water + await Session.createEvent(session.id, { + type: 'ADD_ITEM', + amount_unit: 150, // 1.50 EUR + description: 'Water', + metadata: { sku: 'DRINK-010', category: 'beverages' }, + }) + console.log('āœ… Added: Water (1.50 EUR)') + + // Apply discount + console.log('\n4ļøāƒ£ Applying discount...\n') + await Session.createEvent(session.id, { + type: 'REMOVE_ITEM', + amount_unit: 100, // -1.00 EUR discount + description: 'Happy Hour Discount', + }) + console.log('āœ… Applied discount: -1.00 EUR') + + // Check session status + console.log('\n5ļøāƒ£ Checking session status...\n') + const sessionDetails = await Session.get(session.id) + console.log('Total amount:', sessionDetails.amount_unit / 100, 'EUR') + console.log('Amount charged:', (sessionDetails.amount_unit - sessionDetails.residual_amount_unit) / 100, 'EUR') + console.log('Residual available:', sessionDetails.residual_amount_unit / 100, 'EUR') + + // Close the session + console.log('\n6ļøāƒ£ Closing session...\n') + const closedSession = await Session.update(session.id, { + status: 'CLOSE', + }) + + console.log('āœ… Session closed!') + console.log('Final status:', closedSession.status) + console.log('Final amount charged:', (closedSession.amount_unit - closedSession.residual_amount_unit) / 100, 'EUR') + console.log('\nšŸŽ‰ Transaction complete!') + + } catch (sessionError) { + if (sessionError instanceof Error) { + console.log('āš ļø Could not open session (expected if payment not authorized)') + console.log(' Error:', sessionError.message) + console.log('\nšŸ’” Session Workflow:') + console.log(' 1. Create FUND_LOCK payment') + console.log(' 2. Customer authorizes in Satispay app') + console.log(' 3. Payment status becomes AUTHORIZED') + console.log(' 4. Open session with fund_lock_id') + console.log(' 5. Add/remove items with createEvent()') + console.log(' 6. Close session to finalize charge') + } + } + + } catch (error) { + console.error('āŒ Error:', error) + + if (error instanceof Error) { + console.error('Message:', error.message) + } + + process.exit(1) + } +} + +main() diff --git a/examples/reports.ts b/examples/reports.ts new file mode 100644 index 0000000..987e759 --- /dev/null +++ b/examples/reports.ts @@ -0,0 +1,101 @@ +import { Api, Report } from '../src/index.js' + +/** + * Example: Create and manage reports + * + * āš ļø IMPORTANT: Report APIs require special authentication keys. + * Contact tech@satispay.com to enable report access for your account. + */ + +// Configure API +Api.setSandbox(true) +Api.setPublicKey(process.env.SATISPAY_PUBLIC_KEY || '') +Api.setPrivateKey(process.env.SATISPAY_PRIVATE_KEY || '') +Api.setKeyId(process.env.SATISPAY_KEY_ID || '') + +async function main() { + try { + console.log('šŸ“Š Creating a new report...\n') + + // Create a new report for the last month + const today = new Date() + const lastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1) + const lastMonthEnd = new Date(today.getFullYear(), today.getMonth(), 0) + + const report = await Report.create({ + type: 'PAYMENT_FEE', + format: 'CSV', + from_date: lastMonth.toISOString().split('T')[0], + to_date: lastMonthEnd.toISOString().split('T')[0], + columns: [ + 'transaction_id', + 'transaction_date', + 'total_amount', + 'fee_amount', + 'transaction_type', + 'external_code', + ], + }) + + console.log('āœ… Report created successfully!') + console.log('Report ID:', report.id) + console.log('Status:', report.status) + console.log('Format:', report.format) + console.log('Period:', `${report.from_date} to ${report.to_date}\n`) + + // Poll for report completion + console.log('ā³ Waiting for report to be ready...\n') + let reportDetails = report + let attempts = 0 + const maxAttempts = 30 + + while (reportDetails.status === 'PENDING' && attempts < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, 5000)) // Wait 5 seconds + reportDetails = await Report.get(report.id) + attempts++ + console.log(`Status check ${attempts}/${maxAttempts}: ${reportDetails.status}`) + } + + if (reportDetails.status === 'READY') { + console.log('\nāœ… Report is ready!') + console.log('Download URL:', reportDetails.download_url) + console.log('\nšŸ’” The download URL is pre-signed and will expire.') + } else if (reportDetails.status === 'FAILED') { + console.log('\nāŒ Report generation failed.') + } else { + console.log('\nā±ļø Report is still pending after maximum attempts.') + } + + // List all reports + console.log('\nšŸ“‹ Fetching list of reports...\n') + const reportsList = await Report.all({ limit: 10 }) + console.log(`Found ${reportsList.list.length} reports:`) + reportsList.list.forEach((r, index) => { + console.log(`${index + 1}. ID: ${r.id}`) + console.log(` Status: ${r.status}`) + console.log(` Period: ${r.from_date} to ${r.to_date}`) + console.log(` Created: ${r.created_at}\n`) + }) + + if (reportsList.has_more) { + console.log('šŸ’” More reports available. Use pagination to fetch them.') + } + } catch (error) { + console.error('āŒ Error:', error) + + if (error instanceof Error) { + console.error('Message:', error.message) + + // Special authentication error handling + if (error.message.includes('401') || error.message.includes('403')) { + console.error('\nāš ļø Authentication Error:') + console.error('Report APIs require special authentication keys.') + console.error('Contact tech@satispay.com to enable report access.') + } + } + + process.exit(1) + } +} + +main() diff --git a/package.json b/package.json index 25f97ba..7bb5021 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,8 @@ "test:watch": "vitest", "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", + "test:e2e": "vitest run tests/e2e", + "test:e2e:watch": "vitest tests/e2e", "lint": "eslint src", "lint:fix": "eslint src --fix", "format": "prettier --write \"src/**/*.ts\"" @@ -58,17 +60,18 @@ "node": ">=18.0.0" }, "devDependencies": { - "@eslint/js": "^9.17.0", - "@types/node": "^22.10.2", - "@vitest/ui": "^2.1.8", - "@vitest/coverage-v8": "^2.1.8", - "eslint": "^9.17.0", + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@vitest/coverage-v8": "^4.0.14", + "@vitest/ui": "^4.0.14", + "dotenv": "^17.2.3", + "eslint": "^9.39.1", "eslint-plugin-vitest": "^0.5.4", - "prettier": "^3.4.2", - "typescript": "^5.7.2", - "typescript-eslint": "^8.19.1", - "vite": "^6.0.3", - "vite-plugin-dts": "^4.3.0", - "vitest": "^2.1.8" + "prettier": "^3.7.3", + "typescript": "^5.9.3", + "typescript-eslint": "^8.48.0", + "vite": "^7.2.6", + "vite-plugin-dts": "^4.5.4", + "vitest": "^4.0.14" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 21408f0..d866d4b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,48 +9,47 @@ importers: .: devDependencies: '@eslint/js': - specifier: ^9.17.0 + specifier: ^9.39.1 version: 9.39.1 '@types/node': - specifier: ^22.10.2 - version: 22.19.1 + specifier: ^24.10.1 + version: 24.10.1 '@vitest/coverage-v8': - specifier: ^2.1.8 - version: 2.1.9(vitest@2.1.9) + specifier: ^4.0.14 + version: 4.0.14(vitest@4.0.14) '@vitest/ui': - specifier: ^2.1.8 - version: 2.1.9(vitest@2.1.9) + specifier: ^4.0.14 + version: 4.0.14(vitest@4.0.14) + dotenv: + specifier: ^17.2.3 + version: 17.2.3 eslint: - specifier: ^9.17.0 + specifier: ^9.39.1 version: 9.39.1 eslint-plugin-vitest: specifier: ^0.5.4 - version: 0.5.4(eslint@9.39.1)(typescript@5.9.3)(vitest@2.1.9) + version: 0.5.4(eslint@9.39.1)(typescript@5.9.3)(vitest@4.0.14) prettier: - specifier: ^3.4.2 - version: 3.6.2 + specifier: ^3.7.3 + version: 3.7.3 typescript: - specifier: ^5.7.2 + specifier: ^5.9.3 version: 5.9.3 typescript-eslint: - specifier: ^8.19.1 - version: 8.47.0(eslint@9.39.1)(typescript@5.9.3) + specifier: ^8.48.0 + version: 8.48.0(eslint@9.39.1)(typescript@5.9.3) vite: - specifier: ^6.0.3 - version: 6.4.1(@types/node@22.19.1) + specifier: ^7.2.6 + version: 7.2.6(@types/node@24.10.1) vite-plugin-dts: - specifier: ^4.3.0 - version: 4.5.4(@types/node@22.19.1)(rollup@4.53.2)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.1)) + specifier: ^4.5.4 + version: 4.5.4(@types/node@24.10.1)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.6(@types/node@24.10.1)) vitest: - specifier: ^2.1.8 - version: 2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9) + specifier: ^4.0.14 + version: 4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14) packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -68,14 +67,9 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} @@ -83,192 +77,96 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -281,12 +179,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -299,12 +191,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -317,48 +203,24 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -427,17 +289,6 @@ packages: resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} engines: {node: 20 || >=22} - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -473,10 +324,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -629,35 +476,44 @@ packages: '@rushstack/ts-command-line@5.1.3': resolution: {integrity: sha512-Kdv0k/BnnxIYFlMVC1IxrIS0oGQd4T4b7vKfx52Y2+wk2WZSDFIvedr7JrhenzSlm3ou5KwtoTGTGd5nbODRug==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@types/argparse@1.0.38': resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.19.1': - resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} - '@typescript-eslint/eslint-plugin@8.47.0': - resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} + '@typescript-eslint/eslint-plugin@8.48.0': + resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.47.0 + '@typescript-eslint/parser': ^8.48.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.47.0': - resolution: {integrity: sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==} + '@typescript-eslint/parser@8.48.0': + resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.47.0': - resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==} + '@typescript-eslint/project-service@8.48.0': + resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -666,18 +522,18 @@ packages: resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/scope-manager@8.47.0': - resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} + '@typescript-eslint/scope-manager@8.48.0': + resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.47.0': - resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==} + '@typescript-eslint/tsconfig-utils@8.48.0': + resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.47.0': - resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==} + '@typescript-eslint/type-utils@8.48.0': + resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -687,8 +543,8 @@ packages: resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/types@8.47.0': - resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==} + '@typescript-eslint/types@8.48.0': + resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@7.18.0': @@ -700,8 +556,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.47.0': - resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==} + '@typescript-eslint/typescript-estree@8.48.0': + resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -712,8 +568,8 @@ packages: peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/utils@8.47.0': - resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==} + '@typescript-eslint/utils@8.48.0': + resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -723,52 +579,52 @@ packages: resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/visitor-keys@8.47.0': - resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} + '@typescript-eslint/visitor-keys@8.48.0': + resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/coverage-v8@2.1.9': - resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + '@vitest/coverage-v8@4.0.14': + resolution: {integrity: sha512-EYHLqN/BY6b47qHH7gtMxAg++saoGmsjWmAq9MlXxAz4M0NcHh9iOyKhBZyU4yxZqOd8Xnqp80/5saeitz4Cng==} peerDependencies: - '@vitest/browser': 2.1.9 - vitest: 2.1.9 + '@vitest/browser': 4.0.14 + vitest: 4.0.14 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@2.1.9': - resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.0.14': + resolution: {integrity: sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==} - '@vitest/mocker@2.1.9': - resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + '@vitest/mocker@4.0.14': + resolution: {integrity: sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==} peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 + vite: ^6.0.0 || ^7.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@2.1.9': - resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.0.14': + resolution: {integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==} - '@vitest/runner@2.1.9': - resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.0.14': + resolution: {integrity: sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==} - '@vitest/snapshot@2.1.9': - resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.0.14': + resolution: {integrity: sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==} - '@vitest/spy@2.1.9': - resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.0.14': + resolution: {integrity: sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==} - '@vitest/ui@2.1.9': - resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==} + '@vitest/ui@4.0.14': + resolution: {integrity: sha512-fvDz8o7SQpFLoSBo6Cudv+fE85/fPCkwTnLAN85M+Jv7k59w2mSIjT9Q5px7XwGrmYqqKBEYxh/09IBGd1E7AQ==} peerDependencies: - vitest: 2.1.9 + vitest: 4.0.14 - '@vitest/utils@2.1.9': - resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.0.14': + resolution: {integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==} '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -837,22 +693,10 @@ packages: alien-signals@0.4.14: resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -867,6 +711,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@0.3.8: + resolution: {integrity: sha512-szgSZqUxI5T8mLKvS7WTjF9is+MVbOeLADU73IseOcrqhxr/VAvy6wfoVE39KnKzA7JRhjF5eUagNlHwvZPlKQ==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -880,26 +727,18 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} engines: {node: '>=18'} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - check-error@2.1.1: - resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} - engines: {node: '>= 16'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -935,10 +774,6 @@ packages: supports-color: optional: true - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -950,14 +785,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} @@ -966,11 +796,6 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -1095,10 +920,6 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - fs-extra@11.3.2: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} @@ -1119,10 +940,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1180,10 +997,6 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1211,12 +1024,12 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -1260,12 +1073,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1273,8 +1080,8 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + magicast@0.5.1: + resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} @@ -1299,10 +1106,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -1324,6 +1127,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1336,9 +1142,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1357,24 +1160,13 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1400,8 +1192,8 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.3: + resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} engines: {node: '>=14'} hasBin: true @@ -1461,10 +1253,6 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -1494,22 +1282,6 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1526,10 +1298,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - test-exclude@7.0.1: - resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} - engines: {node: '>=18'} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1540,16 +1308,8 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@1.2.0: - resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} - engines: {node: '>=14.0.0'} - - tinyspy@3.0.2: - resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: @@ -1576,8 +1336,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.47.0: - resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==} + typescript-eslint@8.48.0: + resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1596,8 +1356,8 @@ packages: ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} @@ -1606,11 +1366,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite-node@2.1.9: - resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - vite-plugin-dts@4.5.4: resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} peerDependencies: @@ -1620,50 +1375,19 @@ packages: vite: optional: true - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@7.2.6: + resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vite@6.4.1: - resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@types/node': ^20.19.0 || >=22.12.0 jiti: '>=1.21.0' - less: '*' + less: ^4.0.0 lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' + 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 @@ -1691,23 +1415,32 @@ packages: yaml: optional: true - vitest@2.1.9: - resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@4.0.14: + resolution: {integrity: sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.9 - '@vitest/ui': 2.1.9 + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.14 + '@vitest/browser-preview': 4.0.14 + '@vitest/browser-webdriverio': 4.0.14 + '@vitest/ui': 4.0.14 happy-dom: '*' jsdom: '*' peerDependenciesMeta: '@edge-runtime/vm': optional: true + '@opentelemetry/api': + optional: true '@types/node': optional: true - '@vitest/browser': + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': optional: true '@vitest/ui': optional: true @@ -1733,14 +1466,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -1750,11 +1475,6 @@ packages: snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -1768,152 +1488,83 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@bcoe/v8-coverage@0.2.3': {} - - '@esbuild/aix-ppc64@0.21.5': - optional: true + '@bcoe/v8-coverage@1.0.2': {} '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -1980,22 +1631,6 @@ snapshots: dependencies: '@isaacs/balanced-match': 4.0.1 - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2005,23 +1640,23 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@microsoft/api-extractor-model@7.32.0(@types/node@22.19.1)': + '@microsoft/api-extractor-model@7.32.0(@types/node@24.10.1)': dependencies: '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.0 - '@rushstack/node-core-library': 5.18.0(@types/node@22.19.1) + '@rushstack/node-core-library': 5.18.0(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.55.0(@types/node@22.19.1)': + '@microsoft/api-extractor@7.55.0(@types/node@24.10.1)': dependencies: - '@microsoft/api-extractor-model': 7.32.0(@types/node@22.19.1) + '@microsoft/api-extractor-model': 7.32.0(@types/node@24.10.1) '@microsoft/tsdoc': 0.16.0 '@microsoft/tsdoc-config': 0.18.0 - '@rushstack/node-core-library': 5.18.0(@types/node@22.19.1) + '@rushstack/node-core-library': 5.18.0(@types/node@24.10.1) '@rushstack/rig-package': 0.6.0 - '@rushstack/terminal': 0.19.3(@types/node@22.19.1) - '@rushstack/ts-command-line': 5.1.3(@types/node@22.19.1) + '@rushstack/terminal': 0.19.3(@types/node@24.10.1) + '@rushstack/ts-command-line': 5.1.3(@types/node@24.10.1) diff: 8.0.2 lodash: 4.17.21 minimatch: 10.0.3 @@ -2053,9 +1688,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@pkgjs/parseargs@0.11.0': - optional: true - '@polka/url@1.0.0-next.29': {} '@rollup/pluginutils@5.3.0(rollup@4.53.2)': @@ -2132,7 +1764,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.2': optional: true - '@rushstack/node-core-library@5.18.0(@types/node@22.19.1)': + '@rushstack/node-core-library@5.18.0(@types/node@24.10.1)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -2143,52 +1775,61 @@ snapshots: resolve: 1.22.11 semver: 7.5.4 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@rushstack/problem-matcher@0.1.1(@types/node@22.19.1)': + '@rushstack/problem-matcher@0.1.1(@types/node@24.10.1)': optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 '@rushstack/rig-package@0.6.0': dependencies: resolve: 1.22.11 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.19.3(@types/node@22.19.1)': + '@rushstack/terminal@0.19.3(@types/node@24.10.1)': dependencies: - '@rushstack/node-core-library': 5.18.0(@types/node@22.19.1) - '@rushstack/problem-matcher': 0.1.1(@types/node@22.19.1) + '@rushstack/node-core-library': 5.18.0(@types/node@24.10.1) + '@rushstack/problem-matcher': 0.1.1(@types/node@24.10.1) supports-color: 8.1.1 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 - '@rushstack/ts-command-line@5.1.3(@types/node@22.19.1)': + '@rushstack/ts-command-line@5.1.3(@types/node@24.10.1)': dependencies: - '@rushstack/terminal': 0.19.3(@types/node@22.19.1) + '@rushstack/terminal': 0.19.3(@types/node@24.10.1) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 transitivePeerDependencies: - '@types/node' + '@standard-schema/spec@1.0.0': {} + '@types/argparse@1.0.38': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} - '@types/node@22.19.1': + '@types/node@24.10.1': dependencies: - undici-types: 6.21.0 + undici-types: 7.16.0 - '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.47.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.0 eslint: 9.39.1 graphemer: 1.4.0 ignore: 7.0.5 @@ -2198,22 +1839,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.48.0 debug: 4.4.3 eslint: 9.39.1 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.47.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) - '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -2224,20 +1865,20 @@ snapshots: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/scope-manager@8.47.0': + '@typescript-eslint/scope-manager@8.48.0': dependencies: - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 - '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.47.0(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -2247,7 +1888,7 @@ snapshots: '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/types@8.47.0': {} + '@typescript-eslint/types@8.48.0': {} '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.3)': dependencies: @@ -2264,17 +1905,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.47.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3) + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/visitor-keys': 8.48.0 debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 + tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -2291,12 +1931,12 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.47.0(eslint@9.39.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.48.0 + '@typescript-eslint/types': 8.48.0 + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) eslint: 9.39.1 typescript: 5.9.3 transitivePeerDependencies: @@ -2307,79 +1947,77 @@ snapshots: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.47.0': + '@typescript-eslint/visitor-keys@8.48.0': dependencies: - '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/types': 8.48.0 eslint-visitor-keys: 4.2.1 - '@vitest/coverage-v8@2.1.9(vitest@2.1.9)': + '@vitest/coverage-v8@4.0.14(vitest@4.0.14)': dependencies: - '@ampproject/remapping': 2.3.0 - '@bcoe/v8-coverage': 0.2.3 - debug: 4.4.3 + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.14 + 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 - magic-string: 0.30.21 - magicast: 0.3.5 + magicast: 0.5.1 + obug: 2.1.1 std-env: 3.10.0 - test-exclude: 7.0.1 - tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9) + tinyrainbow: 3.0.3 + vitest: 4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14) transitivePeerDependencies: - supports-color - '@vitest/expect@2.1.9': + '@vitest/expect@4.0.14': dependencies: - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - tinyrainbow: 1.2.0 + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + chai: 6.2.1 + tinyrainbow: 3.0.3 - '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.1))': + '@vitest/mocker@4.0.14(vite@7.2.6(@types/node@24.10.1))': dependencies: - '@vitest/spy': 2.1.9 + '@vitest/spy': 4.0.14 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@22.19.1) + vite: 7.2.6(@types/node@24.10.1) - '@vitest/pretty-format@2.1.9': + '@vitest/pretty-format@4.0.14': dependencies: - tinyrainbow: 1.2.0 + tinyrainbow: 3.0.3 - '@vitest/runner@2.1.9': + '@vitest/runner@4.0.14': dependencies: - '@vitest/utils': 2.1.9 - pathe: 1.1.2 + '@vitest/utils': 4.0.14 + pathe: 2.0.3 - '@vitest/snapshot@2.1.9': + '@vitest/snapshot@4.0.14': dependencies: - '@vitest/pretty-format': 2.1.9 + '@vitest/pretty-format': 4.0.14 magic-string: 0.30.21 - pathe: 1.1.2 + pathe: 2.0.3 - '@vitest/spy@2.1.9': - dependencies: - tinyspy: 3.0.2 + '@vitest/spy@4.0.14': {} - '@vitest/ui@2.1.9(vitest@2.1.9)': + '@vitest/ui@4.0.14(vitest@4.0.14)': dependencies: - '@vitest/utils': 2.1.9 + '@vitest/utils': 4.0.14 fflate: 0.8.2 flatted: 3.3.3 - pathe: 1.1.2 + pathe: 2.0.3 sirv: 3.0.2 tinyglobby: 0.2.15 - tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9) + tinyrainbow: 3.0.3 + vitest: 4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14) - '@vitest/utils@2.1.9': + '@vitest/utils@4.0.14': dependencies: - '@vitest/pretty-format': 2.1.9 - loupe: 3.2.1 - tinyrainbow: 1.2.0 + '@vitest/pretty-format': 4.0.14 + tinyrainbow: 3.0.3 '@volar/language-core@2.4.23': dependencies: @@ -2463,16 +2101,10 @@ snapshots: alien-signals@0.4.14: {} - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -2483,6 +2115,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@0.3.8: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + balanced-match@1.0.2: {} brace-expansion@1.1.12: @@ -2498,25 +2136,15 @@ snapshots: dependencies: fill-range: 7.1.1 - cac@6.7.14: {} - callsites@3.1.0: {} - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.1 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 + chai@6.2.1: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - check-error@2.1.1: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -2543,8 +2171,6 @@ snapshots: dependencies: ms: 2.1.3 - deep-eql@5.0.2: {} - deep-is@0.1.4: {} diff@8.0.2: {} @@ -2553,42 +2179,12 @@ snapshots: dependencies: path-type: 4.0.0 - eastasianwidth@0.2.0: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} + dotenv@17.2.3: {} entities@4.5.0: {} es-module-lexer@1.7.0: {} - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -2620,12 +2216,12 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-vitest@0.5.4(eslint@9.39.1)(typescript@5.9.3)(vitest@2.1.9): + eslint-plugin-vitest@0.5.4(eslint@9.39.1)(typescript@5.9.3)(vitest@4.0.14): dependencies: '@typescript-eslint/utils': 7.18.0(eslint@9.39.1)(typescript@5.9.3) eslint: 9.39.1 optionalDependencies: - vitest: 2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9) + vitest: 4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14) transitivePeerDependencies: - supports-color - typescript @@ -2750,11 +2346,6 @@ snapshots: flatted@3.3.3: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - fs-extra@11.3.2: dependencies: graceful-fs: 4.2.11 @@ -2774,15 +2365,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - globals@14.0.0: {} globby@11.1.0: @@ -2827,8 +2409,6 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -2858,14 +2438,10 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - jju@1.4.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -2909,10 +2485,6 @@ snapshots: lodash@4.17.21: {} - loupe@3.2.1: {} - - lru-cache@10.4.3: {} - lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -2921,7 +2493,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.3.5: + magicast@0.5.1: dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 @@ -2950,8 +2522,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} - mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -2969,6 +2539,8 @@ snapshots: natural-compare@1.4.0: {} + obug@2.1.1: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2986,8 +2558,6 @@ snapshots: dependencies: p-limit: 3.1.0 - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3000,19 +2570,10 @@ snapshots: path-parse@1.0.7: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - path-type@4.0.0: {} - pathe@1.1.2: {} - pathe@2.0.3: {} - pathval@2.0.1: {} - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3039,7 +2600,7 @@ snapshots: prelude-ls@1.2.1: {} - prettier@3.6.2: {} + prettier@3.7.3: {} punycode@2.3.1: {} @@ -3105,8 +2666,6 @@ snapshots: siginfo@2.0.0: {} - signal-exit@4.1.0: {} - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -3127,26 +2686,6 @@ snapshots: string-argv@0.3.2: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - strip-json-comments@3.1.1: {} supports-color@7.2.0: @@ -3159,12 +2698,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - test-exclude@7.0.1: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 10.5.0 - minimatch: 9.0.5 - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -3174,11 +2707,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinypool@1.1.1: {} - - tinyrainbow@1.2.0: {} - - tinyspy@3.0.2: {} + tinyrainbow@3.0.3: {} to-regex-range@5.0.1: dependencies: @@ -3198,12 +2727,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.47.0(eslint@9.39.1)(typescript@5.9.3): + typescript-eslint@8.48.0(eslint@9.39.1)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.47.0(eslint@9.39.1)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.48.0(eslint@9.39.1)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3) eslint: 9.39.1 typescript: 5.9.3 transitivePeerDependencies: @@ -3215,7 +2744,7 @@ snapshots: ufo@1.6.1: {} - undici-types@6.21.0: {} + undici-types@7.16.0: {} universalify@2.0.1: {} @@ -3223,27 +2752,9 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@2.1.9(@types/node@22.19.1): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 1.1.2 - vite: 5.4.21(@types/node@22.19.1) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite-plugin-dts@4.5.4(@types/node@22.19.1)(rollup@4.53.2)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.1)): + vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.53.2)(typescript@5.9.3)(vite@7.2.6(@types/node@24.10.1)): dependencies: - '@microsoft/api-extractor': 7.55.0(@types/node@22.19.1) + '@microsoft/api-extractor': 7.55.0(@types/node@24.10.1) '@rollup/pluginutils': 5.3.0(rollup@4.53.2) '@volar/typescript': 2.4.23 '@vue/language-core': 2.2.0(typescript@5.9.3) @@ -3254,22 +2765,13 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 6.4.1(@types/node@22.19.1) + vite: 7.2.6(@types/node@24.10.1) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite@5.4.21(@types/node@22.19.1): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.6 - rollup: 4.53.2 - optionalDependencies: - '@types/node': 22.19.1 - fsevents: 2.3.3 - - vite@6.4.1(@types/node@22.19.1): + vite@7.2.6(@types/node@24.10.1): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -3278,35 +2780,36 @@ snapshots: rollup: 4.53.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.19.1 + '@types/node': 24.10.1 fsevents: 2.3.3 - vitest@2.1.9(@types/node@22.19.1)(@vitest/ui@2.1.9): + vitest@4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14): dependencies: - '@vitest/expect': 2.1.9 - '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.1)) - '@vitest/pretty-format': 2.1.9 - '@vitest/runner': 2.1.9 - '@vitest/snapshot': 2.1.9 - '@vitest/spy': 2.1.9 - '@vitest/utils': 2.1.9 - chai: 5.3.3 - debug: 4.4.3 + '@vitest/expect': 4.0.14 + '@vitest/mocker': 4.0.14(vite@7.2.6(@types/node@24.10.1)) + '@vitest/pretty-format': 4.0.14 + '@vitest/runner': 4.0.14 + '@vitest/snapshot': 4.0.14 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + es-module-lexer: 1.7.0 expect-type: 1.2.2 magic-string: 0.30.21 - pathe: 1.1.2 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 std-env: 3.10.0 tinybench: 2.9.0 tinyexec: 0.3.2 - tinypool: 1.1.1 - tinyrainbow: 1.2.0 - vite: 5.4.21(@types/node@22.19.1) - vite-node: 2.1.9(@types/node@22.19.1) + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.6(@types/node@24.10.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.19.1 - '@vitest/ui': 2.1.9(vitest@2.1.9) + '@types/node': 24.10.1 + '@vitest/ui': 4.0.14(vitest@4.0.14) transitivePeerDependencies: + - jiti - less - lightningcss - msw @@ -3314,8 +2817,9 @@ snapshots: - sass-embedded - stylus - sugarss - - supports-color - terser + - tsx + - yaml vscode-uri@3.1.0: {} @@ -3330,18 +2834,6 @@ snapshots: word-wrap@1.2.5: {} - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - yallist@4.0.0: {} yocto-queue@0.1.0: {} diff --git a/src/Payment.ts b/src/Payment.ts index cede864..95f3a3b 100644 --- a/src/Payment.ts +++ b/src/Payment.ts @@ -59,10 +59,10 @@ export class Payment { * * @example * ```typescript - * // Create a MATCH_CODE payment + * // Create a payment using amount in euros (recommended) * const payment = await Payment.create({ * flow: 'MATCH_CODE', - * amount_unit: 1000, // 10.00 EUR in cents + * amount: 10.50, // 10.50 EUR (automatically converted to cents) * currency: 'EUR', * external_code: 'ORDER-123', * metadata: { @@ -71,6 +71,13 @@ export class Payment { * }, * }) * + * // Or using amount_unit in cents + * const payment2 = await Payment.create({ + * flow: 'MATCH_CODE', + * amount_unit: 1050, // 10.50 EUR in cents + * currency: 'EUR', + * }) + * * console.log(`Payment code: ${payment.code_identifier}`) * ``` */ @@ -78,9 +85,16 @@ export class Payment { body: PaymentCreateBody, headers: Record = {} ): Promise { + // Convert amount (euros) to amount_unit (cents) if provided + const processedBody: any = { ...body } + if ('amount' in body && body.amount !== undefined) { + processedBody.amount_unit = Math.round(body.amount * 100) + delete processedBody.amount + } + return Request.post(this.apiPath, { headers, - body, + body: processedBody, sign: true, }) } @@ -99,17 +113,53 @@ export class Payment { /** * Get the payments list + * * @param query Query parameters (optional) * @param headers Custom headers (optional) + * + * @example + * ```typescript + * // List all payments + * const payments = await Payment.all({ limit: 20 }) + * + * // Filter by date using Date object (recommended) + * const yesterday = new Date() + * yesterday.setDate(yesterday.getDate() - 1) + * const filtered = await Payment.all({ + * starting_after_timestamp: yesterday, + * limit: 10 + * }) + * + * // Or using timestamp string in milliseconds + * const filteredByString = await Payment.all({ + * starting_after_timestamp: yesterday.getTime().toString(), + * limit: 10 + * }) + * + * // Pagination using payment ID + * const nextPage = await Payment.all({ + * starting_after: 'last-payment-id', + * limit: 20 + * }) + * ``` + * + * @note The `starting_after_timestamp` parameter accepts both Date objects and timestamp strings in milliseconds. + * Date objects are automatically converted to milliseconds timestamp. */ static async all( query: PaymentQueryParams = {}, headers: Record = {} - ): Promise<{ list: PaymentResponse[]; has_more: boolean }> { + ): Promise<{ data: PaymentResponse[]; has_more: boolean }> { let path = this.apiPath if (Object.keys(query).length > 0) { - const queryString = new URLSearchParams(query as unknown as Record).toString() + // Convert Date object to timestamp string if necessary + const processedQuery = { ...query } + if (processedQuery.starting_after_timestamp instanceof Date) { + processedQuery.starting_after_timestamp = processedQuery.starting_after_timestamp.getTime().toString() + } + + const queryString = new URLSearchParams(processedQuery as unknown as Record).toString() path += `?${queryString}` } @@ -121,18 +171,41 @@ export class Payment { /** * Update a payment + * * @param id Payment ID - * @param body Update data + * @param body Update data (action and optionally amount or amount_unit) * @param headers Custom headers (optional) + * + * @example + * ```typescript + * // Update using amount in euros + * const updated = await Payment.update('payment-id', { + * action: 'ACCEPT', + * amount: 5.50, // Automatically converted to 550 cents + * }) + * + * // Or using amount_unit in cents + * const updated2 = await Payment.update('payment-id', { + * action: 'ACCEPT', + * amount_unit: 550, + * }) + * ``` */ static async update( id: string, body: Partial, headers: Record = {} ): Promise { + // Convert amount (euros) to amount_unit (cents) if provided + const processedBody: any = { ...body } + if ('amount' in body && body.amount !== undefined) { + processedBody.amount_unit = Math.round(body.amount * 100) + delete processedBody.amount + } + return Request.put(`${this.apiPath}/${id}`, { headers, - body, + body: processedBody, sign: true, }) } diff --git a/src/Report.ts b/src/Report.ts new file mode 100644 index 0000000..0277d74 --- /dev/null +++ b/src/Report.ts @@ -0,0 +1,142 @@ +import { Request } from './Request.js' +import type { + ReportCreateBody, + ReportResponse, + ReportListQueryParams, +} from './types.js' + +/** + * Report class for managing Satispay payment reports + * + * @remarks + * The Report API requires special authentication keys that are different from + * standard API keys. Contact tech@satispay.com to enable report access. + * + * Reports are extracted at merchant level and include all shops under the merchant. + * Reports for the previous day should be generated at least 4 hours after midnight + * to ensure complete data. + * + * @example + * ```typescript + * import { Report } from '@volverjs/satispay-node-sdk'; + * + * // Create a new report + * const report = await Report.create({ + * type: 'PAYMENT_FEE', + * format: 'CSV', + * from_date: '2025-11-01', + * to_date: '2025-11-30' + * }); + * + * // Get list of reports + * const reports = await Report.all(); + * + * // Get a specific report + * const reportDetails = await Report.get('report-id-123'); + * ``` + */ +export class Report { + /** + * Create a new report + * + * @param body - Report creation parameters + * @param headers - Optional custom headers + * @returns Promise resolving to the created report + * + * @example + * ```typescript + * const report = await Report.create({ + * type: 'PAYMENT_FEE', + * format: 'CSV', + * from_date: '2025-11-01', + * to_date: '2025-11-30', + * columns: ['transaction_id', 'transaction_date', 'total_amount'] + * }); + * ``` + */ + static async create( + body: ReportCreateBody, + headers: Record = {}, + ): Promise { + return await Request.post('/g_business/v1/reports', { + headers, + body, + sign: true, + }) + } + + /** + * Retrieve a list of previously created reports + * + * @param query - Optional query parameters for pagination + * @param headers - Optional custom headers + * @returns Promise resolving to an object containing list of reports + * + * @example + * ```typescript + * // Get all reports + * const result = await Report.all(); + * + * // Get reports with pagination + * const result = await Report.all({ + * limit: 10, + * starting_after: 'report-123' + * }); + * ``` + */ + static async all( + query: ReportListQueryParams = {}, + headers: Record = {}, + ): Promise<{ list: ReportResponse[]; has_more: boolean }> { + const queryParams = new URLSearchParams() + + if (query.limit) { + queryParams.append('limit', query.limit.toString()) + } + if (query.starting_after) { + queryParams.append('starting_after', query.starting_after) + } + + const queryString = queryParams.toString() + const url = queryString + ? `/g_business/v1/reports?${queryString}` + : '/g_business/v1/reports' + + return await Request.get<{ list: ReportResponse[]; has_more: boolean }>( + url, + { + headers, + sign: true, + }, + ) + } + + /** + * Retrieve details of a specific report + * + * @param id - The report ID + * @param headers - Optional custom headers + * @returns Promise resolving to the report details + * + * @example + * ```typescript + * const report = await Report.get('report-id-123'); + * + * if (report.status === 'READY' && report.download_url) { + * console.log('Download URL:', report.download_url); + * } + * ``` + */ + static async get( + id: string, + headers: Record = {}, + ): Promise { + return await Request.get( + `/g_business/v1/reports/${id}`, + { + headers, + sign: true, + }, + ) + } +} diff --git a/src/Request.ts b/src/Request.ts index 66f9429..297f088 100644 --- a/src/Request.ts +++ b/src/Request.ts @@ -191,11 +191,11 @@ export class Request { fetchOptions.body = body } - // Disable SSL verification in test mode (Node.js only) + // Disable SSL verification in staging mode (Node.js only) // Note: Deno and Bun don't support this directly via fetch - if (Api.getEnv() === 'test' && typeof process !== 'undefined' && process.versions?.node) { + if (Api.getEnv() === 'staging' && typeof process !== 'undefined' && process.versions?.node) { // For Node.js 18+ with fetch, we need to use a custom agent - // This is a workaround for test environments only + // This is a workaround for staging/test environments only const https = await import('https') const agent = new https.Agent({ rejectUnauthorized: false, diff --git a/src/Session.ts b/src/Session.ts new file mode 100644 index 0000000..6dd918e --- /dev/null +++ b/src/Session.ts @@ -0,0 +1,171 @@ +import { Request } from './Request.js' +import type { + SessionCreateBody, + SessionResponse, + SessionUpdateBody, + SessionEventCreateBody, +} from './types.js' + +/** + * Session class for managing Satispay POS sessions + * + * @remarks + * Sessions are used for POS/device integration to manage fund lock payments + * across multiple transactions. A session allows charging a fund lock incrementally + * through multiple events. + * + * @example + * ```typescript + * import { Session } from '@volverjs/satispay-node-sdk'; + * + * // Open a session from a fund lock + * const session = await Session.open({ + * fund_lock_id: 'payment-123' + * }); + * + * // Add items to the session + * await Session.createEvent(session.id, { + * type: 'ADD_ITEM', + * amount_unit: 500, + * description: 'Product A' + * }); + * + * // Get session details + * const details = await Session.get(session.id); + * + * // Close the session + * await Session.update(session.id, { status: 'CLOSE' }); + * ``` + */ +export class Session { + /** + * Open a new session from a fund lock payment + * + * @param body - Session creation parameters containing the fund lock ID + * @param headers - Optional custom headers + * @returns Promise resolving to the created session + * + * @example + * ```typescript + * const session = await Session.open({ + * fund_lock_id: 'payment-fund-lock-123' + * }); + * console.log('Session ID:', session.id); + * console.log('Residual amount:', session.residual_amount_unit); + * ``` + */ + static async open( + body: SessionCreateBody, + headers: Record = {}, + ): Promise { + return await Request.post('/g_business/v1/sessions', { + headers, + body, + sign: true, + }) + } + + /** + * Retrieve details of a specific session + * + * @param id - The session ID + * @param headers - Optional custom headers + * @returns Promise resolving to the session details + * + * @example + * ```typescript + * const session = await Session.get('session-123'); + * console.log('Status:', session.status); + * console.log('Residual amount:', session.residual_amount_unit); + * ``` + */ + static async get( + id: string, + headers: Record = {}, + ): Promise { + return await Request.get( + `/g_business/v1/sessions/${id}`, + { + headers, + sign: true, + }, + ) + } + + /** + * Update a session (typically to close it) + * + * @param id - The session ID + * @param body - Session update parameters + * @param headers - Optional custom headers + * @returns Promise resolving to the updated session + * + * @example + * ```typescript + * // Close a session + * const closedSession = await Session.update('session-123', { + * status: 'CLOSE' + * }); + * console.log('Final status:', closedSession.status); + * ``` + */ + static async update( + id: string, + body: SessionUpdateBody, + headers: Record = {}, + ): Promise { + return await Request.patch( + `/g_business/v1/sessions/${id}`, + { + headers, + body, + sign: true, + }, + ) + } + + /** + * Create an event within a session + * + * @remarks + * Events are used to add, remove, or update items in a POS session. + * Each event can modify the total amount being charged from the fund lock. + * + * @param id - The session ID + * @param body - Event creation parameters + * @param headers - Optional custom headers + * @returns Promise resolving to the updated session after the event + * + * @example + * ```typescript + * // Add an item to the session + * await Session.createEvent('session-123', { + * type: 'ADD_ITEM', + * amount_unit: 1000, + * description: 'Coffee', + * metadata: { sku: 'COFFEE-001' } + * }); + * + * // Remove an item + * await Session.createEvent('session-123', { + * type: 'REMOVE_ITEM', + * amount_unit: 500, + * description: 'Discount applied' + * }); + * ``` + */ + static async createEvent( + id: string, + body: SessionEventCreateBody, + headers: Record = {}, + ): Promise { + return await Request.post( + `/g_business/v1/sessions/${id}/events`, + { + headers, + body, + sign: true, + }, + ) + } +} diff --git a/src/index.ts b/src/index.ts index a10c646..9359a3f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,8 @@ export { Payment } from './Payment.js' export { Consumer } from './Consumer.js' export { DailyClosure } from './DailyClosure.js' export { PreAuthorizedPaymentToken } from './PreAuthorizedPaymentToken.js' +export { Report } from './Report.js' +export { Session } from './Session.js' export { Request } from './Request.js' export { RSAServiceFactory } from './RSAService/RSAServiceFactory.js' diff --git a/src/types.ts b/src/types.ts index 9f4dd96..49b7959 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,7 +23,7 @@ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' /** * Environment */ -export type Environment = 'production' | 'staging' | 'test' +export type Environment = 'production' | 'staging' /** * Payment status @@ -42,6 +42,31 @@ export type PaymentFlow = | 'PRE_AUTHORIZED_FUND_LOCK' | 'HOTP_AUTH' +/** + * Report type + */ +export type ReportType = 'PAYMENT_FEE' + +/** + * Report format type + */ +export type ReportFormatType = 'CSV' | 'PDF' | 'XLSX' + +/** + * Report status + */ +export type ReportStatus = 'PENDING' | 'READY' | 'FAILED' + +/** + * Session status + */ +export type SessionStatus = 'OPEN' | 'CLOSE' + +/** + * Session event type + */ +export type SessionEventType = 'ADD_ITEM' | 'REMOVE_ITEM' | 'UPDATE_TOTAL' + /** * Payment action */ @@ -65,10 +90,14 @@ export type PreAuthorizedTokenStatus = 'PENDING' | 'ACCEPTED' | 'CANCELED' /** * Payment creation body + * + * @property amount - Amount in euros (e.g., 10.50). Will be automatically converted to amount_unit. + * @property amount_unit - Amount in cents (e.g., 1050). Use either 'amount' or 'amount_unit', not both. + * @property meal_voucher_max_amount_unit - Maximum amount in cents that can be paid with meal vouchers + * @property meal_voucher_max_quantity - Maximum number of meal vouchers that can be used */ export type PaymentCreateBody = { flow: PaymentFlow - amount_unit: number currency: string callback_url?: string external_code?: string @@ -77,7 +106,12 @@ export type PaymentCreateBody = { consumer_uid?: string required_success_email?: string pre_authorized_payments_token?: string + meal_voucher_max_amount_unit?: number + meal_voucher_max_quantity?: number } & ( + | { amount: number; amount_unit?: never } + | { amount?: never; amount_unit: number } +) & ( | { flow: 'HOTP_AUTH' | 'PRE_AUTHORIZED' | 'PRE_AUTHORIZED_FUND_LOCK'; token: string } | { flow: 'REFUND'; parent_payment_uid: string } | { flow: 'MATCH_USER'; consumer_uid: string } @@ -86,11 +120,21 @@ export type PaymentCreateBody = { /** * Payment update body + * + * @property amount - Amount in euros (e.g., 10.50). Will be automatically converted to amount_unit. + * @property amount_unit - Amount in cents (e.g., 1050). Use either 'amount' or 'amount_unit', not both. + * @property meal_voucher_max_amount_unit - Maximum amount in cents that can be paid with meal vouchers + * @property meal_voucher_max_quantity - Maximum number of meal vouchers that can be used */ export type PaymentUpdateBody = { action: PaymentAction - amount_unit?: number -} + meal_voucher_max_amount_unit?: number + meal_voucher_max_quantity?: number +} & ( + | { amount?: number; amount_unit?: never } + | { amount?: never; amount_unit?: number } + | { amount?: never; amount_unit?: never } +) /** * Pre-authorized payment token creation body @@ -176,7 +220,7 @@ export type PreAuthorizedPaymentResponse = { export type PaymentQueryParams = { limit?: number starting_after?: string - starting_after_timestamp?: string + starting_after_timestamp?: string | Date consumer_uid?: string payment_type?: string status?: PaymentStatus @@ -192,3 +236,76 @@ export type DailyClosureQueryParams = { limit?: number starting_after?: string } + +/** + * Report creation body + */ +export type ReportCreateBody = { + type: ReportType + format?: ReportFormatType + from_date: string + to_date: string + columns?: string[] +} + +/** + * Report response + */ +export type ReportResponse = { + id: string + type: ReportType + format: ReportFormatType + status: ReportStatus + from_date: string + to_date: string + download_url?: string + created_at: string + updated_at: string +} + +/** + * Report list query parameters + */ +export type ReportListQueryParams = { + limit?: number + starting_after?: string +} + +/** + * Session creation body + */ +export type SessionCreateBody = { + fund_lock_id: string +} + +/** + * Session response + */ +export type SessionResponse = { + id: string + amount_unit: number + residual_amount_unit: number + currency: string + status: SessionStatus + type: string + consumer_uid?: string + available?: boolean + expiration_date?: string +} + +/** + * Session update body + */ +export type SessionUpdateBody = { + status: SessionStatus +} + +/** + * Session event creation body + */ +export type SessionEventCreateBody = { + type: SessionEventType + amount_unit?: number + description?: string + metadata?: Record +} diff --git a/tests/Api.test.ts b/tests/Api.test.ts index 3eeb2e5..bcfc3b8 100644 --- a/tests/Api.test.ts +++ b/tests/Api.test.ts @@ -14,7 +14,7 @@ describe('Api', () => { }) it('should set and get environment correctly', () => { - const environments: Environment[] = ['production', 'staging', 'test'] + const environments: Environment[] = ['production', 'staging'] environments.forEach((env) => { Api.setEnv(env) @@ -90,4 +90,43 @@ describe('Api', () => { expect(version).toMatch(/^\d+\.\d+\.\d+$/) }) }) + + describe('sandbox management', () => { + it('should check if sandbox is enabled', () => { + Api.setEnv('staging') + expect(Api.getSandbox()).toBe(true) + + Api.setEnv('production') + expect(Api.getSandbox()).toBe(false) + }) + + it('should enable sandbox', () => { + Api.setSandbox(true) + expect(Api.getEnv()).toBe('staging') + expect(Api.getSandbox()).toBe(true) + }) + + it('should disable sandbox', () => { + Api.setSandbox(false) + expect(Api.getEnv()).toBe('production') + expect(Api.getSandbox()).toBe(false) + }) + + it('should enable sandbox by default', () => { + Api.setSandbox() + expect(Api.getEnv()).toBe('staging') + }) + }) + + describe('getAuthservicesUrl', () => { + it('should return production URL', () => { + Api.setEnv('production') + expect(Api.getAuthservicesUrl()).toBe('https://authservices.satispay.com') + }) + + it('should return staging URL', () => { + Api.setEnv('staging') + expect(Api.getAuthservicesUrl()).toBe('https://staging.authservices.satispay.com') + }) + }) }) diff --git a/tests/Payment.test.ts b/tests/Payment.test.ts index 805ed60..15589da 100644 --- a/tests/Payment.test.ts +++ b/tests/Payment.test.ts @@ -63,6 +63,49 @@ describe('Payment', () => { expect(result).toEqual(mockPaymentResponse) }) + it('should create a payment using amount in euros', async () => { + const mockBody: PaymentCreateBody = { + flow: 'MATCH_CODE', + amount: 10.50, // 10.50 euros + currency: 'EUR', + } + + vi.mocked(Request.post).mockResolvedValue(mockPaymentResponse) + + const result = await Payment.create(mockBody) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/payments', { + headers: {}, + body: { + flow: 'MATCH_CODE', + amount_unit: 1050, // Converted to cents + currency: 'EUR', + }, + sign: true, + }) + expect(result).toEqual(mockPaymentResponse) + }) + + it('should handle decimal amounts correctly', async () => { + const mockBody: PaymentCreateBody = { + flow: 'MATCH_CODE', + amount: 99.99, + currency: 'EUR', + } + + vi.mocked(Request.post).mockResolvedValue(mockPaymentResponse) + + await Payment.create(mockBody) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/payments', + expect.objectContaining({ + body: expect.objectContaining({ + amount_unit: 9999, + }), + }) + ) + }) + it('should create a payment with custom headers', async () => { const mockBody: PaymentCreateBody = { flow: 'MATCH_CODE', @@ -180,6 +223,43 @@ describe('Payment', () => { sign: true, }) }) + + it('should convert Date object to timestamp in milliseconds', async () => { + const testDate = new Date('2024-01-15T10:30:00.000Z') + const expectedTimestamp = testDate.getTime().toString() + + vi.mocked(Request.get).mockResolvedValue(mockListResponse) + + await Payment.all({ + starting_after_timestamp: testDate, + limit: 5, + }) + + expect(Request.get).toHaveBeenCalledWith( + `/g_business/v1/payments?starting_after_timestamp=${expectedTimestamp}&limit=5`, + expect.objectContaining({ + sign: true, + }) + ) + }) + + it('should accept timestamp as string without conversion', async () => { + const timestampString = '1705315800000' + + vi.mocked(Request.get).mockResolvedValue(mockListResponse) + + await Payment.all({ + starting_after_timestamp: timestampString, + limit: 5, + }) + + expect(Request.get).toHaveBeenCalledWith( + `/g_business/v1/payments?starting_after_timestamp=${timestampString}&limit=5`, + expect.objectContaining({ + sign: true, + }) + ) + }) }) describe('update', () => { @@ -200,7 +280,7 @@ describe('Payment', () => { expect(result).toEqual(mockPaymentResponse) }) - it('should update a payment with amount', async () => { + it('should update a payment with amount_unit', async () => { const paymentId = 'payment-123' const updateBody = { action: 'ACCEPT' as const, @@ -219,6 +299,48 @@ describe('Payment', () => { ) }) + it('should update a payment with amount in euros', async () => { + const paymentId = 'payment-123' + const updateBody = { + action: 'ACCEPT' as const, + amount: 7.50, // 7.50 euros + } + vi.mocked(Request.put).mockResolvedValue(mockPaymentResponse) + + await Payment.update(paymentId, updateBody) + + expect(Request.put).toHaveBeenCalledWith( + '/g_business/v1/payments/payment-123', + expect.objectContaining({ + body: { + action: 'ACCEPT', + amount_unit: 750, // Converted to cents + }, + sign: true, + }) + ) + }) + + it('should handle decimal amounts in update', async () => { + const paymentId = 'payment-123' + const updateBody = { + action: 'ACCEPT' as const, + amount: 12.99, + } + vi.mocked(Request.put).mockResolvedValue(mockPaymentResponse) + + await Payment.update(paymentId, updateBody) + + expect(Request.put).toHaveBeenCalledWith( + '/g_business/v1/payments/payment-123', + expect.objectContaining({ + body: expect.objectContaining({ + amount_unit: 1299, + }), + }) + ) + }) + it('should handle custom headers in update', async () => { const paymentId = 'payment-123' const updateBody = { action: 'CANCEL' as const } diff --git a/tests/Report.test.ts b/tests/Report.test.ts new file mode 100644 index 0000000..505855f --- /dev/null +++ b/tests/Report.test.ts @@ -0,0 +1,222 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { Report } from '../src/Report' +import { Request } from '../src/Request' +import type { ReportResponse, ReportCreateBody } from '../src/types' + +// Mock Request module +vi.mock('../src/Request', () => ({ + Request: { + get: vi.fn(), + post: vi.fn(), + }, +})) + +describe('Report', () => { + const mockReportResponse: ReportResponse = { + id: 'report-123', + type: 'PAYMENT_FEE', + format: 'CSV', + status: 'READY', + from_date: '2025-11-01', + to_date: '2025-11-30', + download_url: 'https://example.com/report.csv', + created_at: '2025-12-01T10:00:00.000Z', + updated_at: '2025-12-01T10:30:00.000Z', + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('create', () => { + it('should create a new report', async () => { + const mockBody: ReportCreateBody = { + type: 'PAYMENT_FEE', + format: 'CSV', + from_date: '2025-11-01', + to_date: '2025-11-30', + } + + vi.mocked(Request.post).mockResolvedValue(mockReportResponse) + + const result = await Report.create(mockBody) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/reports', { + headers: {}, + body: mockBody, + sign: true, + }) + expect(result).toEqual(mockReportResponse) + }) + + it('should create a report with custom columns', async () => { + const mockBody: ReportCreateBody = { + type: 'PAYMENT_FEE', + format: 'XLSX', + from_date: '2025-11-01', + to_date: '2025-11-30', + columns: ['transaction_id', 'transaction_date', 'total_amount'], + } + + vi.mocked(Request.post).mockResolvedValue(mockReportResponse) + + await Report.create(mockBody) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/reports', { + headers: {}, + body: mockBody, + sign: true, + }) + }) + + it('should create a report with custom headers', async () => { + const mockBody: ReportCreateBody = { + type: 'PAYMENT_FEE', + format: 'PDF', + from_date: '2025-11-01', + to_date: '2025-11-30', + } + const customHeaders = { + 'Idempotency-Key': 'report-unique-123', + } + + vi.mocked(Request.post).mockResolvedValue(mockReportResponse) + + await Report.create(mockBody, customHeaders) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/reports', { + headers: customHeaders, + body: mockBody, + sign: true, + }) + }) + }) + + describe('all', () => { + const mockListResponse = { + list: [mockReportResponse], + has_more: false, + } + + it('should get all reports without query params', async () => { + vi.mocked(Request.get).mockResolvedValue(mockListResponse) + + const result = await Report.all() + + expect(Request.get).toHaveBeenCalledWith('/g_business/v1/reports', { + headers: {}, + sign: true, + }) + expect(result).toEqual(mockListResponse) + }) + + it('should get reports with pagination', async () => { + const query = { + limit: 10, + starting_after: 'report-100', + } + vi.mocked(Request.get).mockResolvedValue(mockListResponse) + + await Report.all(query) + + expect(Request.get).toHaveBeenCalledWith( + '/g_business/v1/reports?limit=10&starting_after=report-100', + expect.objectContaining({ + sign: true, + }), + ) + }) + + it('should handle custom headers', async () => { + const customHeaders = { 'X-Custom': 'header' } + vi.mocked(Request.get).mockResolvedValue(mockListResponse) + + await Report.all({}, customHeaders) + + expect(Request.get).toHaveBeenCalledWith('/g_business/v1/reports', { + headers: customHeaders, + sign: true, + }) + }) + + it('should get reports with limit only', async () => { + const query = { limit: 5 } + vi.mocked(Request.get).mockResolvedValue(mockListResponse) + + await Report.all(query) + + expect(Request.get).toHaveBeenCalledWith( + '/g_business/v1/reports?limit=5', + expect.objectContaining({ + sign: true, + }), + ) + }) + }) + + describe('get', () => { + it('should get a report by id', async () => { + const reportId = 'report-123' + vi.mocked(Request.get).mockResolvedValue(mockReportResponse) + + const result = await Report.get(reportId) + + expect(Request.get).toHaveBeenCalledWith( + '/g_business/v1/reports/report-123', + { + headers: {}, + sign: true, + }, + ) + expect(result).toEqual(mockReportResponse) + }) + + it('should get a report with custom headers', async () => { + const reportId = 'report-123' + const customHeaders = { 'X-Custom': 'value' } + vi.mocked(Request.get).mockResolvedValue(mockReportResponse) + + await Report.get(reportId, customHeaders) + + expect(Request.get).toHaveBeenCalledWith( + '/g_business/v1/reports/report-123', + { + headers: customHeaders, + sign: true, + }, + ) + }) + + it('should handle pending report status', async () => { + const pendingReport: ReportResponse = { + ...mockReportResponse, + status: 'PENDING', + download_url: undefined, + } + vi.mocked(Request.get).mockResolvedValue(pendingReport) + + const result = await Report.get('report-123') + + expect(result.status).toBe('PENDING') + expect(result.download_url).toBeUndefined() + }) + + it('should handle failed report status', async () => { + const failedReport: ReportResponse = { + ...mockReportResponse, + status: 'FAILED', + download_url: undefined, + } + vi.mocked(Request.get).mockResolvedValue(failedReport) + + const result = await Report.get('report-123') + + expect(result.status).toBe('FAILED') + expect(result.download_url).toBeUndefined() + }) + }) +}) diff --git a/tests/Request.test.ts b/tests/Request.test.ts index 09c31f4..0cae5fc 100644 --- a/tests/Request.test.ts +++ b/tests/Request.test.ts @@ -9,7 +9,7 @@ global.fetch = mockFetch as any describe('Request', () => { beforeEach(() => { vi.clearAllMocks() - Api.setEnv('test') + Api.setEnv('staging') Api.setPrivateKey( '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----' ) @@ -185,12 +185,51 @@ describe('Request', () => { await expect(Request.get('/test/path')).rejects.toThrow() }) + it('should throw error with request id when available', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 400, + text: async () => + JSON.stringify({ + message: 'Bad request', + code: 'BAD_REQUEST', + wlt: 'req-123', + }), + headers: new Headers(), + }) + + await expect(Request.get('/test/path')).rejects.toThrow( + 'Bad request, request id: req-123' + ) + }) + + it('should throw generic error when error details not available', async () => { + mockFetch.mockResolvedValue({ + ok: false, + status: 500, + text: async () => 'Internal Server Error', + headers: new Headers(), + }) + + await expect(Request.get('/test/path')).rejects.toThrow( + 'HTTP status is not 2xx: 500' + ) + }) + it('should throw error on network failure', async () => { mockFetch.mockRejectedValue(new Error('Network error')) await expect(Request.get('/test/path')).rejects.toThrow('Network error') }) + it('should handle non-Error thrown values', async () => { + mockFetch.mockRejectedValue('string error') + + await expect(Request.get('/test/path')).rejects.toThrow( + 'Request failed: string error' + ) + }) + it('should handle non-JSON responses', async () => { mockFetch.mockResolvedValue({ ok: true, diff --git a/tests/Session.test.ts b/tests/Session.test.ts new file mode 100644 index 0000000..36ee35d --- /dev/null +++ b/tests/Session.test.ts @@ -0,0 +1,301 @@ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { Session } from '../src/Session' +import { Request } from '../src/Request' +import type { + SessionResponse, + SessionCreateBody, + SessionUpdateBody, + SessionEventCreateBody, +} from '../src/types' + +// Mock Request module +vi.mock('../src/Request', () => ({ + Request: { + get: vi.fn(), + post: vi.fn(), + patch: vi.fn(), + }, +})) + +describe('Session', () => { + const mockSessionResponse: SessionResponse = { + id: 'session-123', + amount_unit: 5000, + residual_amount_unit: 5000, + currency: 'EUR', + status: 'OPEN', + type: 'TO_BUSINESS_CHARGE', + consumer_uid: 'consumer-456', + available: true, + expiration_date: '2025-12-01T18:00:00.000Z', + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + describe('open', () => { + it('should open a new session', async () => { + const mockBody: SessionCreateBody = { + fund_lock_id: 'payment-fund-lock-123', + } + + vi.mocked(Request.post).mockResolvedValue(mockSessionResponse) + + const result = await Session.open(mockBody) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/sessions', { + headers: {}, + body: mockBody, + sign: true, + }) + expect(result).toEqual(mockSessionResponse) + }) + + it('should open a session with custom headers', async () => { + const mockBody: SessionCreateBody = { + fund_lock_id: 'payment-fund-lock-456', + } + const customHeaders = { + 'Idempotency-Key': 'session-unique-123', + } + + vi.mocked(Request.post).mockResolvedValue(mockSessionResponse) + + await Session.open(mockBody, customHeaders) + + expect(Request.post).toHaveBeenCalledWith('/g_business/v1/sessions', { + headers: customHeaders, + body: mockBody, + sign: true, + }) + }) + }) + + describe('get', () => { + it('should get session details by id', async () => { + const sessionId = 'session-123' + vi.mocked(Request.get).mockResolvedValue(mockSessionResponse) + + const result = await Session.get(sessionId) + + expect(Request.get).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123', + { + headers: {}, + sign: true, + }, + ) + expect(result).toEqual(mockSessionResponse) + }) + + it('should get session with custom headers', async () => { + const sessionId = 'session-123' + const customHeaders = { 'X-Custom': 'value' } + vi.mocked(Request.get).mockResolvedValue(mockSessionResponse) + + await Session.get(sessionId, customHeaders) + + expect(Request.get).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123', + { + headers: customHeaders, + sign: true, + }, + ) + }) + + it('should get session with reduced residual amount', async () => { + const sessionWithCharges: SessionResponse = { + ...mockSessionResponse, + residual_amount_unit: 3000, + } + vi.mocked(Request.get).mockResolvedValue(sessionWithCharges) + + const result = await Session.get('session-123') + + expect(result.residual_amount_unit).toBe(3000) + expect(result.amount_unit).toBe(5000) + }) + }) + + describe('update', () => { + it('should close a session', async () => { + const sessionId = 'session-123' + const updateBody: SessionUpdateBody = { + status: 'CLOSE', + } + const closedSession: SessionResponse = { + ...mockSessionResponse, + status: 'CLOSE', + residual_amount_unit: 0, + } + + vi.mocked(Request.patch).mockResolvedValue(closedSession) + + const result = await Session.update(sessionId, updateBody) + + expect(Request.patch).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123', + { + headers: {}, + body: updateBody, + sign: true, + }, + ) + expect(result).toEqual(closedSession) + expect(result.status).toBe('CLOSE') + }) + + it('should update session with custom headers', async () => { + const sessionId = 'session-123' + const updateBody: SessionUpdateBody = { + status: 'CLOSE', + } + const customHeaders = { 'X-Reason': 'customer-request' } + + vi.mocked(Request.patch).mockResolvedValue(mockSessionResponse) + + await Session.update(sessionId, updateBody, customHeaders) + + expect(Request.patch).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123', + { + headers: customHeaders, + body: updateBody, + sign: true, + }, + ) + }) + }) + + describe('createEvent', () => { + it('should add an item to the session', async () => { + const sessionId = 'session-123' + const eventBody: SessionEventCreateBody = { + type: 'ADD_ITEM', + amount_unit: 1000, + description: 'Coffee', + } + const updatedSession: SessionResponse = { + ...mockSessionResponse, + residual_amount_unit: 4000, + } + + vi.mocked(Request.post).mockResolvedValue(updatedSession) + + const result = await Session.createEvent(sessionId, eventBody) + + expect(Request.post).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123/events', + { + headers: {}, + body: eventBody, + sign: true, + }, + ) + expect(result.residual_amount_unit).toBe(4000) + }) + + it('should remove an item from the session', async () => { + const sessionId = 'session-123' + const eventBody: SessionEventCreateBody = { + type: 'REMOVE_ITEM', + amount_unit: 500, + description: 'Discount applied', + } + + vi.mocked(Request.post).mockResolvedValue(mockSessionResponse) + + await Session.createEvent(sessionId, eventBody) + + expect(Request.post).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123/events', + { + headers: {}, + body: eventBody, + sign: true, + }, + ) + }) + + it('should update the total', async () => { + const sessionId = 'session-123' + const eventBody: SessionEventCreateBody = { + type: 'UPDATE_TOTAL', + amount_unit: 3500, + description: 'Total updated', + } + + vi.mocked(Request.post).mockResolvedValue(mockSessionResponse) + + await Session.createEvent(sessionId, eventBody) + + expect(Request.post).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123/events', + { + headers: {}, + body: eventBody, + sign: true, + }, + ) + }) + + it('should create event with metadata', async () => { + const sessionId = 'session-123' + const eventBody: SessionEventCreateBody = { + type: 'ADD_ITEM', + amount_unit: 1200, + description: 'Espresso', + metadata: { + sku: 'COFFEE-001', + category: 'beverages', + }, + } + + vi.mocked(Request.post).mockResolvedValue(mockSessionResponse) + + await Session.createEvent(sessionId, eventBody) + + expect(Request.post).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123/events', + expect.objectContaining({ + body: expect.objectContaining({ + metadata: { + sku: 'COFFEE-001', + category: 'beverages', + }, + }), + }), + ) + }) + + it('should create event with custom headers', async () => { + const sessionId = 'session-123' + const eventBody: SessionEventCreateBody = { + type: 'ADD_ITEM', + amount_unit: 800, + } + const customHeaders = { + 'Idempotency-Key': 'event-unique-123', + } + + vi.mocked(Request.post).mockResolvedValue(mockSessionResponse) + + await Session.createEvent(sessionId, eventBody, customHeaders) + + expect(Request.post).toHaveBeenCalledWith( + '/g_business/v1/sessions/session-123/events', + { + headers: customHeaders, + body: eventBody, + sign: true, + }, + ) + }) + }) +}) diff --git a/tests/e2e/authentication.e2e.test.ts b/tests/e2e/authentication.e2e.test.ts new file mode 100644 index 0000000..e9b8856 --- /dev/null +++ b/tests/e2e/authentication.e2e.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest' +import { Api } from '../../src/Api' +import { canRunE2ETests } from '../setup' + +/** + * E2E tests for Satispay authentication + * + * These tests require: + * - SATISPAY_PUBLIC_KEY, SATISPAY_PRIVATE_KEY, SATISPAY_KEY_ID configured + * + * NOTE: The activation code is not tested because it can only be used once. + * Keys must be manually generated before running E2E tests. + * + * Tests run ONLY in staging environment for safety. + */ + +describe.skipIf(!canRunE2ETests())('E2E: Authentication', () => { + describe('Authentication with existing keys', () => { + it( + 'should have valid authentication keys configured', + () => { + // Verify that keys are configured correctly + const privateKey = Api.getPrivateKey() + const publicKey = Api.getPublicKey() + const keyId = Api.getKeyId() + + expect(privateKey).toBeTruthy() + expect(publicKey).toBeTruthy() + expect(keyId).toBeTruthy() + + // Verify format + expect(privateKey).toContain('-----BEGIN PRIVATE KEY-----') + expect(privateKey).toContain('-----END PRIVATE KEY-----') + expect(publicKey).toContain('-----BEGIN PUBLIC KEY-----') + expect(publicKey).toContain('-----END PUBLIC KEY-----') + expect(keyId).toMatch(/^[a-z0-9]+$/) + + console.log('\nāœ“ Valid authentication keys') + console.log('Key ID:', keyId) + } + ) + }) + + describe('Environment configuration', () => { + it('should be forced to staging environment', () => { + expect(Api.getEnv()).toBe('staging') + }) + + it('should use staging authservices URL', () => { + const url = Api.getAuthservicesUrl() + expect(url).toBe('https://staging.authservices.satispay.com') + }) + + it('should have sandbox mode enabled', () => { + expect(Api.getSandbox()).toBe(true) + }) + }) +}) diff --git a/tests/e2e/payment.e2e.test.ts b/tests/e2e/payment.e2e.test.ts new file mode 100644 index 0000000..37acc0b --- /dev/null +++ b/tests/e2e/payment.e2e.test.ts @@ -0,0 +1,230 @@ +import { describe, it, expect, beforeAll } from 'vitest' +import { Payment } from '../../src/Payment' +import { CodeGenerator } from '../../src/utils' +import { canRunE2ETests, hasAuthenticationKeys } from '../setup' + +/** + * E2E tests for complete payment flow with Satispay + * + * These tests require: + * - SATISPAY_PUBLIC_KEY, SATISPAY_PRIVATE_KEY, SATISPAY_KEY_ID configured + * - Staging or test environment + * + * NOTE: These tests create real payments in the configured environment. + * Use only with test/staging environments. + */ + +describe.skipIf(!canRunE2ETests() || !hasAuthenticationKeys())('E2E: Payment Flow', () => { + let testPaymentId: string | undefined + + beforeAll(() => { + console.log('\nāš ļø WARNING: These tests create real payments') + console.log('Make sure you are in staging/test environment\n') + }) + + describe('Create Payment', () => { + it( + 'should create a new payment', + async () => { + const externalCode = CodeGenerator.generateExternalCode('E2E-TEST') + const amount = 1.00 // 1 euro + + const payment = await Payment.create({ + flow: 'MATCH_CODE', + amount: amount, // Using amount in euros + currency: 'EUR', + external_code: externalCode, + metadata: { + test: 'e2e-test', + timestamp: new Date().toISOString(), + }, + }) + + // Verifica la risposta + expect(payment.id).toBeTruthy() + expect(payment.amount_unit).toBe(100) // Converted to cents + expect(payment.currency).toBe('EUR') + expect(payment.external_code).toBe(externalCode) + expect(payment.status).toBe('PENDING') + + // Save ID for subsequent tests + testPaymentId = payment.id + + console.log('\nāœ“ Payment created successfully') + console.log('Payment ID:', payment.id) + console.log('External Code:', payment.external_code) + console.log('Amount:', amount, 'EUR') + console.log('Status:', payment.status) + }, + 30000 + ) + }) + + describe('Get Payment', () => { + it( + 'should retrieve an existing payment', + async () => { + if (!testPaymentId) { + console.log('āš ļø Skipped: no payment ID available from previous test') + return + } + + const payment = await Payment.get(testPaymentId) + + // Verify response + expect(payment.id).toBe(testPaymentId) + expect(payment.amount_unit).toBeTruthy() + expect(payment.currency).toBe('EUR') + expect(payment.status).toBeTruthy() + + console.log('\nāœ“ Payment retrieved successfully') + console.log('Payment ID:', payment.id) + console.log('Status:', payment.status) + }, + 30000 + ) + }) + + describe('List Payments', () => { + it( + 'should list payments with pagination', + async () => { + const result = await Payment.all({ + limit: 10, + }) + + // Verify response structure + expect(result).toBeDefined() + expect(result.data).toBeDefined() + expect(Array.isArray(result.data)).toBe(true) + expect(result.has_more).toBeDefined() + + console.log('\nšŸ“‹ Payment list response:') + console.log('Total payments found:', result.data.length) + console.log('Has more pages:', result.has_more) + + if (result.data.length > 0) { + const firstPayment = result.data[0] + expect(firstPayment.id).toBeTruthy() + expect(firstPayment.amount_unit).toBeDefined() + expect(firstPayment.currency).toBe('EUR') + expect(firstPayment.status).toBeTruthy() + + console.log('āœ“ Payment list retrieved successfully') + console.log('First payment ID:', firstPayment.id) + console.log('First payment status:', firstPayment.status) + console.log('First payment date:', firstPayment.insert_date) + } else { + console.log('āš ļø No payments found in staging environment') + console.log('šŸ’” Note: Staging environment may have limited or no historical data') + console.log('šŸ’” The payment created in this test run should appear in subsequent calls') + } + }, + 30000 + ) + + it( + 'should find the payment created in this test run', + async () => { + if (!testPaymentId) { + console.log('āš ļø Skipped: no payment ID available') + return + } + + // Try to find the payment in the list + const result = await Payment.all({ + limit: 100, // Increase limit to find recent payment + }) + + const createdPayment = result.data.find(p => p.id === testPaymentId) + + if (createdPayment) { + console.log('\nāœ“ Found the payment created in this test run') + console.log('Payment ID:', createdPayment.id) + console.log('Status:', createdPayment.status) + console.log('Amount:', createdPayment.amount_unit / 100, 'EUR') + expect(createdPayment.id).toBe(testPaymentId) + } else { + console.log('\nāš ļø Payment not found in list') + console.log('Expected payment ID:', testPaymentId) + console.log('Total payments in list:', result.data.length) + console.log('šŸ’” This might be due to API indexing delay or staging environment behavior') + } + }, + 30000 + ) + + // Note: starting_after_timestamp now supports Date objects directly + it( + 'should filter payments by date using starting_after_timestamp', + async () => { + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + + const result = await Payment.all({ + starting_after_timestamp: yesterday, // Date object is automatically converted + limit: 5, + }) + + expect(result).toBeDefined() + expect(result.data).toBeDefined() + expect(Array.isArray(result.data)).toBe(true) + + console.log('\nāœ“ Date filter applied successfully') + console.log('Payments found:', result.data.length) + console.log('Filter date:', yesterday.toISOString()) + }, + 30000 + ) + }) + + describe('Update Payment', () => { + it( + 'should update payment action', + async () => { + if (!testPaymentId) { + console.log('āš ļø Skipped: no payment ID available') + return + } + + const payment = await Payment.update(testPaymentId, { + action: 'CANCEL', + }) + + // Verify response + expect(payment.id).toBe(testPaymentId) + + console.log('\nāœ“ Payment updated') + console.log('Payment ID:', payment.id) + console.log('Status:', payment.status) + }, + 30000 + ) + }) + + describe('Payment by External Code', () => { + it( + 'should create and retrieve payment by external code', + async () => { + // Crea un nuovo pagamento con external code univoco + const externalCode = CodeGenerator.generateUuidExternalCode('E2E-SEARCH') + + const createdPayment = await Payment.create({ + flow: 'MATCH_CODE', + amount: 0.50, // 0.50 euros + currency: 'EUR', + external_code: externalCode, + }) + + // Verify it was created with correct external code + expect(createdPayment.external_code).toBe(externalCode) + expect(createdPayment.amount_unit).toBe(50) // 0.50 EUR = 50 cents + + console.log('\nāœ“ Payment created and found by external code') + console.log('External Code:', createdPayment.external_code) + console.log('Payment ID:', createdPayment.id) + }, + 30000 + ) + }) +}) diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..76b1c3a --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,63 @@ +import { beforeAll } from 'vitest' +import { Api } from '../src/Api' +import { config } from 'dotenv' + +// Load environment variables from .env.local (if exists) +// dotenv automatically converts \n to real newlines +config({ path: '.env.local', override: true, quiet: true }) + +/** + * Setup for E2E tests + * Loads environment variables needed for integration tests + * + * NOTE: E2E tests require pre-configured keys and run ONLY in staging environment + */ +beforeAll(() => { + // Check that environment variables are available for E2E tests + const hasE2EConfig = + process.env.SATISPAY_PUBLIC_KEY && + process.env.SATISPAY_PRIVATE_KEY && + process.env.SATISPAY_KEY_ID + + if (hasE2EConfig) { + // Force staging environment for safety + Api.setEnv('staging') + + // Configure keys if available + if (process.env.SATISPAY_PUBLIC_KEY) { + Api.setPublicKey(process.env.SATISPAY_PUBLIC_KEY) + } + + if (process.env.SATISPAY_PRIVATE_KEY) { + Api.setPrivateKey(process.env.SATISPAY_PRIVATE_KEY) + } + + if (process.env.SATISPAY_KEY_ID) { + Api.setKeyId(process.env.SATISPAY_KEY_ID) + } + + return + } + + console.log('⚠ E2E configuration not found, E2E tests will be skipped') +}) + +/** + * Helper to check if E2E tests can be executed + * Requires all three keys to be configured + */ +export function canRunE2ETests(): boolean { + return !!( + process.env.SATISPAY_PUBLIC_KEY && + process.env.SATISPAY_PRIVATE_KEY && + process.env.SATISPAY_KEY_ID + ) +} + +/** + * Helper to check if keys are configured + * (Alias of canRunE2ETests for backwards compatibility) + */ +export function hasAuthenticationKeys(): boolean { + return canRunE2ETests() +} diff --git a/tests/utils.test.ts b/tests/utils.test.ts new file mode 100644 index 0000000..5d6941a --- /dev/null +++ b/tests/utils.test.ts @@ -0,0 +1,345 @@ +import { describe, it, expect } from 'vitest' +import { + Amount, + DateUtils, + Validation, + CodeGenerator, + PaymentStatusUtils, +} from '../src/utils' + +describe('Amount', () => { + describe('toCents', () => { + it('should convert euros to cents', () => { + expect(Amount.toCents(10.5)).toBe(1050) + expect(Amount.toCents(1)).toBe(100) + expect(Amount.toCents(0.01)).toBe(1) + }) + + it('should round to nearest cent', () => { + expect(Amount.toCents(10.505)).toBe(1051) + expect(Amount.toCents(10.504)).toBe(1050) + }) + }) + + describe('toEuros', () => { + it('should convert cents to euros', () => { + expect(Amount.toEuros(1050)).toBe(10.5) + expect(Amount.toEuros(100)).toBe(1) + expect(Amount.toEuros(1)).toBe(0.01) + }) + }) + + describe('format', () => { + it('should format amount with default locale', () => { + const formatted = Amount.format(1050) + expect(formatted).toContain('10') + expect(formatted).toContain('50') + }) + + it('should format amount with custom locale', () => { + const formatted = Amount.format(1050, 'en-US') + expect(formatted).toContain('10') + expect(formatted).toContain('50') + }) + }) + + describe('parse', () => { + it('should parse formatted amount', () => { + expect(Amount.parse('10,50 €')).toBe(1050) + expect(Amount.parse('10.50')).toBe(1050) + expect(Amount.parse('1,00')).toBe(100) + }) + + it('should throw error on invalid format', () => { + expect(() => Amount.parse('invalid')).toThrow('Invalid amount format') + }) + }) + + describe('isValid', () => { + it('should validate positive integers', () => { + expect(Amount.isValid(100)).toBe(true) + expect(Amount.isValid(1)).toBe(true) + }) + + it('should reject invalid amounts', () => { + expect(Amount.isValid(0)).toBe(false) + expect(Amount.isValid(-100)).toBe(false) + expect(Amount.isValid(10.5)).toBe(false) + }) + }) +}) + +describe('DateUtils', () => { + describe('formatForApi', () => { + it('should format Date object', () => { + const date = new Date('2024-01-15T10:30:00Z') + expect(DateUtils.formatForApi(date)).toBe('2024-01-15') + }) + + it('should format ISO string', () => { + expect(DateUtils.formatForApi('2024-01-15T10:30:00Z')).toBe('2024-01-15') + }) + }) + + describe('parseFromApi', () => { + it('should parse date string', () => { + const date = DateUtils.parseFromApi('2024-01-15T10:30:00Z') + expect(date).toBeInstanceOf(Date) + expect(date.getFullYear()).toBe(2024) + expect(date.getMonth()).toBe(0) + expect(date.getDate()).toBe(15) + }) + }) + + describe('getDailyClosureRange', () => { + it('should return start and end of day', () => { + const date = new Date('2024-01-15T10:30:00Z') + const range = DateUtils.getDailyClosureRange(date) + + expect(range.start.getHours()).toBe(0) + expect(range.start.getMinutes()).toBe(0) + expect(range.start.getSeconds()).toBe(0) + + expect(range.end.getHours()).toBe(23) + expect(range.end.getMinutes()).toBe(59) + expect(range.end.getSeconds()).toBe(59) + }) + }) + + describe('getToday', () => { + it('should return today at midnight', () => { + const today = DateUtils.getToday() + expect(today.getHours()).toBe(0) + expect(today.getMinutes()).toBe(0) + expect(today.getSeconds()).toBe(0) + }) + }) + + describe('getYesterday', () => { + it('should return yesterday at midnight', () => { + const yesterday = DateUtils.getYesterday() + const expectedDate = new Date() + expectedDate.setDate(expectedDate.getDate() - 1) + expectedDate.setHours(0, 0, 0, 0) + + expect(yesterday.getDate()).toBe(expectedDate.getDate()) + expect(yesterday.getHours()).toBe(0) + }) + }) + + describe('isToday', () => { + it('should check if date is today', () => { + const today = new Date() + expect(DateUtils.isToday(today)).toBe(true) + + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + expect(DateUtils.isToday(yesterday)).toBe(false) + }) + }) + + describe('format', () => { + it('should format date with default locale', () => { + const date = new Date('2024-01-15T10:30:00Z') + const formatted = DateUtils.format(date) + expect(formatted).toBeTruthy() + }) + + it('should format date with custom locale', () => { + const date = new Date('2024-01-15T10:30:00Z') + const formatted = DateUtils.format(date, 'en-US') + expect(formatted).toContain('January') + }) + }) + + describe('formatDateTime', () => { + it('should format date and time', () => { + const date = new Date('2024-01-15T10:30:00Z') + const formatted = DateUtils.formatDateTime(date) + expect(formatted).toBeTruthy() + }) + }) +}) + +describe('Validation', () => { + describe('validateExternalCode', () => { + it('should validate correct external codes', () => { + expect(Validation.validateExternalCode('ORDER-123')).toBe(true) + expect(Validation.validateExternalCode('test_code')).toBe(true) + expect(Validation.validateExternalCode('ABC123')).toBe(true) + }) + + it('should reject empty codes', () => { + expect(Validation.validateExternalCode('')).toContain('cannot be empty') + expect(Validation.validateExternalCode(' ')).toContain('cannot be empty') + }) + + it('should reject too long codes', () => { + const longCode = 'a'.repeat(51) + expect(Validation.validateExternalCode(longCode)).toContain('50 characters') + }) + + it('should reject invalid characters', () => { + expect(Validation.validateExternalCode('code with spaces')).toContain( + 'letters, numbers' + ) + expect(Validation.validateExternalCode('code@invalid')).toContain( + 'letters, numbers' + ) + }) + }) + + describe('validateFlow', () => { + it('should validate correct flows', () => { + expect(Validation.validateFlow('MATCH_CODE')).toBe(true) + expect(Validation.validateFlow('MATCH_USER')).toBe(true) + expect(Validation.validateFlow('REFUND')).toBe(true) + }) + + it('should reject invalid flows', () => { + expect(Validation.validateFlow('INVALID')).toBe(false) + expect(Validation.validateFlow('')).toBe(false) + }) + }) + + describe('validateCurrency', () => { + it('should validate EUR', () => { + expect(Validation.validateCurrency('EUR')).toBe(true) + }) + + it('should reject other currencies', () => { + expect(Validation.validateCurrency('USD')).toBe(false) + expect(Validation.validateCurrency('GBP')).toBe(false) + }) + }) + + describe('validatePhone', () => { + it('should validate Italian phone numbers', () => { + expect(Validation.validatePhone('+393331234567')).toBe(true) + expect(Validation.validatePhone('+39 333 123 4567')).toBe(true) + }) + + it('should reject invalid phone numbers', () => { + expect(Validation.validatePhone('123456789')).toContain('Invalid') + expect(Validation.validatePhone('+1234567890')).toContain('Invalid') + }) + }) + + describe('validateMetadata', () => { + it('should validate correct metadata', () => { + expect(Validation.validateMetadata({ key: 'value' })).toBe(true) + expect(Validation.validateMetadata({ a: 1, b: 'test' })).toBe(true) + }) + + it('should reject non-object metadata', () => { + expect(Validation.validateMetadata(null as any)).toContain('must be an object') + expect(Validation.validateMetadata('string' as any)).toContain('must be an object') + }) + + it('should reject too large metadata', () => { + const largeMetadata = { data: 'a'.repeat(1000) } + expect(Validation.validateMetadata(largeMetadata)).toContain('1000 characters') + }) + }) +}) + +describe('CodeGenerator', () => { + describe('generateExternalCode', () => { + it('should generate code with timestamp', () => { + const code = CodeGenerator.generateExternalCode() + expect(code).toMatch(/^ORDER-\d+$/) + }) + + it('should use custom prefix', () => { + const code = CodeGenerator.generateExternalCode('PAYMENT') + expect(code).toMatch(/^PAYMENT-\d+$/) + }) + }) + + describe('generateRandomExternalCode', () => { + it('should generate code with random suffix', () => { + const code = CodeGenerator.generateRandomExternalCode() + expect(code).toMatch(/^ORDER-[a-z0-9]+$/) + }) + + it('should use custom prefix', () => { + const code = CodeGenerator.generateRandomExternalCode('TEST') + expect(code).toMatch(/^TEST-[a-z0-9]+$/) + }) + + it('should generate unique codes', () => { + const code1 = CodeGenerator.generateRandomExternalCode() + const code2 = CodeGenerator.generateRandomExternalCode() + expect(code1).not.toBe(code2) + }) + }) + + describe('generateUuidExternalCode', () => { + it('should generate UUID code', () => { + const code = CodeGenerator.generateUuidExternalCode() + expect(code).toMatch(/^ORDER-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + }) + + it('should use custom prefix', () => { + const code = CodeGenerator.generateUuidExternalCode('INVOICE') + expect(code).toMatch(/^INVOICE-[0-9a-f]{8}-/) + }) + + it('should generate unique codes', () => { + const code1 = CodeGenerator.generateUuidExternalCode() + const code2 = CodeGenerator.generateUuidExternalCode() + expect(code1).not.toBe(code2) + }) + }) +}) + +describe('PaymentStatusUtils', () => { + describe('status checkers', () => { + it('should check if payment is pending', () => { + expect(PaymentStatusUtils.isPending('PENDING')).toBe(true) + expect(PaymentStatusUtils.isPending('ACCEPTED')).toBe(false) + }) + + it('should check if payment is accepted', () => { + expect(PaymentStatusUtils.isAccepted('ACCEPTED')).toBe(true) + expect(PaymentStatusUtils.isAccepted('PENDING')).toBe(false) + }) + + it('should check if payment is canceled', () => { + expect(PaymentStatusUtils.isCanceled('CANCELED')).toBe(true) + expect(PaymentStatusUtils.isCanceled('PENDING')).toBe(false) + }) + + it('should check if payment is expired', () => { + expect(PaymentStatusUtils.isExpired('EXPIRED')).toBe(true) + expect(PaymentStatusUtils.isExpired('PENDING')).toBe(false) + }) + + it('should check if payment is final', () => { + expect(PaymentStatusUtils.isFinal('ACCEPTED')).toBe(true) + expect(PaymentStatusUtils.isFinal('CANCELED')).toBe(true) + expect(PaymentStatusUtils.isFinal('EXPIRED')).toBe(true) + expect(PaymentStatusUtils.isFinal('PENDING')).toBe(false) + }) + }) + + describe('getLabel', () => { + it('should return Italian labels by default', () => { + expect(PaymentStatusUtils.getLabel('PENDING')).toBe('In attesa') + expect(PaymentStatusUtils.getLabel('ACCEPTED')).toBe('Accettato') + expect(PaymentStatusUtils.getLabel('CANCELED')).toBe('Annullato') + expect(PaymentStatusUtils.getLabel('EXPIRED')).toBe('Scaduto') + }) + + it('should return English labels', () => { + expect(PaymentStatusUtils.getLabel('PENDING', 'en-US')).toBe('Pending') + expect(PaymentStatusUtils.getLabel('ACCEPTED', 'en-US')).toBe('Accepted') + expect(PaymentStatusUtils.getLabel('CANCELED', 'en-US')).toBe('Canceled') + expect(PaymentStatusUtils.getLabel('EXPIRED', 'en-US')).toBe('Expired') + }) + + it('should return status as-is for unknown status', () => { + expect(PaymentStatusUtils.getLabel('UNKNOWN')).toBe('UNKNOWN') + }) + }) +}) diff --git a/vitest.config.ts b/vitest.config.ts index e2968f2..5281984 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,9 +6,10 @@ export default defineConfig({ globals: true, environment: 'node', include: ['tests/**/*.test.ts'], + setupFiles: ['./tests/setup.ts'], coverage: { provider: 'v8', - reporter: ['text', 'json', 'html'], + reporter: ['text', 'json', 'html', 'lcov'], include: ['src/**/*.ts'], exclude: [ 'node_modules/', @@ -17,7 +18,16 @@ export default defineConfig({ 'tests/', '**/*.config.*', '**/*.d.ts', + 'src/index.ts', + 'src/types.ts', + 'src/bin/**', ], + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, }, }, resolve: { From 00747584a615def5f4954acc9eb61ddc99190938 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Tue, 2 Dec 2025 00:03:38 +0100 Subject: [PATCH 2/3] Upload workflows --- .github/workflows/build.yml | 36 ++++++++++++ .github/workflows/ci.yml | 86 ---------------------------- .github/workflows/main.yml | 57 ++++++++++++++++++ .github/workflows/pr-check-suite.yml | 26 +++++++++ .github/workflows/release-tag.yml | 26 +++++++++ .github/workflows/release.yml | 61 -------------------- .github/workflows/sonarcloud.yml | 22 +++++++ .github/workflows/test.yml | 49 ++++++++++++++++ 8 files changed, 216 insertions(+), 147 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pr-check-suite.yml create mode 100644 .github/workflows/release-tag.yml delete mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/sonarcloud.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..468fcbb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,36 @@ +name: Build library + +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Build release + run: pnpm build + + - name: Bump version with release tag name + run: pnpm version --no-git-tag-version ${{ github.event.release.tag_name }} + + - name: Pack package + run: pnpm pack + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: package + path: 'volverjs-satispay-node-sdk-*.tgz' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index ac8e93b..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: CI - -on: - push: - branches: [main, develop] - pull_request: - branches: [main, develop] - -jobs: - test: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [18.x, 20.x, 22.x] - - steps: - - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Lint - run: pnpm lint - - - name: Build - run: pnpm build - - - name: Test - run: pnpm test - env: - SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} - SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} - SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} - - - name: Test Coverage - run: pnpm test:coverage - env: - SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} - SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} - SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x' - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/coverage-final.json - flags: unittests - name: codecov-umbrella - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Check formatting - run: pnpm format --check - - - name: Lint - run: pnpm lint diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..11af7e7 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,57 @@ +name: Main pipeline + +on: + # Runs on release publish + release: + types: [published] + +jobs: + analysis: + uses: ./.github/workflows/sonarcloud.yml + secrets: inherit + + build: + uses: ./.github/workflows/build.yml + + test: + needs: build + uses: ./.github/workflows/test.yml + + publish-npm: + needs: [test, analysis] + runs-on: ubuntu-latest + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: package + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + + - run: npm publish $(ls *.tgz) --access=public --tag ${{ github.event.release.prerelease && 'next' || 'latest'}} + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} + + publish-gpr: + needs: [test, analysis] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: package + + - uses: actions/setup-node@v4 + with: + node-version: 20 + registry-url: https://npm.pkg.github.com/ + + - run: npm publish $(ls *.tgz) --access=public --tag ${{ github.event.release.prerelease && 'next' || 'latest'}} + env: + NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/.github/workflows/pr-check-suite.yml b/.github/workflows/pr-check-suite.yml new file mode 100644 index 0000000..2328813 --- /dev/null +++ b/.github/workflows/pr-check-suite.yml @@ -0,0 +1,26 @@ +name: Check PR + +on: + # Run on pull request + pull_request: + branches: [main, develop] + +# Sets permissions of the GITHUB_TOKEN +permissions: + contents: write + pages: write + id-token: write + pull-requests: write + +jobs: + analysis: + uses: ./.github/workflows/sonarcloud.yml + secrets: inherit + + build: + uses: ./.github/workflows/build.yml + + test: + needs: build + uses: ./.github/workflows/test.yml + diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml new file mode 100644 index 0000000..f31b94a --- /dev/null +++ b/.github/workflows/release-tag.yml @@ -0,0 +1,26 @@ +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 + +name: Create Release + +jobs: + build: + permissions: + contents: write # to create release (yyx990803/release-tag) + + name: Create Release + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@master + - name: Create Release for Tag + id: release_tag + uses: yyx990803/release-tag@master + env: + GITHUB_TOKEN: ${{ secrets.RELEASE_TAG_GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + body: | + Please refer to [CHANGELOG.md](https://github.com/volverjs/satispay-node-sdk/blob/${{ contains(github.ref, 'beta') && 'develop' || 'main'}}/CHANGELOG.md) for details. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 47a6a67..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - cache: 'pnpm' - registry-url: 'https://registry.npmjs.org' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm build - - - name: Test - run: pnpm test - env: - SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} - SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} - SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} - - - name: Test Coverage - run: pnpm test:coverage - env: - SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} - SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} - SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} - - - name: Publish to npm - run: pnpm publish --access public --no-git-checks - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - generate_release_notes: true - draft: false - prerelease: false diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..82eee80 --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,22 @@ +name: SonarCloud analysis + +on: + workflow_call: + +jobs: + sonarcloud: + name: SonarCloud + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + if: env.SONAR_TOKEN + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: SonarCloud Scan + if: env.SONAR_TOKEN + uses: SonarSource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0d71cce --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,49 @@ +name: Run library test + +on: + workflow_call: + +jobs: + vitest: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Test + run: pnpm test + env: + SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} + SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} + SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} + + - name: Test Coverage + run: pnpm test:coverage + env: + SATISPAY_PUBLIC_KEY: ${{ secrets.SATISPAY_PUBLIC_KEY }} + SATISPAY_PRIVATE_KEY: ${{ secrets.SATISPAY_PRIVATE_KEY }} + SATISPAY_KEY_ID: ${{ secrets.SATISPAY_KEY_ID }} + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20.x' + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/coverage-final.json + flags: unittests + name: codecov-umbrella \ No newline at end of file From 847971630fbbe08ca9ca42507f8d192ec8122b40 Mon Sep 17 00:00:00 2001 From: Alessandro Bellesia Date: Tue, 2 Dec 2025 00:06:40 +0100 Subject: [PATCH 3/3] Add pnpm version --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 7bb5021..b558ac5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@volverjs/satispay-node-sdk", "version": "0.0.1", + "packageManager": "pnpm@10.24.0", "description": "(Unofficial) Satispay GBusiness Node.js API SDK", "type": "module", "main": "dist/index.js",