diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 199e585..11e132c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,12 +37,11 @@ jobs: - name: Lint code (Biome) run: bun run biome:check + - name: Build packages + run: bun run build + - name: Type check run: bunx tsc -b --noEmit - name: Run tests run: bun run test - - # Skip this for now - # - name: Build packages - # run: bun run build diff --git a/bun.lock b/bun.lock index ea5938c..c4bf343 100644 --- a/bun.lock +++ b/bun.lock @@ -14,7 +14,7 @@ }, "packages/adapters": { "name": "@abacatepay/adapters", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@abacatepay/zod": "^1.0.0", }, @@ -36,7 +36,7 @@ }, "packages/elysia": { "name": "@abacatepay/elysia", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@abacatepay/adapters": "^1.0.1", }, @@ -50,7 +50,7 @@ }, "packages/eslint-plugin": { "name": "@abacatepay/eslint-plugin", - "version": "0.1.0", + "version": "0.1.2", "devDependencies": { "@types/eslint": "^9.6.1", "typescript": "^5", @@ -61,7 +61,7 @@ }, "packages/express": { "name": "@abacatepay/express", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { "@abacatepay/adapters": "^1.0.1", }, @@ -76,7 +76,7 @@ }, "packages/fastify": { "name": "@abacatepay/fastify", - "version": "1.0.2", + "version": "1.0.3", "dependencies": { "@abacatepay/adapters": "^1.0.1", }, @@ -90,7 +90,7 @@ }, "packages/hono": { "name": "@abacatepay/hono", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@abacatepay/adapters": "^1.0.1", }, @@ -104,7 +104,7 @@ }, "packages/rest": { "name": "@abacatepay/rest", - "version": "0.0.1", + "version": "0.0.3", "devDependencies": { "@biomejs/biome": "^2.3.11", "typescript": "^5.0.0", @@ -112,10 +112,10 @@ }, "packages/sdk": { "name": "@abacatepay/sdk", - "version": "1.0.0", + "version": "1.2.0", "dependencies": { - "@abacatepay/rest": "^0.0.1", - "@abacatepay/types": "^2.0.1", + "@abacatepay/rest": "^0.0.3", + "@abacatepay/types": "^2.0.3", }, "devDependencies": { "@biomejs/biome": "^2.3.11", @@ -124,7 +124,7 @@ }, "packages/supabase": { "name": "@abacatepay/supabase", - "version": "1.0.2", + "version": "1.0.3", "dependencies": { "@abacatepay/adapters": "^1.0.1", }, @@ -135,7 +135,7 @@ }, "packages/typebox": { "name": "@abacatepay/typebox", - "version": "1.0.0", + "version": "1.0.1", "devDependencies": { "@biomejs/biome": "^2.3.11", "typescript": "^5.0.0", @@ -147,7 +147,7 @@ }, "packages/types": { "name": "@abacatepay/types", - "version": "2.0.1", + "version": "2.0.3", "devDependencies": { "@biomejs/biome": "^2.3.8", "typescript": "^5.9.3", @@ -155,7 +155,7 @@ }, "packages/zod": { "name": "@abacatepay/zod", - "version": "1.0.0", + "version": "1.0.1", "devDependencies": { "@biomejs/biome": "^2.3.11", "typescript": "^5.0.0", @@ -833,8 +833,14 @@ "@abacatepay/hono/@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], + "@abacatepay/rest/@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], + + "@abacatepay/sdk/@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], + "@abacatepay/supabase/@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], + "@abacatepay/types/@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], + "@abacatepay/zod/@biomejs/biome": ["@biomejs/biome@2.3.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.12", "@biomejs/cli-darwin-x64": "2.3.12", "@biomejs/cli-linux-arm64": "2.3.12", "@biomejs/cli-linux-arm64-musl": "2.3.12", "@biomejs/cli-linux-x64": "2.3.12", "@biomejs/cli-linux-x64-musl": "2.3.12", "@biomejs/cli-win32-arm64": "2.3.12", "@biomejs/cli-win32-x64": "2.3.12" }, "bin": { "biome": "bin/biome" } }, "sha512-AR7h4aSlAvXj7TAajW/V12BOw2EiS0AqZWV5dGozf4nlLoUF/ifvD0+YgKSskT0ylA6dY1A8AwgP8kZ6yaCQnA=="], "@commitlint/config-validator/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -959,6 +965,38 @@ "@abacatepay/hono/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="], + + "@abacatepay/rest/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="], + + "@abacatepay/sdk/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], + "@abacatepay/supabase/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], "@abacatepay/supabase/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], @@ -975,6 +1013,22 @@ "@abacatepay/supabase/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], + "@abacatepay/types/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-nbOsuQROa3DLla5vvsTZg+T5WVPGi9/vYxETm9BOuLHBJN3oWQIg3MIkE2OfL18df1ZtNkqXkH6Yg9mdTPem7A=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-aqkeSf7IH+wkzFpKeDVPSXy9uDjxtLpYA6yzkYsY+tVjwFFirSuajHDI3ul8en90XNs1NA0n8kgBrjwRi5JeyA=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-CQtqrJ+qEEI8tgRSTjjzk6wJAwfH3wQlkIGsM5dlecfRZaoT+XCms/mf7G4kWNexrke6mnkRzNy6w8ebV177ow=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.12", "", { "os": "linux", "cpu": "x64" }, "sha512-kVGWtupRRsOjvw47YFkk5mLiAdpCPMWBo1jOwAzh+juDpUb2sWarIp+iq+CPL1Wt0LLZnYtP7hH5kD6fskcxmg=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-Re4I7UnOoyE4kHMqpgtG6UvSBGBbbtvsOvBROgCCoH7EgANN6plSQhvo2W7OCITvTp7gD6oZOyZy72lUdXjqZg=="], + + "@abacatepay/types/@biomejs/biome/@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.12", "", { "os": "win32", "cpu": "x64" }, "sha512-qqGVWqNNek0KikwPZlOIoxtXgsNGsX+rgdEzgw82Re8nF02W+E2WokaQhpF5TdBh/D/RQ3TLppH+otp6ztN0lw=="], + "@abacatepay/zod/@biomejs/biome/@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cO6fn+KiMBemva6EARDLQBxeyvLzgidaFRJi8G7OeRqz54kWK0E+uSjgFaiHlc3DZYoa0+1UFE8mDxozpc9ieg=="], "@abacatepay/zod/@biomejs/biome/@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-/fiF/qmudKwSdvmSrSe/gOTkW77mHHkH8Iy7YC2rmpLuk27kbaUOPa7kPiH5l+3lJzTUfU/t6x1OuIq/7SGtxg=="], diff --git a/packages/adapters/README.md b/packages/adapters/README.md index 5af6913..5b7b909 100644 --- a/packages/adapters/README.md +++ b/packages/adapters/README.md @@ -74,11 +74,11 @@ if (!parsed.success) { throw parsed.error; }; -await dispatch(parsed, { +await dispatch(parsed.data, { onPayload({ event }) { console.log('Evento recebido:', event); }, - onPayoutDone({ data }) { + onPayoutCompleted({ data }) { console.log(data.transaction.id); }, }); diff --git a/packages/adapters/package.json b/packages/adapters/package.json index 9b47177..59888fa 100644 --- a/packages/adapters/package.json +++ b/packages/adapters/package.json @@ -1,65 +1,65 @@ { - "name": "@abacatepay/adapters", - "version": "1.0.2", - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "main": "./dist/index.js", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "exports": { - "./webhooks": { - "import": "./dist/webhooks/index.js", - "types": "./dist/webhooks/index.d.ts" - }, - "./webhooks/*": { - "import": "./dist/webhooks/*.js", - "types": "./dist/webhooks/*.d.ts" - } - }, - "description": "Checkout and webhook adapters for AbacatePay.", - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "files": [ - "dist" - ], - "keywords": [ - "abacatepay", - "payments", - "webhooks", - "checkouts", - "adapters" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "sideEffects": false, - "type": "module", - "types": "./dist/index.d.ts", - "dependencies": { - "@abacatepay/zod": "^1.0.0" - } + "name": "@abacatepay/adapters", + "version": "2.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "main": "./dist/index.js", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "exports": { + "./webhooks": { + "import": "./dist/webhooks/index.js", + "types": "./dist/webhooks/index.d.ts" + }, + "./webhooks/*": { + "import": "./dist/webhooks/*.js", + "types": "./dist/webhooks/*.d.ts" + } + }, + "description": "Checkout and webhook adapters for AbacatePay.", + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "files": [ + "dist" + ], + "keywords": [ + "abacatepay", + "payments", + "webhooks", + "checkouts", + "adapters" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "dependencies": { + "@abacatepay/zod": "^2.0.0" + } } diff --git a/packages/adapters/src/version.ts b/packages/adapters/src/version.ts index aa64d56..5d2fafb 100644 --- a/packages/adapters/src/version.ts +++ b/packages/adapters/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/adapters](https://www.npmjs.com/package/@abacatepay/adapters). */ -export const version = '1.0.2' as const; +export const version = '2.0.0' as const; diff --git a/packages/adapters/src/webhooks/dispatch.ts b/packages/adapters/src/webhooks/dispatch.ts index 0b2b65c..7c86547 100644 --- a/packages/adapters/src/webhooks/dispatch.ts +++ b/packages/adapters/src/webhooks/dispatch.ts @@ -1,16 +1,41 @@ import type { WebhookEvent } from '@abacatepay/zod/v2'; import type { WebhookOptions } from './types'; -export const dispatch = ( - data: WebhookEvent, - { onBillingPaid, onPayload, onPayoutDone, onPayoutFailed }: WebhookOptions, -) => { +export const dispatch = (data: WebhookEvent, options: WebhookOptions) => { + const { onPayload } = options; + switch (data.event) { - case 'billing.paid': - return (onBillingPaid ?? onPayload)?.(data); - case 'payout.done': - return (onPayoutDone ?? onPayload)?.(data); + case 'payout.completed': + return (options.onPayoutCompleted ?? onPayload)?.(data); case 'payout.failed': - return (onPayoutFailed ?? onPayload)?.(data); + return (options.onPayoutFailed ?? onPayload)?.(data); + case 'checkout.completed': + return (options.onCheckoutCompleted ?? onPayload)?.(data); + case 'transparent.completed': + return (options.onTransparentCompleted ?? onPayload)?.(data); + case 'checkout.refunded': + return (options.onCheckoutRefunded ?? onPayload)?.(data); + case 'checkout.disputed': + return (options.onCheckoutDisputed ?? onPayload)?.(data); + case 'checkout.lost': + return (options.onCheckoutLost ?? onPayload)?.(data); + case 'transparent.refunded': + return (options.onTransparentRefunded ?? onPayload)?.(data); + case 'transparent.disputed': + return (options.onTransparentDisputed ?? onPayload)?.(data); + case 'transparent.lost': + return (options.onTransparentLost ?? onPayload)?.(data); + case 'subscription.completed': + return (options.onSubscriptionCompleted ?? onPayload)?.(data); + case 'subscription.cancelled': + return (options.onSubscriptionCancelled ?? onPayload)?.(data); + case 'subscription.renewed': + return (options.onSubscriptionRenewed ?? onPayload)?.(data); + case 'subscription.trial_started': + return (options.onSubscriptionTrialStarted ?? onPayload)?.(data); + case 'transfer.completed': + return (options.onTransferCompleted ?? onPayload)?.(data); + case 'transfer.failed': + return (options.onTransferFailed ?? onPayload)?.(data); } }; diff --git a/packages/adapters/src/webhooks/types.ts b/packages/adapters/src/webhooks/types.ts index 60f5c01..8462d7d 100644 --- a/packages/adapters/src/webhooks/types.ts +++ b/packages/adapters/src/webhooks/types.ts @@ -1,10 +1,24 @@ import type { - WebhookBillingPaidEvent, + WebhookCheckoutCompletedEvent, WebhookEvent, - WebhookPayoutDoneEvent, + WebhookEventType, + WebhookPayoutCompletedEvent, WebhookPayoutFailedEvent, + WebhookTransparentCompletedEvent, + WebhookUndocumentedEvent, } from '@abacatepay/zod/v2'; +/** + * Narrows the generic `WebhookUndocumentedEvent` payload to a single event + * type, for events whose `data` shape AbacatePay hasn't documented yet. + * + * @unstable + */ +type UndocumentedEventPayload = Omit< + WebhookUndocumentedEvent, + 'event' +> & { event: Type }; + /** * Options to use in Webhooks */ @@ -18,15 +32,111 @@ export interface WebhookOptions { */ onPayload?(data: WebhookEvent): unknown; /** - * Function to execute when a `payout.done` event is trigerred + * Function to execute when a `payout.completed` event is triggered + */ + onPayoutCompleted?(data: WebhookPayoutCompletedEvent): unknown; + /** + * Function to execute when a `payout.failed` event is triggered */ - onPayoutDone?(data: WebhookPayoutDoneEvent): unknown; + onPayoutFailed?(data: WebhookPayoutFailedEvent): unknown; /** - * Function to execute when a `billind.paid` event is trigerred + * Function to execute when a `checkout.completed` event is triggered */ - onBillingPaid?(data: WebhookBillingPaidEvent): unknown; + onCheckoutCompleted?(data: WebhookCheckoutCompletedEvent): unknown; /** - * Function to execute when a `payout.failed` event is trigerred + * Function to execute when a `transparent.completed` event is triggered */ - onPayoutFailed?(data: WebhookPayoutFailedEvent): unknown; + onTransparentCompleted?(data: WebhookTransparentCompletedEvent): unknown; + /** + * Function to execute when a `checkout.refunded` event is triggered + * + * @unstable + */ + onCheckoutRefunded?( + data: UndocumentedEventPayload<'checkout.refunded'>, + ): unknown; + /** + * Function to execute when a `checkout.disputed` event is triggered + * + * @unstable + */ + onCheckoutDisputed?( + data: UndocumentedEventPayload<'checkout.disputed'>, + ): unknown; + /** + * Function to execute when a `checkout.lost` event is triggered + * + * @unstable + */ + onCheckoutLost?(data: UndocumentedEventPayload<'checkout.lost'>): unknown; + /** + * Function to execute when a `transparent.refunded` event is triggered + * + * @unstable + */ + onTransparentRefunded?( + data: UndocumentedEventPayload<'transparent.refunded'>, + ): unknown; + /** + * Function to execute when a `transparent.disputed` event is triggered + * + * @unstable + */ + onTransparentDisputed?( + data: UndocumentedEventPayload<'transparent.disputed'>, + ): unknown; + /** + * Function to execute when a `transparent.lost` event is triggered + * + * @unstable + */ + onTransparentLost?( + data: UndocumentedEventPayload<'transparent.lost'>, + ): unknown; + /** + * Function to execute when a `subscription.completed` event is triggered + * + * @unstable + */ + onSubscriptionCompleted?( + data: UndocumentedEventPayload<'subscription.completed'>, + ): unknown; + /** + * Function to execute when a `subscription.cancelled` event is triggered + * + * @unstable + */ + onSubscriptionCancelled?( + data: UndocumentedEventPayload<'subscription.cancelled'>, + ): unknown; + /** + * Function to execute when a `subscription.renewed` event is triggered + * + * @unstable + */ + onSubscriptionRenewed?( + data: UndocumentedEventPayload<'subscription.renewed'>, + ): unknown; + /** + * Function to execute when a `subscription.trial_started` event is triggered + * + * @unstable + */ + onSubscriptionTrialStarted?( + data: UndocumentedEventPayload<'subscription.trial_started'>, + ): unknown; + /** + * Function to execute when a `transfer.completed` event is triggered + * + * @unstable + */ + onTransferCompleted?( + data: UndocumentedEventPayload<'transfer.completed'>, + ): unknown; + /** + * Function to execute when a `transfer.failed` event is triggered + * + * @unstable + */ + onTransferFailed?(data: UndocumentedEventPayload<'transfer.failed'>): unknown; } diff --git a/packages/adapters/test/index.test.ts b/packages/adapters/test/index.test.ts index e69de29..859a96f 100644 --- a/packages/adapters/test/index.test.ts +++ b/packages/adapters/test/index.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, test } from 'bun:test'; +import { dispatch, parse } from '../src/webhooks'; + +const basePayout = { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date(), + updatedAt: new Date(), +}; + +describe('dispatch', () => { + test('routes payout.completed to onPayoutCompleted, not onPayload', () => { + const event = parse({ + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { transaction: { ...basePayout, status: 'COMPLETE' } }, + }); + + expect(event.success).toBe(true); + if (!event.success) return; + + let onPayoutCompletedCalled = false; + let onPayloadCalled = false; + + dispatch(event.data, { + secret: 'whsec_test', + onPayoutCompleted: () => { + onPayoutCompletedCalled = true; + }, + onPayload: () => { + onPayloadCalled = true; + }, + }); + + expect(onPayoutCompletedCalled).toBe(true); + expect(onPayloadCalled).toBe(false); + }); + + test('falls back to onPayload when a specific handler is not provided', () => { + const event = parse({ + id: 'log_2', + event: 'payout.failed', + devMode: false, + data: { transaction: { ...basePayout, status: 'CANCELLED' } }, + }); + + expect(event.success).toBe(true); + if (!event.success) return; + + let onPayloadCalled = false; + + dispatch(event.data, { + secret: 'whsec_test', + onPayload: () => { + onPayloadCalled = true; + }, + }); + + expect(onPayloadCalled).toBe(true); + }); + + test('routes an undocumented event type (e.g. subscription.renewed) by its own handler', () => { + const event = parse({ + id: 'log_3', + event: 'subscription.renewed', + devMode: false, + data: { subscriptionId: 'subs_123' }, + }); + + expect(event.success).toBe(true); + if (!event.success) return; + + let onSubscriptionRenewedCalled = false; + + dispatch(event.data, { + secret: 'whsec_test', + onSubscriptionRenewed: () => { + onSubscriptionRenewedCalled = true; + }, + }); + + expect(onSubscriptionRenewedCalled).toBe(true); + }); +}); diff --git a/packages/adapters/tsconfig.json b/packages/adapters/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/adapters/tsconfig.json +++ b/packages/adapters/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/elysia/README.md b/packages/elysia/README.md index 5827c29..95dc79c 100644 --- a/packages/elysia/README.md +++ b/packages/elysia/README.md @@ -38,19 +38,22 @@ import { Webhooks } from '@abacatepay/elysia'; const app = new Elysia(); -app.post( - '/webhooks/abacatepay', - Webhooks({ - secret: '...', - onPayload(payload) { - console.log('Evento recebido:', payload.event); - }, - }), -); +const { ok, handler, error } = Webhooks({ + secret: '...', + onPayload(payload) { + console.log('Evento recebido:', payload.event); + }, +}); + +if (!ok) throw new Error(error); + +app.post('/webhooks/abacatepay', handler); ```
+`Webhooks(...)` nunca lança exceção — se o `secret` estiver ausente, ela retorna `{ ok: false, error }` em vez de `{ ok: true, handler }`. + ## Segurança por padrão
@@ -71,11 +74,12 @@ Você pode lidar com eventos específicos sem boilerplate: ```ts -Webhooks({ - onBillingPaid({ data }) { +const { handler } = Webhooks({ + secret, + onCheckoutCompleted({ data }) { console.log('Cobrança paga:', data.payment.amount); }, - onPayoutDone({ data }) { + onPayoutCompleted({ data }) { console.log('Payout concluído:', data.transaction.id); }, onPayoutFailed({ data }) { @@ -88,7 +92,7 @@ Webhooks({

Ou tratar tudo de forma genérica:

```ts -Webhooks({ +const { handler } = Webhooks({ secret, onPayload({ data, event }) { console.log(event, data); diff --git a/packages/elysia/package.json b/packages/elysia/package.json index 7e74a00..07a4f00 100644 --- a/packages/elysia/package.json +++ b/packages/elysia/package.json @@ -1,65 +1,65 @@ { - "name": "@abacatepay/elysia", - "version": "1.0.2", - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "main": "./dist/index.js", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "elysia": "^1.4.22" - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "description": "Checkouts and Webhooks made simple with AbacatePay and Elysia.", - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "files": [ - "dist" - ], - "keywords": [ - "abacatepay", - "payments", - "elysia", - "api", - "webhooks" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "sideEffects": false, - "type": "module", - "types": "./dist/index.d.ts", - "dependencies": { - "@abacatepay/adapters": "^1.0.1" - } + "name": "@abacatepay/elysia", + "version": "2.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "main": "./dist/index.js", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "elysia": "^1.4.22" + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "description": "Checkouts and Webhooks made simple with AbacatePay and Elysia.", + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "files": [ + "dist" + ], + "keywords": [ + "abacatepay", + "payments", + "elysia", + "api", + "webhooks" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "dependencies": { + "@abacatepay/adapters": "^2.0.0" + } } diff --git a/packages/elysia/src/errors.ts b/packages/elysia/src/errors.ts deleted file mode 100644 index 422ab21..0000000 --- a/packages/elysia/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface AbacatePayElysiaErrorOptions { - code: string; -} - -export class AbacatePayElysiaError extends Error { - /** - * The code of the error. - */ - readonly code: string; - /** - * URL for the docs of [@abacatepay/elysia](https://www.npmjs.com/package/@abacatepay/elysia) - */ - static readonly docs = 'https://docs.abacatepay.com/pages/ecosystem/elysia'; - - constructor(message: string, options: AbacatePayElysiaErrorOptions) { - super(message); - - this.code = options.code; - this.name = 'AbacatePayElysiaError'; - } -} diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 1d2db0e..6a72b4f 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -5,38 +5,49 @@ import { type WebhookOptions, } from '@abacatepay/adapters/webhooks'; import { type Context, status } from 'elysia'; -import { AbacatePayElysiaError } from './errors'; export { version } from './version'; +export type WebhooksHandler = (context: Context) => Promise; + +/** + * Result of {@link Webhooks}. Never throws — a missing secret resolves to + * `{ ok: false, error }` instead. + */ +export type WebhooksResult = + | { ok: true; error: null; handler: WebhooksHandler } + | { ok: false; error: string; handler: null }; + /** * A simple utility which resolves incoming webhook payloads by signing the webhook secret properly. * @param options Options to use */ -export const Webhooks = (options: WebhookOptions) => { +export const Webhooks = (options: WebhookOptions): WebhooksResult => { if (!options.secret) - throw new AbacatePayElysiaError('Webhook secret is missing.', { - code: 'WEBHOOK_SECRET_MISSING', - }); + return { ok: false, error: 'Webhook secret is missing.', handler: null }; - return async (context: Context) => { - if (context.query.webhookSecret !== options.secret) - return status('Unauthorized', { error: 'Unauthorized' }); + return { + ok: true, + error: null, + handler: async (context: Context) => { + if (context.query.webhookSecret !== options.secret) + return status('Unauthorized', { error: 'Unauthorized' }); - const signature = context.headers['x-webhook-signature']; + const signature = context.headers['x-webhook-signature']; - if (!signature) - return status('Bad Request', { error: 'Missing signature' }); + if (!signature) + return status('Bad Request', { error: 'Missing signature' }); - const raw = await context.request.text(); + const raw = await context.request.text(); - if (!verify(raw, signature)) - return status('Unauthorized', { error: 'Invalid signature' }); + if (!verify(raw, signature)) + return status('Unauthorized', { error: 'Invalid signature' }); - const { data, success } = parse(context.body); + const { data, success } = parse(context.body); - if (!success) return status('Bad Request', { error: 'Invalid payload' }); + if (!success) return status('Bad Request', { error: 'Invalid payload' }); - await dispatch(data, options); + await dispatch(data, options); + }, }; }; diff --git a/packages/elysia/src/version.ts b/packages/elysia/src/version.ts index 44fb3c0..2ae64a4 100644 --- a/packages/elysia/src/version.ts +++ b/packages/elysia/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/elysia](https://www.npmjs.com/package/@abacatepay/elysia). */ -export const version = '1.0.2' as const; +export const version = '2.0.0' as const; diff --git a/packages/elysia/test/index.test.ts b/packages/elysia/test/index.test.ts index e69de29..5b6e43a 100644 --- a/packages/elysia/test/index.test.ts +++ b/packages/elysia/test/index.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'bun:test'; +import { createHmac } from 'node:crypto'; +import { ABACATEPAY_SHARED_KEY } from '@abacatepay/adapters/webhooks'; +import { Webhooks } from '../src'; + +const sign = (raw: string) => + createHmac('sha256', ABACATEPAY_SHARED_KEY).update(raw).digest('base64'); + +const mockContext = ({ + webhookSecret, + signature, + raw, + body, +}: { + webhookSecret?: string; + signature?: string; + raw?: string; + body?: unknown; +}) => { + const headers = signature ? { 'x-webhook-signature': signature } : {}; + const text = async () => raw ?? ''; + + return { query: { webhookSecret }, headers, request: { text }, body }; +}; + +describe('Webhooks', () => { + test('never throws on a missing secret, resolves { ok: false } instead', () => { + const result = Webhooks({}); + + expect(result.ok).toBe(false); + expect(result.handler).toBeNull(); + if (result.ok) return; + expect(result.error).toContain('secret'); + }); + + test('rejects a mismatched webhookSecret query param', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const result = (await handler( + mockContext({ webhookSecret: 'wrong' }) as never, + )) as { code: number }; + + expect(result.code).toBe(401); + }); + + test('rejects an invalid signature', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const result = (await handler( + mockContext({ + webhookSecret: 'whsec_test', + signature: 'bogus', + raw: '{}', + }) as never, + )) as { code: number }; + + expect(result.code).toBe(401); + }); + + test('dispatches a validly signed event', async () => { + let dispatched = false; + + const { ok, handler } = Webhooks({ + secret: 'whsec_test', + onPayoutCompleted() { + dispatched = true; + }, + }); + if (!ok) throw new Error('expected ok'); + + const event = { + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'COMPLETE', + }, + }, + }; + const raw = JSON.stringify(event); + + await handler( + mockContext({ + webhookSecret: 'whsec_test', + signature: sign(raw), + raw, + body: event, + }) as never, + ); + + expect(dispatched).toBe(true); + }); +}); diff --git a/packages/elysia/tsconfig.json b/packages/elysia/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/elysia/tsconfig.json +++ b/packages/elysia/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/express/README.md b/packages/express/README.md index 3548ce1..1f34283 100644 --- a/packages/express/README.md +++ b/packages/express/README.md @@ -38,20 +38,26 @@ import { Webhooks } from '@abacatepay/express'; const app = express(); -app.post( - '/webhooks/abacatepay', - express.raw({ type: '*/*' }), - Webhooks({ - secret: '...', - onPayload(payload) { - console.log('Evento recebido:', payload.event); - }, - }), -); +const { ok, handler, error } = Webhooks({ + secret: '...', + onPayload(payload) { + console.log('Evento recebido:', payload.event); + }, +}); + +if (!ok) throw new Error(error); + +app.post('/webhooks/abacatepay', express.raw({ type: '*/*' }), handler); ```
+`Webhooks(...)` nunca lança exceção — se o `secret` estiver ausente, ela retorna `{ ok: false, error }` em vez de `{ ok: true, handler }`. + +
+ +
+ ## Segurança por padrão
@@ -72,11 +78,12 @@ Você pode lidar com eventos específicos sem boilerplate: ```ts -Webhooks({ - onBillingPaid({ data }) { +const { handler } = Webhooks({ + secret, + onCheckoutCompleted({ data }) { console.log('Cobrança paga:', data.payment.amount); }, - onPayoutDone({ data }) { + onPayoutCompleted({ data }) { console.log('Payout concluído:', data.transaction.id); }, onPayoutFailed({ data }) { @@ -89,7 +96,7 @@ Webhooks({

Ou tratar tudo de forma genérica:

```ts -Webhooks({ +const { handler } = Webhooks({ secret, onPayload({ data, event }) { console.log(event, data); diff --git a/packages/express/package.json b/packages/express/package.json index 5000cde..2ef256c 100644 --- a/packages/express/package.json +++ b/packages/express/package.json @@ -1,66 +1,66 @@ { - "name": "@abacatepay/express", - "version": "1.0.2", - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "main": "./dist/index.js", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "@types/express": "^5.0.6", - "express": "^5.2.1" - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "description": "Checkouts and Webhooks made simple with AbacatePay and Express.", - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "files": [ - "dist" - ], - "keywords": [ - "abacatepay", - "payments", - "express", - "api", - "webhooks" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "sideEffects": false, - "type": "module", - "types": "./dist/index.d.ts", - "dependencies": { - "@abacatepay/adapters": "^1.0.1" - } + "name": "@abacatepay/express", + "version": "2.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "main": "./dist/index.js", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "@types/express": "^5.0.6", + "express": "^5.2.1" + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "description": "Checkouts and Webhooks made simple with AbacatePay and Express.", + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "files": [ + "dist" + ], + "keywords": [ + "abacatepay", + "payments", + "express", + "api", + "webhooks" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "dependencies": { + "@abacatepay/adapters": "^2.0.0" + } } diff --git a/packages/express/src/errors.ts b/packages/express/src/errors.ts deleted file mode 100644 index da83501..0000000 --- a/packages/express/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface AbacatePayExpressErrorOptions { - code: string; -} - -export class AbacatePayExpressError extends Error { - /** - * The code of the error. - */ - readonly code: string; - /** - * URL for the docs of [@abacatepay/express](https://www.npmjs.com/package/@abacatepay/express) - */ - static readonly docs = 'https://docs.abacatepay.com/pages/ecosystem/express'; - - constructor(message: string, options: AbacatePayExpressErrorOptions) { - super(message); - - this.code = options.code; - this.name = 'AbacatePayExpressError'; - } -} diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index 1e32d66..4e2382c 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -5,69 +5,82 @@ import { type WebhookOptions, } from '@abacatepay/adapters/webhooks'; import type { Request, Response } from 'express'; -import { AbacatePayExpressError } from './errors'; const BAD_REQUEST_STATUS_CODE = 400; const UNAUTHORIZED_STATUS_CODE = 401; const NO_CONTENT_STATUS_CODE = 204; -export { AbacatePayExpressError } from './errors'; export { version } from './version'; -export const Webhooks = (options: WebhookOptions) => { +export type WebhooksHandler = ( + req: Request, + res: Response, +) => Promise; + +/** + * Result of {@link Webhooks}. Never throws — a missing secret resolves to + * `{ ok: false, error }` instead. + */ +export type WebhooksResult = + | { ok: true; error: null; handler: WebhooksHandler } + | { ok: false; error: string; handler: null }; + +export const Webhooks = (options: WebhookOptions): WebhooksResult => { if (!options.secret) - throw new AbacatePayExpressError('Webhook secret is missing.', { - code: 'WEBHOOK_SECRET_MISSING', - }); + return { ok: false, error: 'Webhook secret is missing.', handler: null }; - return async (req: Request, res: Response) => { - const { webhookSecret } = req.query; + return { + ok: true, + error: null, + handler: async (req: Request, res: Response) => { + const { webhookSecret } = req.query; - if (webhookSecret !== options.secret) - return res - .status(UNAUTHORIZED_STATUS_CODE) - .json({ error: 'Unauthorized' }); + if (webhookSecret !== options.secret) + return res + .status(UNAUTHORIZED_STATUS_CODE) + .json({ error: 'Unauthorized' }); - const signature = req.headers['x-webhook-signature']; + const signature = req.headers['x-webhook-signature']; - if (typeof signature !== 'string') - return res - .status(BAD_REQUEST_STATUS_CODE) - .json({ error: 'Missing signature' }); + if (typeof signature !== 'string') + return res + .status(BAD_REQUEST_STATUS_CODE) + .json({ error: 'Missing signature' }); - const { body } = req; + const { body } = req; - if (!Buffer.isBuffer(body)) - return res - .status(BAD_REQUEST_STATUS_CODE) - .json({ error: 'Invalid raw body' }); + if (!Buffer.isBuffer(body)) + return res + .status(BAD_REQUEST_STATUS_CODE) + .json({ error: 'Invalid raw body' }); - const raw = body.toString('utf8'); + const raw = body.toString('utf8'); - if (!verify(raw, signature)) - return res - .status(UNAUTHORIZED_STATUS_CODE) - .json({ error: 'Invalid signature' }); + if (!verify(raw, signature)) + return res + .status(UNAUTHORIZED_STATUS_CODE) + .json({ error: 'Invalid signature' }); - let parsed: unknown; + let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch { - return res - .status(BAD_REQUEST_STATUS_CODE) - .json({ error: 'Invalid JSON' }); - } + try { + parsed = JSON.parse(raw); + } catch { + return res + .status(BAD_REQUEST_STATUS_CODE) + .json({ error: 'Invalid JSON' }); + } - const { data, success } = parse(parsed); + const { data, success } = parse(parsed); - if (!success) - return res - .status(BAD_REQUEST_STATUS_CODE) - .json({ error: 'Invalid payload' }); + if (!success) + return res + .status(BAD_REQUEST_STATUS_CODE) + .json({ error: 'Invalid payload' }); - await dispatch(data, options); + await dispatch(data, options); - return res.status(NO_CONTENT_STATUS_CODE).send(); + return res.status(NO_CONTENT_STATUS_CODE).send(); + }, }; }; diff --git a/packages/express/src/version.ts b/packages/express/src/version.ts index 16ffa86..636c714 100644 --- a/packages/express/src/version.ts +++ b/packages/express/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/express](https://www.npmjs.com/package/@abacatepay/express). */ -export const version = '1.0.2' as const; +export const version = '2.0.0' as const; diff --git a/packages/express/test/index.test.ts b/packages/express/test/index.test.ts index e69de29..d7f3ca6 100644 --- a/packages/express/test/index.test.ts +++ b/packages/express/test/index.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test } from 'bun:test'; +import { createHmac } from 'node:crypto'; +import { ABACATEPAY_SHARED_KEY } from '@abacatepay/adapters/webhooks'; +import { Webhooks } from '../src'; + +const sign = (raw: string) => + createHmac('sha256', ABACATEPAY_SHARED_KEY).update(raw).digest('base64'); + +const mockRes = () => { + const res = { + statusCode: 0, + body: undefined as unknown, + status(code: number) { + res.statusCode = code; + return res; + }, + json(body: unknown) { + res.body = body; + return res; + }, + send(body?: unknown) { + res.body = body; + return res; + }, + }; + return res; +}; + +describe('Webhooks', () => { + test('never throws on a missing secret, resolves { ok: false } instead', () => { + const result = Webhooks({}); + + expect(result.ok).toBe(false); + expect(result.handler).toBeNull(); + if (result.ok) return; + expect(result.error).toContain('secret'); + }); + + test('rejects a mismatched webhookSecret query param with 401', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const res = mockRes(); + await handler( + { query: { webhookSecret: 'wrong' }, headers: {} } as never, + res as never, + ); + + expect(res.statusCode).toBe(401); + }); + + test('dispatches a validly signed event', async () => { + let dispatched = false; + + const { ok, handler } = Webhooks({ + secret: 'whsec_test', + onPayoutCompleted() { + dispatched = true; + }, + }); + if (!ok) throw new Error('expected ok'); + + const body = JSON.stringify({ + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'COMPLETE', + }, + }, + }); + + const res = mockRes(); + await handler( + { + query: { webhookSecret: 'whsec_test' }, + headers: { 'x-webhook-signature': sign(body) }, + body: Buffer.from(body), + } as never, + res as never, + ); + + expect(res.statusCode).toBe(204); + expect(dispatched).toBe(true); + }); +}); diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/express/tsconfig.json +++ b/packages/express/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/fastify/README.md b/packages/fastify/README.md index b899ef7..14bdf67 100644 --- a/packages/fastify/README.md +++ b/packages/fastify/README.md @@ -46,19 +46,22 @@ app.addContentTypeParser( }, ); -app.post( - '/webhooks/abacatepay', - Webhooks({ - secret: '...', - onPayload(payload) { - console.log('Evento recebido:', payload.event); - }, - }), -); +const { ok, handler, error } = Webhooks({ + secret: '...', + onPayload(payload) { + console.log('Evento recebido:', payload.event); + }, +}); + +if (!ok) throw new Error(error); + +app.post('/webhooks/abacatepay', handler); ```
+`Webhooks(...)` nunca lança exceção — se o `secret` estiver ausente, ela retorna `{ ok: false, error }` em vez de `{ ok: true, handler }`. + Você precisa usar `.addContentTypeParser` para a rota `/webhooks/abacatepay` receber o body como string e validar corretamente. ## Segurança por padrão @@ -81,11 +84,12 @@ Você pode lidar com eventos específicos sem boilerplate:
```ts -Webhooks({ - onBillingPaid({ data }) { +const { handler } = Webhooks({ + secret, + onCheckoutCompleted({ data }) { console.log('Cobrança paga:', data.payment.amount); }, - onPayoutDone({ data }) { + onPayoutCompleted({ data }) { console.log('Payout concluído:', data.transaction.id); }, onPayoutFailed({ data }) { @@ -98,7 +102,7 @@ Webhooks({

Ou tratar tudo de forma genérica:

```ts -Webhooks({ +const { handler } = Webhooks({ secret, onPayload({ data, event }) { console.log(event, data); diff --git a/packages/fastify/package.json b/packages/fastify/package.json index cfdb3f2..faf355f 100644 --- a/packages/fastify/package.json +++ b/packages/fastify/package.json @@ -1,65 +1,65 @@ { - "name": "@abacatepay/fastify", - "version": "1.0.3", - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "main": "./dist/index.js", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "fastify": "^5.7.1" - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "description": "Checkouts and Webhooks made simple with AbacatePay and Fastify.", - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "files": [ - "dist" - ], - "keywords": [ - "abacatepay", - "payments", - "fastify", - "api", - "webhooks" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "sideEffects": false, - "type": "module", - "types": "./dist/index.d.ts", - "dependencies": { - "@abacatepay/adapters": "^1.0.1" - } + "name": "@abacatepay/fastify", + "version": "2.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "main": "./dist/index.js", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "fastify": "^5.7.1" + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "description": "Checkouts and Webhooks made simple with AbacatePay and Fastify.", + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "files": [ + "dist" + ], + "keywords": [ + "abacatepay", + "payments", + "fastify", + "api", + "webhooks" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "dependencies": { + "@abacatepay/adapters": "^2.0.0" + } } diff --git a/packages/fastify/src/errors.ts b/packages/fastify/src/errors.ts deleted file mode 100644 index 4a76d0b..0000000 --- a/packages/fastify/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface AbacatePayFastifyErrorOptions { - code: string; -} - -export class AbacatePayFastifyError extends Error { - /** - * The code of the error. - */ - readonly code: string; - /** - * URL for the docs of [@abacatepay/fastify](https://www.npmjs.com/package/@abacatepay/fastify) - */ - static readonly docs = 'https://docs.abacatepay.com/pages/ecosystem/fastify'; - - constructor(message: string, options: AbacatePayFastifyErrorOptions) { - super(message); - - this.code = options.code; - this.name = 'AbacatePayFastifyError'; - } -} diff --git a/packages/fastify/src/index.ts b/packages/fastify/src/index.ts index 285f21c..208e4cc 100644 --- a/packages/fastify/src/index.ts +++ b/packages/fastify/src/index.ts @@ -5,57 +5,80 @@ import { type WebhookOptions, } from '@abacatepay/adapters/webhooks'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import { AbacatePayFastifyError } from './errors'; const BAD_REQUEST_STATUS_CODE = 400; const UNAUTHORIZED_STATUS_CODE = 401; const NO_CONTENT_STATUS_CODE = 204; -export { AbacatePayFastifyError } from './errors'; export { version } from './version'; -export const Webhooks = (options: WebhookOptions) => { +export type WebhooksHandler = ( + req: FastifyRequest, + reply: FastifyReply, +) => Promise; + +/** + * Result of {@link Webhooks}. Never throws — a missing secret resolves to + * `{ ok: false, error }` instead. + */ +export type WebhooksResult = + | { ok: true; error: null; handler: WebhooksHandler } + | { ok: false; error: string; handler: null }; + +export const Webhooks = (options: WebhookOptions): WebhooksResult => { if (!options.secret) - throw new AbacatePayFastifyError('Webhook secret is missing.', { - code: 'WEBHOOK_SECRET_MISSING', - }); + return { ok: false, error: 'Webhook secret is missing.', handler: null }; + + return { + ok: true, + error: null, + handler: async (req: FastifyRequest, reply: FastifyReply) => { + const { webhookSecret } = req.query as Record; + + if (webhookSecret !== options.secret) + return reply + .status(UNAUTHORIZED_STATUS_CODE) + .send({ error: 'Unauthorized' }); - return async (req: FastifyRequest, reply: FastifyReply) => { - const { webhookSecret } = req.query as Record; + const signature = req.headers['x-webhook-signature']; - if (webhookSecret !== options.secret) - return reply - .status(UNAUTHORIZED_STATUS_CODE) - .send({ error: 'Unauthorized' }); + if (typeof signature !== 'string') + return reply + .status(BAD_REQUEST_STATUS_CODE) + .send({ error: 'Missing signature' }); - const signature = req.headers['x-webhook-signature']; + const { body } = req; - if (typeof signature !== 'string') - return reply - .status(BAD_REQUEST_STATUS_CODE) - .send({ error: 'Missing signature' }); + if (typeof body !== 'string') + return reply + .status(BAD_REQUEST_STATUS_CODE) + .send({ error: 'Invalid raw body' }); - const { body } = req; + if (!verify(body, signature)) + return reply + .status(UNAUTHORIZED_STATUS_CODE) + .send({ error: 'Invalid signature' }); - if (typeof body !== 'string') - return reply - .status(BAD_REQUEST_STATUS_CODE) - .send({ error: 'Invalid raw body' }); + let parsed: unknown; - if (!verify(body, signature)) - return reply - .status(UNAUTHORIZED_STATUS_CODE) - .send({ error: 'Invalid signature' }); + try { + parsed = JSON.parse(body); + } catch { + return reply + .status(BAD_REQUEST_STATUS_CODE) + .send({ error: 'Invalid JSON' }); + } - const { data, success } = parse(JSON.parse(body)); + const { data, success } = parse(parsed); - if (!success) - return reply - .status(BAD_REQUEST_STATUS_CODE) - .send({ error: 'Invalid payload' }); + if (!success) + return reply + .status(BAD_REQUEST_STATUS_CODE) + .send({ error: 'Invalid payload' }); - await dispatch(data, options); + await dispatch(data, options); - return reply.status(NO_CONTENT_STATUS_CODE).send(); + return reply.status(NO_CONTENT_STATUS_CODE).send(); + }, }; }; diff --git a/packages/fastify/src/version.ts b/packages/fastify/src/version.ts index bc7a605..39c39d2 100644 --- a/packages/fastify/src/version.ts +++ b/packages/fastify/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/fastify](https://www.npmjs.com/package/@abacatepay/fastify). */ -export const version = '1.0.3' as const; +export const version = '2.0.0' as const; diff --git a/packages/fastify/test/index.test.ts b/packages/fastify/test/index.test.ts index e69de29..47d32b0 100644 --- a/packages/fastify/test/index.test.ts +++ b/packages/fastify/test/index.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, test } from 'bun:test'; +import { createHmac } from 'node:crypto'; +import { ABACATEPAY_SHARED_KEY } from '@abacatepay/adapters/webhooks'; +import { Webhooks } from '../src'; + +const sign = (raw: string) => + createHmac('sha256', ABACATEPAY_SHARED_KEY).update(raw).digest('base64'); + +const mockReply = () => { + const reply = { + statusCode: 0, + body: undefined as unknown, + status(code: number) { + reply.statusCode = code; + return reply; + }, + send(body?: unknown) { + reply.body = body; + return reply; + }, + }; + return reply; +}; + +describe('Webhooks', () => { + test('never throws on a missing secret, resolves { ok: false } instead', () => { + const result = Webhooks({}); + + expect(result.ok).toBe(false); + expect(result.handler).toBeNull(); + if (result.ok) return; + expect(result.error).toContain('secret'); + }); + + test('rejects a mismatched webhookSecret query param with 401', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const reply = mockReply(); + await handler( + { query: { webhookSecret: 'wrong' }, headers: {} } as never, + reply as never, + ); + + expect(reply.statusCode).toBe(401); + }); + + test('returns 400 on a malformed JSON body instead of throwing', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const body = '{not valid json'; + const reply = mockReply(); + await handler( + { + query: { webhookSecret: 'whsec_test' }, + headers: { 'x-webhook-signature': sign(body) }, + body, + } as never, + reply as never, + ); + + expect(reply.statusCode).toBe(400); + }); + + test('dispatches a validly signed event', async () => { + let dispatched = false; + + const { ok, handler } = Webhooks({ + secret: 'whsec_test', + onPayoutCompleted() { + dispatched = true; + }, + }); + if (!ok) throw new Error('expected ok'); + + const body = JSON.stringify({ + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'COMPLETE', + }, + }, + }); + + const reply = mockReply(); + await handler( + { + query: { webhookSecret: 'whsec_test' }, + headers: { 'x-webhook-signature': sign(body) }, + body, + } as never, + reply as never, + ); + + expect(reply.statusCode).toBe(204); + expect(dispatched).toBe(true); + }); +}); diff --git a/packages/fastify/tsconfig.json b/packages/fastify/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/fastify/tsconfig.json +++ b/packages/fastify/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/hono/README.md b/packages/hono/README.md index 601461e..d95fafe 100644 --- a/packages/hono/README.md +++ b/packages/hono/README.md @@ -38,19 +38,22 @@ import { Webhooks } from '@abacatepay/hono'; const app = new Hono(); -app.post( - '/webhooks/abacatepay', - Webhooks({ - secret: '...', - onPayload(payload) { - console.log('Evento recebido:', payload.event); - }, - }), -); +const { ok, handler, error } = Webhooks({ + secret: '...', + onPayload(payload) { + console.log('Evento recebido:', payload.event); + }, +}); + +if (!ok) throw new Error(error); + +app.post('/webhooks/abacatepay', handler); ```
+`Webhooks(...)` nunca lança exceção — se o `secret` estiver ausente, ela retorna `{ ok: false, error }` em vez de `{ ok: true, handler }`. + ## Segurança por padrão
@@ -71,11 +74,12 @@ Você pode lidar com eventos específicos sem boilerplate: ```ts -Webhooks({ - onBillingPaid({ data }) { +const { handler } = Webhooks({ + secret, + onCheckoutCompleted({ data }) { console.log('Cobrança paga:', data.payment.amount); }, - onPayoutDone({ data }) { + onPayoutCompleted({ data }) { console.log('Payout concluído:', data.transaction.id); }, onPayoutFailed({ data }) { @@ -88,7 +92,7 @@ Webhooks({

Ou tratar tudo de forma genérica:

```ts -Webhooks({ +const { handler } = Webhooks({ secret, onPayload({ data, event }) { console.log(event, data); diff --git a/packages/hono/package.json b/packages/hono/package.json index ae2d346..ee1feb9 100644 --- a/packages/hono/package.json +++ b/packages/hono/package.json @@ -1,65 +1,65 @@ { - "name": "@abacatepay/hono", - "version": "1.0.1", - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "main": "./dist/index.js", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "hono": "^4.11.4" - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "description": "Checkouts and Webhooks made simple with AbacatePay and Hono.", - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "files": [ - "dist" - ], - "keywords": [ - "abacatepay", - "payments", - "hono", - "api", - "webhooks" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "sideEffects": false, - "type": "module", - "types": "./dist/index.d.ts", - "dependencies": { - "@abacatepay/adapters": "^1.0.1" - } + "name": "@abacatepay/hono", + "version": "2.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "main": "./dist/index.js", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "hono": "^4.11.4" + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "description": "Checkouts and Webhooks made simple with AbacatePay and Hono.", + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "files": [ + "dist" + ], + "keywords": [ + "abacatepay", + "payments", + "hono", + "api", + "webhooks" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "dependencies": { + "@abacatepay/adapters": "^2.0.0" + } } diff --git a/packages/hono/src/errors.ts b/packages/hono/src/errors.ts deleted file mode 100644 index 6bf98d6..0000000 --- a/packages/hono/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface AbacatePayHonoErrorOptions { - code: string; -} - -export class AbacatePayHonoError extends Error { - /** - * The code of the error. - */ - readonly code: string; - /** - * URL for the docs of [@abacatepay/hono](https://www.npmjs.com/package/@abacatepay/hono) - */ - static readonly docs = 'https://docs.abacatepay.com/pages/ecosystem/hono'; - - constructor(message: string, options: AbacatePayHonoErrorOptions) { - super(message); - - this.code = options.code; - this.name = 'AbacatePayHonoError'; - } -} diff --git a/packages/hono/src/index.ts b/packages/hono/src/index.ts index c2d8363..e4bc8f5 100644 --- a/packages/hono/src/index.ts +++ b/packages/hono/src/index.ts @@ -5,41 +5,65 @@ import { type WebhookOptions, } from '@abacatepay/adapters/webhooks'; import type { Context } from 'hono'; -import { AbacatePayHonoError } from './errors'; const BAD_REQUEST_STATUS_CODE = 400; const UNAUTHORIZED_STATUS_CODE = 401; -export { AbacatePayHonoError } from './errors'; export { version } from './version'; -export const Webhooks = (options: WebhookOptions) => { +export type WebhooksHandler = (ctx: Context) => Promise; + +/** + * Result of {@link Webhooks}. Never throws — a missing secret resolves to + * `{ ok: false, error }` instead. + */ +export type WebhooksResult = + | { ok: true; error: null; handler: WebhooksHandler } + | { ok: false; error: string; handler: null }; + +export const Webhooks = (options: WebhookOptions): WebhooksResult => { if (!options.secret) - throw new AbacatePayHonoError('Webhook secret is missing.', { - code: 'WEBHOOK_SECRET_MISSING', - }); + return { ok: false, error: 'Webhook secret is missing.', handler: null }; + + return { + ok: true, + error: null, + handler: async (ctx: Context) => { + const webhookSecret = ctx.req.query('webhookSecret'); + + if (webhookSecret !== options.secret) + return ctx.json({ error: 'Unauthorized' }, UNAUTHORIZED_STATUS_CODE); - return async (ctx: Context) => { - const webhookSecret = ctx.req.query('webhookSecret'); + const signature = ctx.req.header('x-webhook-signature'); - if (webhookSecret !== options.secret) - return ctx.json({ error: 'Unauthorized' }, UNAUTHORIZED_STATUS_CODE); + if (!signature) + return ctx.json( + { error: 'Missing signature' }, + BAD_REQUEST_STATUS_CODE, + ); - const signature = ctx.req.header('x-webhook-signature'); + const raw = await ctx.req.text(); - if (!signature) - return ctx.json({ error: 'Missing signature' }, BAD_REQUEST_STATUS_CODE); + if (!verify(raw, signature)) + return ctx.json( + { error: 'Invalid signature' }, + UNAUTHORIZED_STATUS_CODE, + ); - const raw = await ctx.req.text(); + let parsed: unknown; - if (!verify(raw, signature)) - return ctx.json({ error: 'Invalid signature' }, UNAUTHORIZED_STATUS_CODE); + try { + parsed = JSON.parse(raw); + } catch { + return ctx.json({ error: 'Invalid JSON' }, BAD_REQUEST_STATUS_CODE); + } - const { success, data } = parse(JSON.parse(raw)); + const { success, data } = parse(parsed); - if (!success) - return ctx.json({ error: 'Invalid payload' }, BAD_REQUEST_STATUS_CODE); + if (!success) + return ctx.json({ error: 'Invalid payload' }, BAD_REQUEST_STATUS_CODE); - await dispatch(data, options); + await dispatch(data, options); + }, }; }; diff --git a/packages/hono/src/version.ts b/packages/hono/src/version.ts index 75dfa3d..b7518ba 100644 --- a/packages/hono/src/version.ts +++ b/packages/hono/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/hono](https://www.npmjs.com/package/@abacatepay/hono). */ -export const version = '1.0.1' as const; +export const version = '2.0.0' as const; diff --git a/packages/hono/test/index.test.ts b/packages/hono/test/index.test.ts index e69de29..4d7c801 100644 --- a/packages/hono/test/index.test.ts +++ b/packages/hono/test/index.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, test } from 'bun:test'; +import { createHmac } from 'node:crypto'; +import { ABACATEPAY_SHARED_KEY } from '@abacatepay/adapters/webhooks'; +import { Webhooks } from '../src'; + +const sign = (raw: string) => + createHmac('sha256', ABACATEPAY_SHARED_KEY).update(raw).digest('base64'); + +const mockContext = ({ + query, + headers = {}, + body, +}: { + query?: string; + headers?: Record; + body?: string; +}) => ({ + req: { + query: (_key: string) => query, + header: (key: string) => headers[key.toLowerCase()], + text: async () => body ?? '', + }, + json: (payload: unknown, status: number) => ({ payload, status }), +}); + +describe('Webhooks', () => { + test('never throws on a missing secret, resolves { ok: false } instead', () => { + const result = Webhooks({}); + + expect(result.ok).toBe(false); + expect(result.handler).toBeNull(); + if (result.ok) return; + expect(result.error).toContain('secret'); + }); + + test('rejects a mismatched webhookSecret query param with 401', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const ctx = mockContext({ query: 'wrong' }); + const result = await handler(ctx as never); + + expect((result as { status: number })?.status).toBe(401); + }); + + test('returns 400 on a malformed JSON body instead of throwing', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const body = '{not valid json'; + const ctx = mockContext({ + query: 'whsec_test', + headers: { 'x-webhook-signature': sign(body) }, + body, + }); + const result = await handler(ctx as never); + + expect((result as { status: number })?.status).toBe(400); + }); + + test('dispatches a validly signed event', async () => { + let dispatched = false; + + const { ok, handler } = Webhooks({ + secret: 'whsec_test', + onPayoutCompleted() { + dispatched = true; + }, + }); + if (!ok) throw new Error('expected ok'); + + const body = JSON.stringify({ + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'COMPLETE', + }, + }, + }); + + const ctx = mockContext({ + query: 'whsec_test', + headers: { 'x-webhook-signature': sign(body) }, + body, + }); + await handler(ctx as never); + + expect(dispatched).toBe(true); + }); +}); diff --git a/packages/hono/tsconfig.json b/packages/hono/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/hono/tsconfig.json +++ b/packages/hono/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/rest/README.md b/packages/rest/README.md index 7f1e24a..223238f 100644 --- a/packages/rest/README.md +++ b/packages/rest/README.md @@ -30,13 +30,13 @@ Simule um pagamento QRCodePix que tenha o ID `pix_char_123456` ```ts -import { REST } from '@abacatepay/rest'; +import { createREST } from '@abacatepay/rest'; -const client = new REST({ +const client = createREST({ secret: process.env.ABACATEPAY_API_KEY!, }); -const pix = await client.post('/pixQrCode/simulate-payment', { +const pix = await client.post('/transparents/simulate-payment', { query: { id: 'pix_char_123456' }, }); @@ -52,7 +52,7 @@ Por padrão, o REST faz **3 retries** automaticamente para erros **retryable** ( ```ts -const client = new REST({ +const client = createREST({ retry: { max: 5 }, @@ -65,7 +65,7 @@ const client = new REST({ ```ts -const client = new REST({ +const client = createREST({ retry: { max: 7, backoff(attempt) { @@ -82,26 +82,20 @@ const client = new REST({ ## Tratando erros -O REST Client expõe erros tipados para facilitar o controle e o tratamento deles +O REST Client **nunca lança exceções** — toda chamada resolve com o mesmo formato `{ data, error, success }` que a própria API da AbacatePay retorna. Falhas do lado do cliente (rede, timeout, secret ausente) são normalizadas para esse mesmo formato. ```ts -import { AbacatePayError, HTTPError } from '@abacatepay/rest'; +const { data, error, success } = await client.get('/customers/invalid'); -try { - await client.get('/customers/invalid'); -} catch (err) { - if (err instanceof AbacatePayError) { - console.error('Erro da API:', err.message); +if (!success) { + console.error('Erro:', error); - return; - } - - if (err instanceof HTTPError) { - console.error(`Erro de rede/HTTP em ${err.route}: ${err.message}`); - } + return; } + +console.log(data); ```
diff --git a/packages/rest/package.json b/packages/rest/package.json index 7733c98..82d46d3 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,60 +1,60 @@ { - "name": "@abacatepay/rest", - "version": "0.0.3", - "type": "module", - "sideEffects": false, - "license": "Apache-2.0", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && bun build src/index.ts --outdir dist --target node && bun run types", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "keywords": [ - "abacatepay", - "payments", - "sdk", - "rest", - "api", - "typescript" - ], - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "description": "A REST client for AbacatePay API" + "name": "@abacatepay/rest", + "version": "1.0.0", + "type": "module", + "sideEffects": false, + "license": "Apache-2.0", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && bun build src/index.ts --outdir dist --target node && bun run types", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "keywords": [ + "abacatepay", + "payments", + "sdk", + "rest", + "api", + "typescript" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "description": "A REST client for AbacatePay API" } diff --git a/packages/rest/src/client.ts b/packages/rest/src/client.ts index 41dacd7..18ec70d 100644 --- a/packages/rest/src/client.ts +++ b/packages/rest/src/client.ts @@ -1,4 +1,3 @@ -import { AbacatePayError, HTTPError } from './errors'; import type { InternalHandleErrorOptions, InternalHandleTimeoutErrorOptions, @@ -11,148 +10,98 @@ import { isTimeoutError, RATE_LIMIT_STATUS_CODE, RETRYABLE_STATUS, - SERVICE_UNAVAILABLE_STATUS_CODE, sleep, } from './utils'; const DEFAULT_TIMEOUT_IN_MS = 5_000; +const NO_CONTENT_STATUS_CODE = 204; /** - * Represents the class that manages handlers for endpoints. + * Builds an `APIResponse`-shaped failure so client-side issues (network + * errors, timeouts, missing credentials) are indistinguishable from a real + * API error to callers — nothing in this client ever throws. */ -export class REST { - public constructor( - /** - * Options to use in all requests. - */ - public options: RESTOptions = {}, - ) {} - - /** - * Sets the authorization token that should be used for requests. - * @param secret The secret to use. - */ - public setSecret(secret: string) { - this.options.secret = secret; - - return this; - } - - /** - * Runs a GET request from the API. - */ - public get(route: string, options?: MakeRequestOptionsWithoutMethod) { - return this.makeRequest(route, { ...options, method: 'GET' }); - } - - /** - * Runs a POST request from the API. - */ - public post(route: string, options?: MakeRequestOptionsWithoutMethod) { - return this.makeRequest(route, { ...options, method: 'POST' }); - } - - /** - * Runs a DELETE request from the API. - */ - public delete(route: string, options?: MakeRequestOptionsWithoutMethod) { - return this.makeRequest(route, { ...options, method: 'DELETE' }); - } - - /** - * Runs a PUT request from the API. - */ - public put(route: string, options?: MakeRequestOptionsWithoutMethod) { - return this.makeRequest(route, { ...options, method: 'PUT' }); - } - - /** - * Runs a PATCH request from the API. - */ - public patch(route: string, options?: MakeRequestOptionsWithoutMethod) { - return this.makeRequest(route, { ...options, method: 'PATCH' }); - } - - private async makeRequest( - route: string, - options: MakeRequestOptions, - attempt = 0, - ): Promise { - const url = this.makeURL(route, options.query); - const { timeout = DEFAULT_TIMEOUT_IN_MS } = this.options; +const asFailure = (error: string): R => + ({ data: null, error, success: false }) as R; - const retry = options.retry ?? this.options.retry ?? { max: 3 }; +/** + * Creates a REST client for the AbacatePay API. + * + * Every method resolves — it never rejects or throws. Client-side failures + * (network errors, timeouts, missing secret) are normalized into the same + * `{ data, error, success }` shape a real API response has. + */ +export const createREST = (options: RESTOptions = {}) => { + const makeURL = (route: string, query?: MakeRequestOptions['query']) => { + const base = `${options.base ?? 'https://api.abacatepay.com/v'}${options.version ?? 2}${route}`; - try { - const response = await fetch(url, { - method: options.method, - signal: AbortSignal.timeout(timeout), - headers: this.makeHeaders(options.headers), - body: 'body' in options ? JSON.stringify(options.body) : null, - }); + return query ? `${base}?${new URLSearchParams(query)}` : base; + }; - if (!response.ok) - return this.handleError({ route, retry, attempt, options, response }); + const makeHeaders = (custom?: Record) => { + const { + secret = process.env.ABACATEPAY_SECRET ?? process.env.ABACATEPAY_API_KEY, + } = options; - return this.process(response); - } catch (err) { - if (isTimeoutError(err)) - return this.handleTimeout({ retry, route, attempt, options }); + if (!secret) return null; - throw new HTTPError(`${err}`, route, 0, ''); - } - } + return { + 'Content-Type': 'application/json', + Authorization: `Bearer ${secret}`, + ...options.headers, + ...custom, + }; + }; + + const parseResponse = async (response: Response): Promise => { + if (response.status === NO_CONTENT_STATUS_CODE) + return { data: null, error: null, success: true } as R; - private async handleTimeout({ + return (await response.json()) as R; + }; + + const handleTimeout = async ({ retry, route, attempt, - options, - }: InternalHandleTimeoutErrorOptions) { + requestOptions, + }: InternalHandleTimeoutErrorOptions): Promise => { if (attempt >= retry.max) - throw new HTTPError( - `${retry.max} attempts were performed, all failed`, - route, - SERVICE_UNAVAILABLE_STATUS_CODE, - options.method, - ); + return asFailure(`${retry.max} attempts were performed, all failed`); if (retry.onRetry) - await retry.onRetry({ - attempt, - options, - }); + await retry.onRetry({ attempt, options: requestOptions }); const delay = (retry.backoff ?? backoff)(attempt); await sleep(delay); - return this.makeRequest(route, options, attempt + 1); - } + return makeRequest(route, requestOptions, attempt + 1); + }; - private async handleError({ + const handleError = async ({ route, retry, - options, + requestOptions, attempt, response, - }: InternalHandleErrorOptions) { + }: InternalHandleErrorOptions): Promise => { if (!RETRYABLE_STATUS.includes(response.status)) { - const { error } = await response.json(); + const body = await response.json().catch(() => null); - throw new AbacatePayError(error); + return asFailure( + body?.error ?? + `Request to ${route} failed with status ${response.status}`, + ); } - const { onRateLimit } = this.options; + const { onRateLimit } = options; if (attempt >= retry.max) - throw new HTTPError( - `${retry.max} attempts were performed, all failed`, - route, - response.status, - options.method, - ); - if (retry.onRetry) await retry.onRetry({ attempt, options, response }); + return asFailure(`${retry.max} attempts were performed, all failed`); + + if (retry.onRetry) + await retry.onRetry({ attempt, options: requestOptions, response }); if (response.status === RATE_LIMIT_STATUS_CODE && onRateLimit) await onRateLimit(response); @@ -160,43 +109,109 @@ export class REST { await sleep(delay); - return this.makeRequest(route, options, attempt + 1); - } + return makeRequest(route, requestOptions, attempt + 1); + }; - private async process(response: Response) { - const NO_CONTENT_STATUS_CODE = 204; + const makeRequest = async ( + route: string, + requestOptions: MakeRequestOptions, + attempt = 0, + ): Promise => { + const headers = makeHeaders(requestOptions.headers); - if (response.status === NO_CONTENT_STATUS_CODE) return void 0 as R; + if (!headers) + return asFailure( + 'We could not find any AbacatePay secret, use REST({ secret })', + ); - const { data, error } = await response.json(); + const url = makeURL(route, requestOptions.query); + const { timeout = DEFAULT_TIMEOUT_IN_MS } = options; + const retry = requestOptions.retry ?? options.retry ?? { max: 3 }; - // This should never happen - if (error) throw new AbacatePayError(error); + try { + const response = await fetch(url, { + method: requestOptions.method, + signal: AbortSignal.timeout(timeout), + headers, + body: + 'body' in requestOptions ? JSON.stringify(requestOptions.body) : null, + }); - return data as R; - } + if (!response.ok) + return handleError({ + route, + retry, + attempt, + requestOptions, + response, + }); + + return parseResponse(response); + } catch (err) { + if (isTimeoutError(err)) + return handleTimeout({ retry, route, attempt, requestOptions }); - private makeURL(route: string, query?: MakeRequestOptions['query']) { - const base = `${this.options.base ?? 'https://api.abacatepay.com/v'}${this.options.version ?? 1}${route}`; + return asFailure(`${err}`); + } + }; - return query ? `${base}?${new URLSearchParams(query)}` : base; - } + const client = { + /** + * Options used in all requests. Mutating this object (e.g. via + * `setSecret`) affects every subsequent call made with this client. + */ + options, - private makeHeaders(custom?: Record) { - const { - secret = process.env.ABACATEPAY_SECRET ?? process.env.ABACATEPAY_API_KEY, - } = this.options; + /** + * Sets the authorization token that should be used for requests. + * @param secret The secret to use. + */ + setSecret(secret: string) { + options.secret = secret; - if (!secret) - throw new AbacatePayError( - 'We could not find any AbacatePay secret, use REST({ secret })', - ); + return client; + }, - return { - 'Content-Type': 'application/json', - Authorization: `Bearer ${secret}`, - ...this.options.headers, - ...custom, - }; - } -} + /** + * Runs a GET request from the API. + */ + get(route: string, requestOptions?: MakeRequestOptionsWithoutMethod) { + return makeRequest(route, { ...requestOptions, method: 'GET' }); + }, + + /** + * Runs a POST request from the API. + */ + post(route: string, requestOptions?: MakeRequestOptionsWithoutMethod) { + return makeRequest(route, { ...requestOptions, method: 'POST' }); + }, + + /** + * Runs a DELETE request from the API. + */ + delete(route: string, requestOptions?: MakeRequestOptionsWithoutMethod) { + return makeRequest(route, { ...requestOptions, method: 'DELETE' }); + }, + + /** + * Runs a PUT request from the API. + */ + put(route: string, requestOptions?: MakeRequestOptionsWithoutMethod) { + return makeRequest(route, { ...requestOptions, method: 'PUT' }); + }, + + /** + * Runs a PATCH request from the API. + */ + patch(route: string, requestOptions?: MakeRequestOptionsWithoutMethod) { + return makeRequest(route, { ...requestOptions, method: 'PATCH' }); + }, + }; + + return client; +}; + +/** + * A REST client for the AbacatePay API, as returned by {@link createREST}. + */ +export type REST = ReturnType; diff --git a/packages/rest/src/errors.ts b/packages/rest/src/errors.ts deleted file mode 100644 index bcf2a71..0000000 --- a/packages/rest/src/errors.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Represents an API error returned by AbacatePay. - */ -export class AbacatePayError extends Error { - public constructor(public message: string) { - super(message); - - this.name = 'AbacatePayError'; - } -} - -/** - * Represents any HTTP error. - */ -export class HTTPError extends Error { - public constructor( - /** - * The content of the error message. - */ - public message: string, - /** - * Route that returned the error (e.g. `/store/get`). - */ - public route: string, - /** - * Status code of the response. - */ - public status: number, - /** - * Method used in the request. - */ - public method: string, - ) { - super(message); - - this.name = `HTTPError(${route})`; - } -} diff --git a/packages/rest/src/index.ts b/packages/rest/src/index.ts index 1d8166a..9a908ec 100644 --- a/packages/rest/src/index.ts +++ b/packages/rest/src/index.ts @@ -1,9 +1,4 @@ -export { REST } from './client'; - -export { - AbacatePayError, - HTTPError, -} from './errors'; +export { createREST, type REST } from './client'; export * from './types'; export { RETRYABLE_STATUS } from './utils'; diff --git a/packages/rest/src/types.ts b/packages/rest/src/types.ts index 628e448..033b760 100644 --- a/packages/rest/src/types.ts +++ b/packages/rest/src/types.ts @@ -122,7 +122,7 @@ export interface InternalHandleErrorOptions { attempt: number; response: Response; retry: RetryOptions; - options: MakeRequestOptions; + requestOptions: MakeRequestOptions; } /** diff --git a/packages/rest/src/version.ts b/packages/rest/src/version.ts index cf80ee4..59ebc8c 100644 --- a/packages/rest/src/version.ts +++ b/packages/rest/src/version.ts @@ -3,7 +3,7 @@ /** * Current version of [@abacatepay/rest](https://www.npmjs.com/package/@abacatepay/rest). */ -export const version = '0.0.3' as const; +export const version = '1.0.0' as const; /** * Current version of the AbacatePay API. */ diff --git a/packages/rest/test/index.test.ts b/packages/rest/test/index.test.ts index e69de29..b85612b 100644 --- a/packages/rest/test/index.test.ts +++ b/packages/rest/test/index.test.ts @@ -0,0 +1,122 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; +import { createREST } from '../src'; + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + +const originalFetch = globalThis.fetch; + +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +describe('createREST', () => { + test('resolves with the exact success envelope the API returned', async () => { + globalThis.fetch = mock(async () => + jsonResponse({ data: { id: 'cust_123' }, error: null, success: true }), + ) as typeof fetch; + + const client = createREST({ secret: 'sk_test' }); + const result = await client.get('/customers/get?id=cust_123'); + + expect(result).toEqual({ + data: { id: 'cust_123' }, + error: null, + success: true, + }); + }); + + test('normalizes a non-retryable API error into the APIResponse shape instead of throwing', async () => { + globalThis.fetch = mock(async () => + jsonResponse( + { data: null, error: 'Customer not found', success: false }, + 404, + ), + ) as typeof fetch; + + const client = createREST({ secret: 'sk_test' }); + const result = await client.get('/customers/get?id=missing'); + + expect(result).toEqual({ + data: null, + error: 'Customer not found', + success: false, + }); + }); + + test('never throws when no secret can be resolved', async () => { + const fetchSpy = mock(async () => jsonResponse({})); + globalThis.fetch = fetchSpy as typeof fetch; + + const client = createREST({}); + const result = await client.get('/store/get'); + + expect(result).toEqual({ + data: null, + error: 'We could not find any AbacatePay secret, use REST({ secret })', + success: false, + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + test('retries retryable statuses and eventually succeeds', async () => { + let calls = 0; + + globalThis.fetch = mock(async () => { + calls++; + + if (calls < 3) + return jsonResponse( + { data: null, error: 'Unavailable', success: false }, + 503, + ); + + return jsonResponse({ data: { ok: true }, error: null, success: true }); + }) as typeof fetch; + + const client = createREST({ + secret: 'sk_test', + retry: { max: 5, backoff: () => 0 }, + }); + const result = await client.get('/store/get'); + + expect(calls).toBe(3); + expect(result).toEqual({ data: { ok: true }, error: null, success: true }); + }); + + test('does not retry a non-retryable status', async () => { + const fetchSpy = mock(async () => + jsonResponse({ data: null, error: 'Bad request', success: false }, 400), + ); + globalThis.fetch = fetchSpy as typeof fetch; + + const client = createREST({ + secret: 'sk_test', + retry: { max: 5, backoff: () => 0 }, + }); + await client.post('/checkouts/create', { body: {} }); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + }); + + test('gives up after exhausting retries without throwing', async () => { + globalThis.fetch = mock(async () => + jsonResponse({ data: null, error: 'Unavailable', success: false }, 503), + ) as typeof fetch; + + const client = createREST({ + secret: 'sk_test', + retry: { max: 2, backoff: () => 0 }, + }); + const result = await client.get('/store/get'); + + expect(result).toEqual({ + data: null, + error: '2 attempts were performed, all failed', + success: false, + }); + }); +}); diff --git a/packages/rest/tsconfig.json b/packages/rest/tsconfig.json index a47cede..12445ec 100644 --- a/packages/rest/tsconfig.json +++ b/packages/rest/tsconfig.json @@ -1,10 +1,8 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] } diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 6d67fba..78e3f18 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -45,13 +45,11 @@ Nunca utilize sua API key diretamente no código.
```ts -const TEN_REAIS_IN_CENTS = 1_000; - const checkout = await abacate.checkouts.create({ items: [ { - id: 'item_123', - amount: TEN_REAIS_IN_CENTS, + id: 'prod_123', + quantity: 1, }, ], }); @@ -73,47 +71,45 @@ const customers = await abacate.customers.list({ ## Versionamento -Você também pode usar facilmente a v1 da *AbacatePay* sem nenhum problema ou boilerplate, apenas passe `/v1` como sufixo da importação +O pacote é focado 100% na **v2** da API (`import { AbacatePay } from '@abacatepay/sdk'`). + +A v1 continua disponível pelo sufixo `/v1`, mas está **descontinuada**: ela é mantida congelada (sem novos recursos) apenas para quem ainda não migrou, e emite um aviso no console ao ser usada. ```ts +/** @deprecated Migre para a v2 (`@abacatepay/sdk`) */ import { AbacatePay } from '@abacatepay/sdk/v1' const client = AbacatePay({ secret }); ``` -
- -Você terá acesso a *todos os recursos* da v1, sem boilerplate, sem magia, apenas o SDK. -
- ```ts -const data = await abacate.withdraw.create({ +const { data, error, success } = await client.withdraw.create({ method: 'PIX', externalId: 'trx_abc123', ... }); - -console.log(data.receiptUrl); ```
## Tratamento de erros -Erros da API são normalizados e previsíveis com base no pacote [`@abacatepay/rest`](https://www.npmjs.com/package/@abacatepay/rest). +Nenhuma chamada do SDK lança exceção. Toda chamada resolve com o mesmo formato `{ data, error, success }` que a própria API da AbacatePay retorna — inclusive falhas de rede/timeout são normalizadas para esse formato pelo [`@abacatepay/rest`](https://www.npmjs.com/package/@abacatepay/rest).
```ts -try { - await abacate.subscriptions.create({ ... }); -} catch (error) { - if (error instanceof HTTPError) { - console.error(`An HTTP ocurred in route ${error.route} (Status ${error.status})`); - } +const { data, error, success } = await abacate.subscriptions.create({ ... }); + +if (!success) { + console.error(error); + + return; } + +console.log(data); ```
diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e5b9f9c..15a80ae 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,80 +1,80 @@ { - "name": "@abacatepay/sdk", - "version": "1.2.0", - "type": "module", - "sideEffects": false, - "license": "Apache-2.0", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - }, - "./v1": { - "types": "./dist/v1/index.d.ts", - "import": "./dist/v1/index.js" - }, - "./v1/*": { - "types": "./dist/v1/*.d.ts", - "import": "./dist/v1/*.js" - }, - "./v2": { - "types": "./dist/v2/index.d.ts", - "import": "./dist/v2/index.js" - }, - "./v2/*": { - "types": "./dist/v2/*.d.ts", - "import": "./dist/v2/*.js" - }, - "./v*": null - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && bun build src/index.ts --outdir dist --target node && bun run types", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "keywords": [ - "sdk", - "typescript", - "abacatepay", - "nodejs", - "bun", - "api-client" - ], - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "description": "An ergonomic SDK for integration with AbacatePay", - "dependencies": { - "@abacatepay/rest": "^0.0.1", - "@abacatepay/types": "^2.0.1" - } + "name": "@abacatepay/sdk", + "version": "2.0.0", + "type": "module", + "sideEffects": false, + "license": "Apache-2.0", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./v1": { + "types": "./dist/v1/index.d.ts", + "import": "./dist/v1/index.js" + }, + "./v1/*": { + "types": "./dist/v1/*.d.ts", + "import": "./dist/v1/*.js" + }, + "./v2": { + "types": "./dist/v2/index.d.ts", + "import": "./dist/v2/index.js" + }, + "./v2/*": { + "types": "./dist/v2/*.d.ts", + "import": "./dist/v2/*.js" + }, + "./v*": null + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && bun build src/index.ts --outdir dist --target node && bun run types", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "keywords": [ + "sdk", + "typescript", + "abacatepay", + "nodejs", + "bun", + "api-client" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "description": "An ergonomic SDK for integration with AbacatePay", + "dependencies": { + "@abacatepay/rest": "^1.0.0", + "@abacatepay/types": "^3.0.0" + } } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index fd65d37..b1cd2f9 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,4 +1,3 @@ -export { AbacatePayError, HTTPError } from '@abacatepay/rest'; export { API_BASE_URL, API_VERSION } from '@abacatepay/types'; export { version } from './version'; diff --git a/packages/sdk/src/v1/index.ts b/packages/sdk/src/v1/index.ts index 716367c..ba6c9eb 100644 --- a/packages/sdk/src/v1/index.ts +++ b/packages/sdk/src/v1/index.ts @@ -1,4 +1,4 @@ -import { REST } from '@abacatepay/rest'; +import { createREST } from '@abacatepay/rest'; import type { APIWithdraw, RESTGetCheckQRCodePixStatusData, @@ -25,12 +25,26 @@ import type { AbacatePayOptions } from './types'; export * from './types'; +let hasWarnedAboutV1 = false; + /** - * This is the main entry point for interacting with the AbacatePay API, + * This is the main entry point for interacting with the AbacatePay v1 API, * providing high-level, domain-oriented methods on top of the REST client. + * + * @deprecated The v1 API is frozen and no longer receives new features. + * Migrate to the default v2 export (`@abacatepay/sdk`) — see + * https://docs.abacatepay.com */ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { - const client = new REST({ + if (!hasWarnedAboutV1) { + hasWarnedAboutV1 = true; + + console.warn( + '[@abacatepay/sdk] v1 is deprecated, migrate to the default v2 export (`@abacatepay/sdk`) — see https://docs.abacatepay.com', + ); + } + + const client = createREST({ secret, ...rest, version: 1, diff --git a/packages/sdk/src/v2/index.ts b/packages/sdk/src/v2/index.ts index cebde47..071c6bd 100644 --- a/packages/sdk/src/v2/index.ts +++ b/packages/sdk/src/v2/index.ts @@ -1,7 +1,8 @@ -import { REST } from '@abacatepay/rest'; +import { createREST } from '@abacatepay/rest'; import type { RESTDeleteCouponData, RESTDeleteCustomerData, + RESTDeleteProductData, RESTGetCheckoutData, RESTGetCheckQRCodePixStatusData, RESTGetCouponData, @@ -12,21 +13,37 @@ import type { RESTGetListCouponsQueryParams, RESTGetListCustomersData, RESTGetListCustomersQueryParams, + RESTGetListPaymentLinksData, + RESTGetListPaymentLinksQueryParams, RESTGetListPayoutsData, RESTGetListPayoutsQueryParams, + RESTGetListPixTransfersData, + RESTGetListPixTransfersQueryParams, RESTGetListProductsData, RESTGetListProductsQueryParams, RESTGetListSubscriptionsData, RESTGetListSubscriptionsQueryParams, + RESTGetListTransparentsData, + RESTGetListTransparentsQueryParams, + RESTGetListWebhooksData, + RESTGetListWebhooksQueryParams, RESTGetMerchantData, RESTGetMRRData, + RESTGetPaymentLinkData, + RESTGetPixTransferData, + RESTGetPixTransferQueryParams, RESTGetProductData, RESTGetProductQueryParams, RESTGetRevenueByPeriodData, RESTGetRevenueByPeriodQueryParams, RESTGetSearchPayoutData, RESTGetStoreDetailsData, - RESTPatchToggleCouponStatusData, + RESTGetWebhookData, + RESTPostCancelSubscriptionData, + RESTPostChangeSubscriptionPlanBody, + RESTPostChangeSubscriptionPlanData, + RESTPostCreateBoletoBody, + RESTPostCreateBoletoData, RESTPostCreateCouponBody, RESTPostCreateCouponData, RESTPostCreateCustomerBody, @@ -35,13 +52,27 @@ import type { RESTPostCreateNewCheckoutData, RESTPostCreateNewPayoutBody, RESTPostCreateNewWPayoutData, + RESTPostCreatePaymentLinkBody, + RESTPostCreatePaymentLinkData, RESTPostCreateProductBody, RESTPostCreateProductData, RESTPostCreateQRCodePixBody, RESTPostCreateQRCodePixData, RESTPostCreateSubscriptionBody, RESTPostCreateSubscriptionData, + RESTPostCreateTransparentBody, + RESTPostCreateWebhookBody, + RESTPostCreateWebhookData, + RESTPostDeleteWebhookData, + RESTPostRecordSubscriptionUsageBody, + RESTPostRecordSubscriptionUsageData, + RESTPostRefundCheckoutData, + RESTPostRefundPaymentLinkData, + RESTPostRefundTransparentData, + RESTPostSendPixTransferBody, + RESTPostSendPixTransferData, RESTPostSimulateQRCodePixPaymentData, + RESTPostToggleCouponStatusData, } from '@abacatepay/types/v2'; import { Routes } from '@abacatepay/types/v2'; import type { AbacatePayOptions } from './types'; @@ -51,9 +82,12 @@ export * from './types'; /** * This is the main entry point for interacting with the AbacatePay API, * providing high-level, domain-oriented methods on top of the REST client. + * + * No method here ever throws — every call resolves to the exact + * `{ data, error, success }` shape the AbacatePay API itself returns. */ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { - const client = new REST({ + const client = createREST({ secret, ...rest, version: 2, @@ -89,7 +123,7 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { * @returns Deletion result. */ delete(id: string) { - return client.delete(Routes.customers.delete, { + return client.post(Routes.customers.delete, { body: { id }, }); }, @@ -158,22 +192,108 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { get(id: string) { return client.get(Routes.checkouts.get(id)); }, + + /** + * Refund a completed checkout. + * + * @param id Public ID of the resource to refund (e.g. `bill_`, `char_`, `pix_char_`, `card_`). + * @param reason Optional refund reason, shown in the transaction history. + * @returns The public ID of the created refund transaction. + */ + refund(id: string, reason?: string) { + return client.post( + Routes.checkouts.refund, + { body: { id, reason } }, + ); + }, + }, + + /** + * Reusable payment link operations — unlike a checkout, the same link + * can be paid by multiple customers. + */ + paymentLinks: { + /** + * Create a new payment link. + * + * @param body Payment link creation payload. + * @returns The created payment link. + */ + create(body: RESTPostCreatePaymentLinkBody) { + return client.post( + Routes.paymentLinks.create, + { body }, + ); + }, + + /** + * List payment links with optional pagination. + * + * @param query Optional query parameters. + * @returns A paginated list of payment links. + */ + list(query?: RESTGetListPaymentLinksQueryParams) { + return client.get( + Routes.paymentLinks.list(query), + ); + }, + + /** + * Retrieve a payment link by ID. + * + * @param id Payment link ID. + * @returns The payment link data. + */ + get(id: string) { + return client.get(Routes.paymentLinks.get(id)); + }, + + /** + * Refund a payment made through a payment link. + * + * @param id Public ID of the resource to refund. + * @param reason Optional refund reason, shown in the transaction history. + * @returns The public ID of the created refund transaction. + */ + refund(id: string, reason?: string) { + return client.post( + Routes.paymentLinks.refund, + { body: { id, reason } }, + ); + }, }, /** - * PIX payment operations. + * Embedded PIX QR-code charge operations (Receiving a payment). */ pix: { /** - * Create a new PIX QR Code payment. + * Create a new PIX QR Code charge. * * @param body PIX creation payload. * @returns The created PIX QR Code. */ create(body: RESTPostCreateQRCodePixBody) { + const wireBody: RESTPostCreateTransparentBody = { + method: 'PIX', + data: body, + }; + return client.post( - Routes.transparents.createQRCode, - { body }, + Routes.transparents.create, + { body: wireBody }, + ); + }, + + /** + * List embedded PIX QR-code (and Boleto) charges. + * + * @param query Optional query parameters. + * @returns A paginated list of transparent charges. + */ + list(query?: RESTGetListTransparentsQueryParams) { + return client.get( + Routes.transparents.list(query), ); }, @@ -202,6 +322,82 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { Routes.transparents.checkStatus(id), ); }, + + /** + * Refund an embedded PIX charge. + * + * @param id Public ID of the resource to refund. + * @param reason Optional refund reason, shown in the transaction history. + * @returns The public ID of the created refund transaction. + */ + refund(id: string, reason?: string) { + return client.post( + Routes.transparents.refund, + { body: { id, reason } }, + ); + }, + }, + + /** + * Embedded Boleto charge operations. + */ + boleto: { + /** + * Create a new Boleto charge. + * + * @param body Boleto creation payload. + * @returns The created Boleto. + */ + create(body: RESTPostCreateBoletoBody) { + const wireBody: RESTPostCreateTransparentBody = { + method: 'BOLETO', + data: body, + }; + + return client.post( + Routes.transparents.create, + { body: wireBody }, + ); + }, + }, + + /** + * Outbound PIX transfer operations (Sending money to a third-party PIX key). + */ + transfers: { + /** + * Send a PIX transfer to a third-party PIX key. + * + * @param body Transfer payload. + * @returns The created transfer. + */ + send(body: RESTPostSendPixTransferBody) { + return client.post(Routes.transfers.send, { + body, + }); + }, + + /** + * Retrieve a PIX transfer by ID or external ID. + * + * @param query Query parameters, at least one of `id`/`externalId` is required. + * @returns The transfer data. + */ + get(query: RESTGetPixTransferQueryParams) { + return client.get(Routes.transfers.get(query)); + }, + + /** + * List PIX transfers with optional pagination. + * + * @param query Optional query parameters. + * @returns A paginated list of transfers. + */ + list(query?: RESTGetListPixTransfersQueryParams) { + return client.get( + Routes.transfers.list(query), + ); + }, }, /** @@ -227,7 +423,7 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { * @returns Deletion result. */ delete(id: string) { - return client.delete(Routes.coupons.delete, { + return client.post(Routes.coupons.delete, { body: { id }, }); }, @@ -259,7 +455,7 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { * @returns Updated coupon status. */ toggleStatus(id: string) { - return client.patch( + return client.post( Routes.coupons.toggleStatus, { body: { id } }, ); @@ -316,7 +512,7 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { }, /** - * Payout management operations. + * Payout management operations (Withdrawing to your own account). */ payouts: { /** @@ -374,13 +570,62 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { * List subscriptions with optional pagination. * * @param query Optional query parameters. - * @returns A list of subscriptions. + * @returns A paginated list of subscriptions. */ list(query?: RESTGetListSubscriptionsQueryParams) { return client.get( Routes.subscriptions.list(query), ); }, + + /** + * Cancel a subscription immediately (No grace period). + * + * @param id Subscription ID. + * @returns The cancelled subscription. + */ + cancel(id: string) { + return client.post( + Routes.subscriptions.cancel, + { body: { id } }, + ); + }, + + /** + * Change the product of a subscription. The change is applied at + * the next billing cycle; only one pending change can exist at a + * time. + * + * @param id Subscription ID. + * @param body The new product and quantity. + * @returns The requested plan change. + */ + changePlan( + id: string, + body: Omit, + ) { + return client.post( + Routes.subscriptions.changePlan, + { body: { id, ...body } }, + ); + }, + + /** + * Record usage for a pay-as-you-go (no-cycle) product. + * + * @param id Subscription ID. + * @param body The product, unit count, and action. + * @returns The recorded usage. + */ + recordUsage( + id: string, + body: Omit, + ) { + return client.post( + Routes.subscriptions.recordUsage, + { body: { id, ...body } }, + ); + }, }, /** @@ -418,6 +663,65 @@ export const AbacatePay = ({ secret, rest }: AbacatePayOptions) => { list(query?: RESTGetListProductsQueryParams) { return client.get(Routes.products.list(query)); }, + + /** + * Permanently delete a product. + * + * @param id Product ID. + * @returns Deletion result. + */ + delete(id: string) { + return client.post(Routes.products.delete(id)); + }, + }, + + /** + * Webhook management operations. + */ + webhooks: { + /** + * Create a new webhook. + * + * @param body Webhook creation payload. + * @returns The created webhook. + */ + create(body: RESTPostCreateWebhookBody) { + return client.post(Routes.webhooks.create, { + body, + }); + }, + + /** + * List webhooks with optional filters. + * + * @param query Optional query parameters. + * @returns A paginated list of webhooks. + */ + list(query?: RESTGetListWebhooksQueryParams) { + return client.get(Routes.webhooks.list(query)); + }, + + /** + * Retrieve a webhook by ID. + * + * @param id Webhook ID. + * @returns The webhook data. + */ + get(id: string) { + return client.get(Routes.webhooks.get(id)); + }, + + /** + * Permanently delete a webhook. + * + * @param id Webhook ID. + * @returns Deletion result. + */ + delete(id: string) { + return client.post(Routes.webhooks.delete, { + body: { id }, + }); + }, }, }; }; diff --git a/packages/sdk/src/version.ts b/packages/sdk/src/version.ts index 748e5c6..c135b0e 100644 --- a/packages/sdk/src/version.ts +++ b/packages/sdk/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/sdk](https://www.npmjs.com/package/@abacatepay/sdk). */ -export const version = '1.2.0' as const; +export const version = '2.0.0' as const; diff --git a/packages/sdk/test/index.test.ts b/packages/sdk/test/index.test.ts index e69de29..67a9271 100644 --- a/packages/sdk/test/index.test.ts +++ b/packages/sdk/test/index.test.ts @@ -0,0 +1,109 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test'; +import { AbacatePay } from '../src'; +import { AbacatePay as AbacatePayV1 } from '../src/v1'; + +const jsonResponse = (body: unknown, status = 200) => + new Response(JSON.stringify(body), { + status, + headers: { 'Content-Type': 'application/json' }, + }); + +const originalFetch = globalThis.fetch; +const originalWarn = console.warn; + +afterEach(() => { + globalThis.fetch = originalFetch; + console.warn = originalWarn; +}); + +describe('AbacatePay (v2)', () => { + test('customers.get builds the right request and returns the response untouched', async () => { + const fetchSpy = mock(async (url: string) => { + expect(url).toBe( + 'https://api.abacatepay.com/v2/customers/get?id=cust_123', + ); + + return jsonResponse({ + data: { id: 'cust_123', email: 'foo@bar.com' }, + error: null, + success: true, + }); + }); + globalThis.fetch = fetchSpy as typeof fetch; + + const abacate = AbacatePay({ secret: 'sk_test' }); + const result = await abacate.customers.get('cust_123'); + + expect(result).toEqual({ + data: { id: 'cust_123', email: 'foo@bar.com' }, + error: null, + success: true, + }); + }); + + test('checkouts.create posts the body as-is and never throws on an API error', async () => { + const fetchSpy = mock(async (url: string, init?: RequestInit) => { + expect(url).toBe('https://api.abacatepay.com/v2/checkouts/create'); + expect(init?.method).toBe('POST'); + expect(JSON.parse(init?.body as string)).toEqual({ + items: [{ id: 'prod_123', quantity: 1 }], + }); + + return jsonResponse( + { data: null, error: 'Invalid item', success: false }, + 400, + ); + }); + globalThis.fetch = fetchSpy as typeof fetch; + + const abacate = AbacatePay({ secret: 'sk_test' }); + const result = await abacate.checkouts.create({ + items: [{ id: 'prod_123', quantity: 1 }], + }); + + expect(result).toEqual({ + data: null, + error: 'Invalid item', + success: false, + }); + }); + + test('pix.create wraps the body in the { method, data } envelope the API expects', async () => { + const fetchSpy = mock(async (url: string, init?: RequestInit) => { + expect(url).toBe('https://api.abacatepay.com/v2/transparents/create'); + expect(JSON.parse(init?.body as string)).toEqual({ + method: 'PIX', + data: { amount: 1000 }, + }); + + return jsonResponse({ + data: { id: 'pix_char_123' }, + error: null, + success: true, + }); + }); + globalThis.fetch = fetchSpy as typeof fetch; + + const abacate = AbacatePay({ secret: 'sk_test' }); + await abacate.pix.create({ amount: 1000 }); + }); +}); + +describe('AbacatePay (v1, deprecated)', () => { + test('warns once when used', async () => { + globalThis.fetch = mock(async () => + jsonResponse({ data: [], error: null, success: true }), + ) as typeof fetch; + + const warnings: unknown[] = []; + console.warn = (...args: unknown[]) => { + warnings.push(args.join(' ')); + }; + + AbacatePayV1({ secret: 'sk_test' }); + + expect( + warnings.some((message) => `${message}`.includes('deprecated')), + ).toBe(true); + }); +}); diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/supabase/README.md b/packages/supabase/README.md index b022d33..fced7e7 100644 --- a/packages/supabase/README.md +++ b/packages/supabase/README.md @@ -35,16 +35,22 @@ Nenhuma dependência extra é necessária. O pacote já vem pronto para uso com ```ts import { Webhooks } from '@abacatepay/supabase'; -export const POST = Webhooks({ +const { ok, handler, error } = Webhooks({ secret: '...', onPayload({ event, data }) { ... }, }); + +if (!ok) throw new Error(error); + +export const POST = handler; ```
+`Webhooks(...)` nunca lança exceção — se o `secret` estiver ausente, ela retorna `{ ok: false, error }` em vez de `{ ok: true, handler }`. + ## Segurança por padrão
@@ -65,11 +71,12 @@ Você pode lidar com eventos específicos sem boilerplate:
```ts -Webhooks({ - onBillingPaid({ data }) { +const { handler } = Webhooks({ + secret, + onCheckoutCompleted({ data }) { console.log('Cobrança paga:', data.payment.amount); }, - onPayoutDone({ data }) { + onPayoutCompleted({ data }) { console.log('Payout concluído:', data.transaction.id); }, onPayoutFailed({ data }) { @@ -82,7 +89,7 @@ Webhooks({

Ou tratar tudo de forma genérica:

```ts -Webhooks({ +const { handler } = Webhooks({ secret, onPayload({ data, event }) { console.log(event, data); diff --git a/packages/supabase/package.json b/packages/supabase/package.json index e44d783..50737f5 100644 --- a/packages/supabase/package.json +++ b/packages/supabase/package.json @@ -1,62 +1,62 @@ { - "name": "@abacatepay/supabase", - "version": "1.0.3", - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "main": "./dist/index.js", - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "description": "Checkouts and Webhooks made simple with AbacatePay and Supabase.", - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "files": [ - "dist" - ], - "keywords": [ - "abacatepay", - "payments", - "supabase", - "api", - "webhooks" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run scripts/version.ts && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build" - }, - "sideEffects": false, - "type": "module", - "types": "./dist/index.d.ts", - "dependencies": { - "@abacatepay/adapters": "^1.0.1" - } + "name": "@abacatepay/supabase", + "version": "2.0.0", + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "main": "./dist/index.js", + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "description": "Checkouts and Webhooks made simple with AbacatePay and Supabase.", + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "files": [ + "dist" + ], + "keywords": [ + "abacatepay", + "payments", + "supabase", + "api", + "webhooks" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run scripts/version.ts && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build" + }, + "sideEffects": false, + "type": "module", + "types": "./dist/index.d.ts", + "dependencies": { + "@abacatepay/adapters": "^2.0.0" + } } diff --git a/packages/supabase/src/errors.ts b/packages/supabase/src/errors.ts deleted file mode 100644 index c8e6b88..0000000 --- a/packages/supabase/src/errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -interface AbacatePaySupabaseErrorOptions { - code: string; -} - -export class AbacatePaySupabaseError extends Error { - /** - * The code of the error. - */ - readonly code: string; - /** - * URL for the docs of [@abacatepay/supabase](https://www.npmjs.com/package/@abacatepay/supabase) - */ - static readonly docs = 'https://docs.abacatepay.com/pages/ecosystem/supabase'; - - constructor(message: string, options: AbacatePaySupabaseErrorOptions) { - super(message); - - this.code = options.code; - this.name = 'AbacatePaySupabaseError'; - } -} diff --git a/packages/supabase/src/index.ts b/packages/supabase/src/index.ts index 7372c16..2e3e37c 100644 --- a/packages/supabase/src/index.ts +++ b/packages/supabase/src/index.ts @@ -4,63 +4,73 @@ import { verify, type WebhookOptions, } from '@abacatepay/adapters/webhooks'; -import { AbacatePaySupabaseError } from './errors'; const NO_CONTENT = 204; const BAD_REQUEST = 400; const UNAUTHORIZED = 401; -export { AbacatePaySupabaseError } from './errors'; export { version } from './version'; -export const Webhooks = (options: WebhookOptions) => { +export type WebhooksHandler = (req: Request) => Promise; + +/** + * Result of {@link Webhooks}. Never throws — a missing secret resolves to + * `{ ok: false, error }` instead. + */ +export type WebhooksResult = + | { ok: true; error: null; handler: WebhooksHandler } + | { ok: false; error: string; handler: null }; + +export const Webhooks = (options: WebhookOptions): WebhooksResult => { if (!options.secret) - throw new AbacatePaySupabaseError('Webhook secret is missing.', { - code: 'WEBHOOK_SECRET_MISSING', - }); + return { ok: false, error: 'Webhook secret is missing.', handler: null }; - return async (req: Request): Promise => { - const url = new URL(req.url); - const webhookSecret = url.searchParams.get('webhookSecret'); + return { + ok: true, + error: null, + handler: async (req: Request): Promise => { + const url = new URL(req.url); + const webhookSecret = url.searchParams.get('webhookSecret'); - if (webhookSecret !== options.secret) - return new Response(JSON.stringify({ error: 'Unauthorized' }), { - status: UNAUTHORIZED, - }); + if (webhookSecret !== options.secret) + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: UNAUTHORIZED, + }); - const signature = req.headers.get('x-webhook-signature'); + const signature = req.headers.get('x-webhook-signature'); - if (!signature) - return new Response(JSON.stringify({ error: 'Missing signature' }), { - status: BAD_REQUEST, - }); + if (!signature) + return new Response(JSON.stringify({ error: 'Missing signature' }), { + status: BAD_REQUEST, + }); - const raw = await req.text(); + const raw = await req.text(); - if (!verify(raw, signature)) - return new Response(JSON.stringify({ error: 'Invalid signature' }), { - status: UNAUTHORIZED, - }); + if (!verify(raw, signature)) + return new Response(JSON.stringify({ error: 'Invalid signature' }), { + status: UNAUTHORIZED, + }); - let parsed: unknown; + let parsed: unknown; - try { - parsed = JSON.parse(raw); - } catch { - return new Response(JSON.stringify({ error: 'Invalid JSON' }), { - status: BAD_REQUEST, - }); - } + try { + parsed = JSON.parse(raw); + } catch { + return new Response(JSON.stringify({ error: 'Invalid JSON' }), { + status: BAD_REQUEST, + }); + } - const { data, success } = parse(parsed); + const { data, success } = parse(parsed); - if (!success) - return new Response(JSON.stringify({ error: 'Invalid payload' }), { - status: BAD_REQUEST, - }); + if (!success) + return new Response(JSON.stringify({ error: 'Invalid payload' }), { + status: BAD_REQUEST, + }); - await dispatch(data, options); + await dispatch(data, options); - return new Response(null, { status: NO_CONTENT }); + return new Response(null, { status: NO_CONTENT }); + }, }; }; diff --git a/packages/supabase/src/version.ts b/packages/supabase/src/version.ts index 9295cda..bca9ccd 100644 --- a/packages/supabase/src/version.ts +++ b/packages/supabase/src/version.ts @@ -3,4 +3,4 @@ /** * Current version of [@abacatepay/supabase](https://www.npmjs.com/package/@abacatepay/supabase). */ -export const version = '1.0.3' as const; +export const version = '2.0.0' as const; diff --git a/packages/supabase/test/index.test.ts b/packages/supabase/test/index.test.ts index e69de29..0de6407 100644 --- a/packages/supabase/test/index.test.ts +++ b/packages/supabase/test/index.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, test } from 'bun:test'; +import { createHmac } from 'node:crypto'; +import { ABACATEPAY_SHARED_KEY } from '@abacatepay/adapters/webhooks'; +import { Webhooks } from '../src'; + +const sign = (raw: string) => + createHmac('sha256', ABACATEPAY_SHARED_KEY).update(raw).digest('base64'); + +describe('Webhooks', () => { + test('never throws on a missing secret, resolves { ok: false } instead', () => { + const result = Webhooks({}); + + expect(result.ok).toBe(false); + expect(result.handler).toBeNull(); + if (result.ok) return; + expect(result.error).toContain('secret'); + }); + + test('rejects a mismatched webhookSecret query param with 401', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const res = await handler( + new Request('https://example.com/webhooks?webhookSecret=wrong'), + ); + + expect(res.status).toBe(401); + }); + + test('returns 400 on a malformed JSON body instead of throwing', async () => { + const { ok, handler } = Webhooks({ secret: 'whsec_test' }); + if (!ok) throw new Error('expected ok'); + + const body = '{not valid json'; + const res = await handler( + new Request('https://example.com/webhooks?webhookSecret=whsec_test', { + method: 'POST', + headers: { 'x-webhook-signature': sign(body) }, + body, + }), + ); + + expect(res.status).toBe(400); + }); + + test('dispatches a validly signed event', async () => { + let dispatched = false; + + const { ok, handler } = Webhooks({ + secret: 'whsec_test', + onPayoutCompleted() { + dispatched = true; + }, + }); + if (!ok) throw new Error('expected ok'); + + const body = JSON.stringify({ + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + status: 'COMPLETE', + }, + }, + }); + + const res = await handler( + new Request('https://example.com/webhooks?webhookSecret=whsec_test', { + method: 'POST', + headers: { 'x-webhook-signature': sign(body) }, + body, + }), + ); + + expect(res.status).toBe(204); + expect(dispatched).toBe(true); + }); +}); diff --git a/packages/supabase/tsconfig.json b/packages/supabase/tsconfig.json index 56c013c..154bbbc 100644 --- a/packages/supabase/tsconfig.json +++ b/packages/supabase/tsconfig.json @@ -1,11 +1,9 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "paths": {} - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "paths": {} + }, + "include": ["src"] } diff --git a/packages/typebox/package.json b/packages/typebox/package.json index 9a9b2fa..1b3a42b 100644 --- a/packages/typebox/package.json +++ b/packages/typebox/package.json @@ -1,91 +1,91 @@ { - "name": "@abacatepay/typebox", - "version": "1.0.1", - "description": "Official TypeBox schemas for the AbacatePay API, including requests, responses, and webhooks.", - "type": "module", - "sideEffects": false, - "license": "Apache-2.0", - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - }, - "./v1": { - "import": "./dist/v1/index.js", - "types": "./dist/v1/index.d.ts" - }, - "./v1/*": { - "import": "./dist/v1/*.js", - "types": "./dist/v1/*.d.ts" - }, - "./v2": { - "import": "./dist/v2/index.js", - "types": "./dist/v2/index.d.ts" - }, - "./v2/*": { - "import": "./dist/v2/*.js", - "types": "./dist/v2/*.d.ts" - }, - "./v*": null - }, - "typesVersions": { - "*": { - "v1": [ - "dist/v1/index.d.ts" - ], - "v2": [ - "dist/v2/index.d.ts" - ] - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run gen:version && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build", - "gen:version": "bun run scripts/version.ts" - }, - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "keywords": [ - "abacatepay", - "payments", - "typebox", - "api", - "typescript" - ], - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "@sinclair/typebox": "^0.34.47", - "typescript": "^5" - } + "name": "@abacatepay/typebox", + "version": "2.0.0", + "description": "Official TypeBox schemas for the AbacatePay API, including requests, responses, and webhooks.", + "type": "module", + "sideEffects": false, + "license": "Apache-2.0", + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./v1": { + "import": "./dist/v1/index.js", + "types": "./dist/v1/index.d.ts" + }, + "./v1/*": { + "import": "./dist/v1/*.js", + "types": "./dist/v1/*.d.ts" + }, + "./v2": { + "import": "./dist/v2/index.js", + "types": "./dist/v2/index.d.ts" + }, + "./v2/*": { + "import": "./dist/v2/*.js", + "types": "./dist/v2/*.d.ts" + }, + "./v*": null + }, + "typesVersions": { + "*": { + "v1": [ + "dist/v1/index.d.ts" + ], + "v2": [ + "dist/v2/index.d.ts" + ] + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run gen:version && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build", + "gen:version": "bun run scripts/version.ts" + }, + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "keywords": [ + "abacatepay", + "payments", + "typebox", + "api", + "typescript" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "@sinclair/typebox": "^0.34.47", + "typescript": "^5" + } } diff --git a/packages/typebox/src/v2/index.ts b/packages/typebox/src/v2/index.ts index 82a3be9..c9f20f5 100644 --- a/packages/typebox/src/v2/index.ts +++ b/packages/typebox/src/v2/index.ts @@ -1,6 +1,7 @@ export * from './resources/checkout'; export * from './resources/coupon'; export * from './resources/customer'; +export * from './resources/paymentLink'; export * from './resources/payout'; export * from './resources/pix'; export * from './resources/product'; diff --git a/packages/typebox/src/v2/resources/checkout.ts b/packages/typebox/src/v2/resources/checkout.ts index 35df8f2..c250aa2 100644 --- a/packages/typebox/src/v2/resources/checkout.ts +++ b/packages/typebox/src/v2/resources/checkout.ts @@ -21,7 +21,7 @@ export type PaymentStatus = Static; /** * https://docs.abacatepay.com/pages/payment/create#body-methods */ -export const PaymentMethod = StringEnum(['PIX', 'CARD'], { +export const PaymentMethod = StringEnum(['PIX', 'CARD', 'BOLETO'], { examples: ['PIX'], description: 'Payment method.', }); @@ -31,6 +31,19 @@ export const PaymentMethod = StringEnum(['PIX', 'CARD'], { */ export type PaymentMethod = Static; +/** + * https://docs.abacatepay.com/pages/payment/create#body-frequency + */ +export const PaymentFrequency = StringEnum( + ['ONE_TIME', 'MULTIPLE_PAYMENTS', 'SUBSCRIPTION'], + { examples: ['ONE_TIME'], description: 'Billing frequency.' }, +); + +/** + * https://docs.abacatepay.com/pages/payment/create#body-frequency + */ +export type PaymentFrequency = Static; + /** * https://docs.abacatepay.com/pages/checkouts/reference#estrutura */ @@ -131,6 +144,48 @@ export const APICheckout = t.Object({ examples: ['2024-11-04T18:38:28.573Z'], description: 'Charge last updated date and time.', }), + frequency: t.Optional(PaymentFrequency), + upSellProductId: t.Optional( + t.Union([t.Null(), t.String()], { + examples: [null], + description: 'ID of an additional product offered as an upsell.', + }), + ), + interest: t.Optional( + t.Union( + [ + t.Null(), + t.Object({ + value: t.Integer({ + examples: [100], + description: 'Monthly interest rate, in hundredths of a percent.', + }), + }), + ], + { + examples: [null], + description: 'Late interest configuration (Applies to BOLETO).', + }, + ), + ), + fine: t.Optional( + t.Union( + [ + t.Null(), + t.Object({ + value: t.Integer({ examples: [200], description: 'Fine value.' }), + type: StringEnum(['PERCENTAGE', 'FIXED'], { + examples: ['PERCENTAGE'], + description: 'Type of fine applied.', + }), + }), + ], + { + examples: [null], + description: 'Late fine configuration (Applies to BOLETO).', + }, + ), + ), }); /** diff --git a/packages/typebox/src/v2/resources/paymentLink.ts b/packages/typebox/src/v2/resources/paymentLink.ts new file mode 100644 index 0000000..b1dae76 --- /dev/null +++ b/packages/typebox/src/v2/resources/paymentLink.ts @@ -0,0 +1,60 @@ +import { type Static, Type as t } from '@sinclair/typebox'; +import { PaymentStatus } from './checkout'; + +/** + * A reusable payment link — unlike a Checkout, the same link can be paid by + * multiple customers instead of being tied to a single transaction. + * + * https://docs.abacatepay.com/pages/payment-links/reference + */ +export const APIPaymentLink = t.Object({ + id: t.String({ + examples: ['bill_abc123xyz'], + description: 'Unique payment link identifier.', + }), + url: t.String({ + format: 'uri', + examples: ['https://app.abacatepay.com/pay/bill_abc123xyz'], + description: 'Shareable checkout URL.', + }), + amount: t.Integer({ + examples: [4000], + description: 'Total amount in cents.', + }), + paidAmount: t.Union([t.Null(), t.Integer()], { + examples: [null], + description: 'Amount paid in cents. `null` if it has not yet been paid.', + }), + status: PaymentStatus, + frequency: t.Literal('MULTIPLE_PAYMENTS', { + examples: ['MULTIPLE_PAYMENTS'], + description: 'Always `MULTIPLE_PAYMENTS` for payment links.', + }), + items: t.Array( + t.Object({ + id: t.String({ description: 'Product ID.' }), + quantity: t.Integer({ minimum: 1, description: 'Item quantity.' }), + }), + { + examples: [[{ id: 'prod_123', quantity: 1 }]], + description: 'List of items included in the payment link.', + }, + ), + externalId: t.Union([t.Null(), t.String()], { + examples: [null], + description: 'Reference ID in your system.', + }), + createdAt: t.Date({ + examples: ['2025-01-01T00:00:00Z'], + description: 'Payment link creation date and time.', + }), + updatedAt: t.Date({ + examples: ['2025-01-01T00:00:00Z'], + description: 'Payment link last updated date and time.', + }), +}); + +/** + * https://docs.abacatepay.com/pages/payment-links/reference + */ +export type APIPaymentLink = Static; diff --git a/packages/typebox/src/v2/resources/pix.ts b/packages/typebox/src/v2/resources/pix.ts index 0cddb4d..82506c4 100644 --- a/packages/typebox/src/v2/resources/pix.ts +++ b/packages/typebox/src/v2/resources/pix.ts @@ -1,4 +1,5 @@ import { type Static, Type as t } from '@sinclair/typebox'; +import { StringEnum } from '../../utils'; import { PaymentStatus } from './checkout'; /** @@ -31,6 +32,12 @@ export const APIQRCodePIX = t.Object({ examples: [80], description: 'Platform fee in cents. Example: 80 means R$0.80.', }), + receiptUrl: t.Optional( + t.Union([t.Null(), t.String({ format: 'uri' })], { + examples: [null], + description: 'Payment receipt URL.', + }), + ), createdAt: t.Date({ examples: ['2025-01-01T12:00:00Z'], description: 'QRCode PIX creation date and time.', @@ -43,9 +50,162 @@ export const APIQRCodePIX = t.Object({ examples: ['2025-01-02T12:00:00Z'], description: 'QRCode expiration date and time.', }), + metadata: t.Optional( + t.Record(t.String(), t.Any(), { + examples: [{}], + description: 'Additional metadata attached to the charge.', + }), + ), }); /** * https://docs.abacatepay.com/pages/transparents/reference */ export type APIQRCodePIX = Static; + +/** + * https://docs.abacatepay.com/pages/transparents/boleto + */ +export const APIBoleto = t.Object({ + id: t.String({ + examples: ['bill_char_123456'], + description: 'Unique Boleto identifier.', + }), + amount: t.Integer({ + examples: [4000], + description: 'Charge amount in cents.', + }), + status: PaymentStatus, + devMode: t.Boolean({ + examples: [true], + description: + 'Indicates whether the charge is in a testing (true) or production (false) environment.', + }), + barCode: t.String({ + examples: ['00190.00009 03384.318207'], + description: 'Boleto barcode.', + }), + url: t.String({ + format: 'uri', + examples: ['https://myshop.com/boleto/bill_char_123456'], + description: 'URL to view/print the Boleto.', + }), + pix: t.Object( + { + brCode: t.String({ + examples: ['00020101021226950014br.gov.bcb.pix'], + description: 'PIX code (copy-and-paste) for payment.', + }), + brCodeBase64: t.String({ + examples: ['data:image/png;base64,iVBORw0KGgoAAA'], + description: + 'PIX code in Base64 format (Useful for displaying in images).', + }), + }, + { description: 'PIX alternative for paying the same Boleto.' }, + ), + platformFee: t.Integer({ + examples: [80], + description: 'Platform fee in cents.', + }), + receiptUrl: t.Union([t.Null(), t.String({ format: 'uri' })], { + examples: [null], + description: 'Payment receipt URL.', + }), + createdAt: t.Date({ + examples: ['2025-01-01T12:00:00Z'], + description: 'Boleto creation date and time.', + }), + updatedAt: t.Date({ + examples: ['2025-01-01T12:00:00Z'], + description: 'Boleto last updated date and time.', + }), + expiresAt: t.Date({ + examples: ['2025-01-05T12:00:00Z'], + description: 'Boleto expiration date and time.', + }), + metadata: t.Optional( + t.Record(t.String(), t.Any(), { + examples: [{}], + description: 'Additional metadata attached to the charge.', + }), + ), +}); + +/** + * https://docs.abacatepay.com/pages/transparents/boleto + */ +export type APIBoleto = Static; + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export const PixTransferStatus = StringEnum( + ['PENDING', 'EXPIRED', 'CANCELLED', 'COMPLETE', 'REFUNDED'], + { examples: ['PENDING'], description: 'Transfer status.' }, +); + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export type PixTransferStatus = Static; + +/** + * https://docs.abacatepay.com/pages/pix/create + */ +export const PixKeyType = StringEnum( + ['CPF', 'CNPJ', 'PHONE', 'EMAIL', 'RANDOM', 'BR_CODE'], + { examples: ['EMAIL'], description: 'Type of the destination PIX key.' }, +); + +/** + * https://docs.abacatepay.com/pages/pix/create + */ +export type PixKeyType = Static; + +/** + * Outbound PIX transfer to a third-party PIX key (Sending money out). + * + * https://docs.abacatepay.com/pages/pix/reference + */ +export const APIPixTransfer = t.Object({ + id: t.String({ + examples: ['pix_transfer_123'], + description: 'Unique transfer identifier.', + }), + status: PixTransferStatus, + devMode: t.Boolean({ + examples: [false], + description: + 'Indicates whether the transfer is in a testing (true) or production (false) environment.', + }), + receiptUrl: t.Union([t.Null(), t.String({ format: 'uri' })], { + examples: [null], + description: 'Proof-of-transfer URL, available once completed.', + }), + amount: t.Integer({ + examples: [4000], + description: 'Transfer amount in cents.', + }), + platformFee: t.Integer({ + examples: [80], + description: 'Platform fee in cents.', + }), + externalId: t.Union([t.Null(), t.String()], { + examples: [null], + description: 'Unique transfer identifier in your system.', + }), + createdAt: t.Date({ + examples: ['2025-01-01T12:00:00Z'], + description: 'Transfer creation date and time.', + }), + updatedAt: t.Date({ + examples: ['2025-01-01T12:00:00Z'], + description: 'Transfer last updated date and time.', + }), +}); + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export type APIPixTransfer = Static; diff --git a/packages/typebox/src/v2/resources/subscription.ts b/packages/typebox/src/v2/resources/subscription.ts index a27b6dc..8e6f864 100644 --- a/packages/typebox/src/v2/resources/subscription.ts +++ b/packages/typebox/src/v2/resources/subscription.ts @@ -120,3 +120,92 @@ export const APISubscription = t.Object({ * https://docs.abacatepay.com/pages/subscriptions/reference#estrutura */ export type APISubscription = Static; + +/** + * Result of `POST /subscriptions/change-plan`. + * + * https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export const APISubscriptionPlanChange = t.Object({ + id: t.String({ + examples: ['sub_change_123'], + description: 'Unique identifier of this plan-change request.', + }), + subscriptionId: t.String({ + examples: ['subs_abc123xyz'], + description: 'ID of the subscription being changed.', + }), + status: StringEnum(['PENDING', 'APPLIED', 'CANCELLED'], { + examples: ['PENDING'], + description: 'Status of the plan-change request.', + }), + productId: t.String({ + examples: ['prod_123'], + description: 'ID of the new product.', + }), + quantity: t.Integer({ + examples: [1], + description: 'New quantity for the product.', + }), + newAmount: t.Integer({ + examples: [4000], + description: + 'Amount that will be charged once the change is applied, in cents.', + }), + requestedAt: t.Date({ + examples: ['2025-01-01T00:00:00Z'], + description: 'When the change was requested.', + }), +}); + +/** + * https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type APISubscriptionPlanChange = Static< + typeof APISubscriptionPlanChange +>; + +/** + * Result of `POST /subscriptions/record-usage`. + * + * https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export const APISubscriptionUsageRecord = t.Object({ + id: t.String({ + examples: ['usage_123'], + description: 'Unique identifier of this usage record.', + }), + subscriptionId: t.String({ + examples: ['subs_abc123xyz'], + description: 'ID of the subscription the usage was recorded against.', + }), + productId: t.String({ + examples: ['prod_123'], + description: 'ID of the pay-as-you-go product the usage applies to.', + }), + units: t.Integer({ examples: [5], description: 'Number of units recorded.' }), + unitPrice: t.Integer({ + examples: [100], + description: 'Price per unit, in cents.', + }), + action: StringEnum(['add', 'subtract'], { + examples: ['add'], + description: + 'Whether the units were added to or subtracted from the current cycle.', + }), + installmentNumber: t.Integer({ + examples: [1], + description: 'The pending installment this usage record was attached to.', + }), + recordedAt: t.Date({ + examples: ['2025-01-01T00:00:00Z'], + description: 'When the usage was recorded.', + }), +}); + +/** + * https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export type APISubscriptionUsageRecord = Static< + typeof APISubscriptionUsageRecord +>; diff --git a/packages/typebox/src/v2/rest.ts b/packages/typebox/src/v2/rest.ts index 1581faa..010afc2 100644 --- a/packages/typebox/src/v2/rest.ts +++ b/packages/typebox/src/v2/rest.ts @@ -1,17 +1,28 @@ import { type Static, type TAnySchema, Type as t } from '@sinclair/typebox'; +import { StringEnum } from '../utils'; import { + APIBoleto, APICheckout, APICoupon, APICustomer, + APIPaymentLink, APIPayout, + APIPixTransfer, APIProduct, APIQRCodePIX, APIStore, APISubscription, + APISubscriptionPlanChange, + APISubscriptionUsageRecord, CouponDiscountKind, PaymentMethod, PaymentStatus, + PixKeyType, } from '.'; +// Imported directly (not via the barrel) so module init order doesn't matter: +// `rest.ts` is exported before `webhook.ts` in `./index`, and importing +// these from '.' would hit a TDZ error at module-eval time. +import { APIWebhook, WebhookEventType } from './webhook'; /** * Any response returned by the AbacatePay API @@ -119,7 +130,7 @@ export const APIResponseWithCursorBasedPagination = ( error: t.Null({ description: 'Error message returned from the API', }), - success: t.Literal(false, { + success: t.Literal(true, { description: 'Whether the response was successfull or not.', }), pagination: t.Object({ @@ -162,10 +173,15 @@ export type APIResponseWithCursorBasedPagination = /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkout/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export const RESTPostCreateNewCheckoutBody = t.Object({ - methods: PaymentMethod, + methods: t.Optional( + t.Array(PaymentMethod, { + description: + 'Payment methods that will be accepted (Defaults to `[PIX, CARD]`).', + }), + ), returnUrl: t.Optional( t.String({ format: 'uri', @@ -216,12 +232,44 @@ export const RESTPostCreateNewCheckoutBody = t.Object({ }), ), items: APICheckout.properties.items, + frequency: t.Optional( + StringEnum(['ONE_TIME', 'MULTIPLE_PAYMENTS', 'SUBSCRIPTION'], { + examples: ['ONE_TIME'], + description: 'Billing frequency. Defaults to `ONE_TIME`.', + }), + ), + upSellProductId: t.Optional( + t.String({ + description: 'ID of an additional product offered as an upsell.', + }), + ), + interest: t.Optional( + t.Object( + { + value: t.Integer({ + description: 'Monthly interest rate, in hundredths of a percent.', + }), + }, + { description: 'Late interest configuration (Applies to BOLETO).' }, + ), + ), + fine: t.Optional( + t.Object( + { + value: t.Integer({ description: 'Fine value.' }), + type: StringEnum(['PERCENTAGE', 'FIXED'], { + description: 'Type of fine applied.', + }), + }, + { description: 'Late fine configuration (Applies to BOLETO).' }, + ), + ), }); /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkout/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export type RESTPostCreateNewCheckoutBody = Static< typeof RESTPostCreateNewCheckoutBody @@ -230,51 +278,125 @@ export type RESTPostCreateNewCheckoutBody = Static< /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkouts/create + * @reference https://docs.abacatepay.com/pages/payment/create */ -export const RESTPostCreateNewCheckoutData = APICheckout; +export const RESTPostCreateNewCheckoutData = APIResponse(APICheckout); /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkouts/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export type RESTPostCreateNewCheckoutData = Static< typeof RESTPostCreateNewCheckoutData >; +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export const RESTPostRefundCheckoutBody = t.Object({ + id: t.String({ + description: + 'Public ID of the resource to refund (prefixes: `bill_`, `char_`, `pix_char_`, `card_`).', + }), + reason: t.Optional( + t.String({ + maxLength: 500, + description: 'Refund reason, shown in the transaction history.', + }), + ), +}); + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export type RESTPostRefundCheckoutBody = Static< + typeof RESTPostRefundCheckoutBody +>; + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export const RESTPostRefundCheckoutData = APIResponse( + t.Object({ + refundPublicId: t.String({ + description: 'Public ID of the refund transaction that was created.', + }), + }), +); + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export type RESTPostRefundCheckoutData = Static< + typeof RESTPostRefundCheckoutData +>; + /** * https://api.abacatepay.com/v2/checkouts/list * - * @reference https://docs.abacatepay.com/pages/checkouts/list + * @reference https://docs.abacatepay.com/pages/payment/list */ -export const RESTGetListCheckoutsData = t.Array(APICheckout); +export const RESTGetListCheckoutsQueryParams = t.Object({ + page: t.Optional( + t.Integer({ minimum: 1, default: 1, description: 'Number of the page.' }), + ), + limit: t.Optional( + t.Integer({ minimum: 1, description: 'Number of items per page.' }), + ), +}); /** * https://api.abacatepay.com/v2/checkouts/list * - * @reference https://docs.abacatepay.com/pages/checkouts/list + * @reference https://docs.abacatepay.com/pages/payment/list + */ +export type RESTGetListCheckoutsQueryParams = Static< + typeof RESTGetListCheckoutsQueryParams +>; + +/** + * https://api.abacatepay.com/v2/checkouts/list + * + * @reference https://docs.abacatepay.com/pages/payment/list + */ +export const RESTGetListCheckoutsData = APIResponseWithPagination( + t.Array(APICheckout), +); + +/** + * https://api.abacatepay.com/v2/checkouts/list + * + * @reference https://docs.abacatepay.com/pages/payment/list */ export type RESTGetListCheckoutsData = Static; /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ -export const RESTGetCheckoutData = APICheckout; +export const RESTGetCheckoutData = APIResponse(APICheckout); /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export type RESTGetCheckoutData = Static; /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export const RESTGetCheckoutQueryParams = t.Object({ id: t.String({ @@ -286,12 +408,166 @@ export const RESTGetCheckoutQueryParams = t.Object({ /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export type RESTGetCheckoutQueryParams = Static< typeof RESTGetCheckoutQueryParams >; +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export const RESTPostCreatePaymentLinkBody = t.Omit( + RESTPostCreateNewCheckoutBody, + ['customerId', 'customer', 'frequency', 'upSellProductId'], +); + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export type RESTPostCreatePaymentLinkBody = Static< + typeof RESTPostCreatePaymentLinkBody +>; + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export const RESTPostCreatePaymentLinkData = APIResponse(APIPaymentLink); + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export type RESTPostCreatePaymentLinkData = Static< + typeof RESTPostCreatePaymentLinkData +>; + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export const RESTGetListPaymentLinksQueryParams = t.Object({ + page: t.Optional( + t.Integer({ minimum: 1, default: 1, description: 'Number of the page.' }), + ), + limit: t.Optional( + t.Integer({ minimum: 1, description: 'Number of items per page.' }), + ), +}); + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export type RESTGetListPaymentLinksQueryParams = Static< + typeof RESTGetListPaymentLinksQueryParams +>; + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export const RESTGetListPaymentLinksData = APIResponseWithPagination( + t.Array(APIPaymentLink), +); + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export type RESTGetListPaymentLinksData = Static< + typeof RESTGetListPaymentLinksData +>; + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export const RESTGetPaymentLinkQueryParams = t.Object({ + id: t.String({ description: 'Unique payment link identifier.' }), +}); + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export type RESTGetPaymentLinkQueryParams = Static< + typeof RESTGetPaymentLinkQueryParams +>; + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export const RESTGetPaymentLinkData = APIResponse(APIPaymentLink); + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export type RESTGetPaymentLinkData = Static; + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export const RESTPostRefundPaymentLinkBody = t.Object({ + id: t.String({ description: 'Public ID of the resource to refund.' }), + reason: t.Optional( + t.String({ + maxLength: 500, + description: 'Refund reason, shown in the transaction history.', + }), + ), +}); + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export type RESTPostRefundPaymentLinkBody = Static< + typeof RESTPostRefundPaymentLinkBody +>; + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export const RESTPostRefundPaymentLinkData = APIResponse( + t.Object({ + refundPublicId: t.String({ + description: 'Public ID of the refund transaction that was created.', + }), + }), +); + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export type RESTPostRefundPaymentLinkData = Static< + typeof RESTPostRefundPaymentLinkData +>; + /** * https://api.abacatepay.com/v2/coupons/create * @@ -335,16 +611,16 @@ export const RESTPostCreateCouponBody = t.Object({ export type RESTPostCreateCouponBody = Static; /** - * https://api.abacatepay.com/v2/coupon/create + * https://api.abacatepay.com/v2/coupons/create * - * @reference https://docs.abacatepay.com/pages/coupon/create + * @reference https://docs.abacatepay.com/pages/coupons/create */ -export const RESTPostCreateCouponData = APICoupon; +export const RESTPostCreateCouponData = APIResponse(APICoupon); /** - * https://api.abacatepay.com/v2/coupon/create + * https://api.abacatepay.com/v2/coupons/create * - * @reference https://docs.abacatepay.com/pages/coupon/create + * @reference https://docs.abacatepay.com/pages/coupons/create */ export type RESTPostCreateCouponData = Static; @@ -353,7 +629,9 @@ export type RESTPostCreateCouponData = Static; * * @reference https://docs.abacatepay.com/pages/coupons/list */ -export const RESTGetListCouponsData = t.Array(APICoupon); +export const RESTGetListCouponsData = APIResponseWithPagination( + t.Array(APICoupon), +); /** * https://api.abacatepay.com/v2/coupons/list @@ -418,7 +696,7 @@ export type RESTGetCouponQueryParams = Static; * * @reference https://docs.abacatepay.com/pages/coupons/get */ -export const RESTGetCouponData = APICoupon; +export const RESTGetCouponData = APIResponse(APICoupon); /** * https://api.abacatepay.com/v2/coupons/get @@ -451,7 +729,7 @@ export type RESTDeleteCouponBody = Static; * * @reference https://docs.abacatepay.com/pages/coupons/delete */ -export const RESTDeleteCouponData = APICoupon; +export const RESTDeleteCouponData = APIResponse(APICoupon); /** * https://api.abacatepay.com/v2/coupons/delete @@ -465,7 +743,7 @@ export type RESTDeleteCouponData = Static; * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export const RESTPatchToggleCouponStatusBody = t.Object({ +export const RESTPostToggleCouponStatusBody = t.Object({ id: t.String({ examples: ['SUMMER_26'], description: 'The ID of the coupon.', @@ -477,8 +755,8 @@ export const RESTPatchToggleCouponStatusBody = t.Object({ * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export type RESTPatchToggleCouponStatusBody = Static< - typeof RESTPatchToggleCouponStatusBody +export type RESTPostToggleCouponStatusBody = Static< + typeof RESTPostToggleCouponStatusBody >; /** @@ -486,15 +764,15 @@ export type RESTPatchToggleCouponStatusBody = Static< * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export const RESTPatchToggleCouponStatusData = APICoupon; +export const RESTPostToggleCouponStatusData = APIResponse(APICoupon); /** * https://api.abacatepay.com/v2/coupons/toggle * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export type RESTPatchToggleCouponStatusData = Static< - typeof RESTPatchToggleCouponStatusData +export type RESTPostToggleCouponStatusData = Static< + typeof RESTPostToggleCouponStatusData >; /** @@ -534,7 +812,7 @@ export type RESTPostCreateNewPayoutBody = Static< * * @reference https://docs.abacatepay.com/pages/payouts/create */ -export const RESTPostCreateNewPayoutData = APIPayout; +export const RESTPostCreateNewPayoutData = APIResponse(APIPayout); /** * https://api.abacatepay.com/v2/payouts/create @@ -566,6 +844,20 @@ export type RESTGetSearchPayoutQueryParams = Static< typeof RESTGetSearchPayoutQueryParams >; +/** + * https://api.abacatepay.com/v2/payouts/get + * + * @reference https://docs.abacatepay.com/pages/payouts/get + */ +export const RESTGetSearchPayoutData = APIResponse(APIPayout); + +/** + * https://api.abacatepay.com/v2/payouts/get + * + * @reference https://docs.abacatepay.com/pages/payouts/get + */ +export type RESTGetSearchPayoutData = Static; + /** * https://api.abacatepay.com/v2/payouts/list * @@ -599,67 +891,378 @@ export type RESTGetListPayoutsQueryParams = Static< >; /** - * https://api.abacatepay.com/v2/payouts/list + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export const RESTGetListPayoutsData = APIResponseWithPagination( + t.Array(APIPayout), +); + +/** + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export type RESTGetListPayoutsData = Static; + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export const RESTPostSendPixTransferBody = t.Object({ + amount: t.Integer({ + minimum: 1, + description: 'Transfer amount in cents (Min 100).', + }), + externalId: t.String({ + description: 'Unique identifier of the transfer in your system.', + }), + description: t.Optional( + t.String({ description: 'Optional transfer description.' }), + ), + pix: t.Object({ + key: t.String({ description: 'The PIX key itself.' }), + type: PixKeyType, + }), +}); + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export type RESTPostSendPixTransferBody = Static< + typeof RESTPostSendPixTransferBody +>; + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export const RESTPostSendPixTransferData = APIResponse(APIPixTransfer); + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export type RESTPostSendPixTransferData = Static< + typeof RESTPostSendPixTransferData +>; + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export const RESTGetPixTransferQueryParams = t.Object({ + id: t.Optional( + t.String({ + description: + 'Unique transfer identifier in AbacatePay. At least one of `id`/`externalId` is required.', + }), + ), + externalId: t.Optional( + t.String({ + description: + 'Unique transfer identifier in your system. At least one of `id`/`externalId` is required.', + }), + ), +}); + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export type RESTGetPixTransferQueryParams = Static< + typeof RESTGetPixTransferQueryParams +>; + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export const RESTGetPixTransferData = APIResponse(APIPixTransfer); + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export type RESTGetPixTransferData = Static; + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export const RESTGetListPixTransfersQueryParams = t.Object({ + limit: t.Optional( + t.Integer({ + minimum: 1, + maximum: 100, + description: 'Number of items per page (1-100).', + }), + ), + after: t.Optional(t.String({ description: 'Cursor for the next page.' })), + before: t.Optional( + t.String({ description: 'Cursor for the previous page.' }), + ), + id: t.Optional( + t.String({ description: 'Filter by AbacatePay transaction ID.' }), + ), + externalId: t.Optional( + t.String({ description: 'Filter by external system ID.' }), + ), + status: t.Optional( + t.String({ description: 'Filter by transaction status.' }), + ), +}); + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export type RESTGetListPixTransfersQueryParams = Static< + typeof RESTGetListPixTransfersQueryParams +>; + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export const RESTGetListPixTransfersData = APIResponseWithCursorBasedPagination( + t.Array(APIPixTransfer), +); + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export type RESTGetListPixTransfersData = Static< + typeof RESTGetListPixTransfersData +>; + +/** + * Inner `data` payload sent to `POST /transparents/create` when `method` is `PIX`. + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export const RESTPostCreateQRCodePixBody = t.Intersect([ + t.Pick(RESTPostCreateNewCheckoutBody, ['customer', 'metadata']), + t.Object({ + amount: t.Integer({ + examples: [1234], + description: 'Charge amount in cents.', + }), + expiresIn: t.Optional( + t.Integer({ + description: 'Billing expiration time in seconds.', + }), + ), + description: t.Optional( + t.String({ + description: 'Message that will appear when paying the PIX.', + }), + ), + }), +]); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export type RESTPostCreateQRCodePixBody = Static< + typeof RESTPostCreateQRCodePixBody +>; + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export const RESTPostCreateQRCodePixData = APIResponse(APIQRCodePIX); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export type RESTPostCreateQRCodePixData = Static< + typeof RESTPostCreateQRCodePixData +>; + +/** + * Inner `data` payload sent to `POST /transparents/create` when `method` is `BOLETO`. + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export const RESTPostCreateBoletoBody = t.Object({ + amount: t.Integer({ description: 'Charge amount in cents.' }), + description: t.Optional( + t.String({ description: 'Message that will appear on the Boleto.' }), + ), + customer: t.Intersect( + [ + t.Pick(APICustomer, ['name', 'taxId']), + t.Partial(t.Pick(APICustomer, ['email', 'cellphone'])), + ], + { + description: + 'Customer data. `name` and `taxId` are mandatory for Boleto.', + }, + ), + metadata: t.Optional( + t.Record(t.String(), t.Any(), { description: 'Optional charge metadata.' }), + ), +}); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export type RESTPostCreateBoletoBody = Static; + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export const RESTPostCreateBoletoData = APIResponse(APIBoleto); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export type RESTPostCreateBoletoData = Static; + +/** + * Wire-level request body for `POST /transparents/create` — the SDK builds this + * from {@link RESTPostCreateQRCodePixBody} / {@link RESTPostCreateBoletoBody}, callers + * never construct it directly. + * + * @reference https://docs.abacatepay.com/pages/transparents/reference + */ +export const RESTPostCreateTransparentBody = t.Union([ + t.Object({ method: t.Literal('PIX'), data: RESTPostCreateQRCodePixBody }), + t.Object({ method: t.Literal('BOLETO'), data: RESTPostCreateBoletoBody }), +]); + +/** + * @reference https://docs.abacatepay.com/pages/transparents/reference + */ +export type RESTPostCreateTransparentBody = Static< + typeof RESTPostCreateTransparentBody +>; + +/** + * https://api.abacatepay.com/v2/transparents/list + * + * @reference https://docs.abacatepay.com/pages/transparents/list + */ +export const RESTGetListTransparentsQueryParams = t.Object({ + after: t.Optional(t.String({ description: 'Cursor for the next page.' })), + before: t.Optional( + t.String({ description: 'Cursor for the previous page.' }), + ), + limit: t.Optional( + t.Integer({ + minimum: 1, + maximum: 100, + description: 'Number of items per page (1-100).', + }), + ), + id: t.Optional( + t.String({ description: 'Filter by QRCode/Boleto identifier.' }), + ), + status: t.Optional(PaymentStatus), +}); + +/** + * https://api.abacatepay.com/v2/transparents/list + * + * @reference https://docs.abacatepay.com/pages/transparents/list + */ +export type RESTGetListTransparentsQueryParams = Static< + typeof RESTGetListTransparentsQueryParams +>; + +/** + * https://api.abacatepay.com/v2/transparents/list * - * @reference https://docs.abacatepay.com/pages/payouts/list + * @reference https://docs.abacatepay.com/pages/transparents/list */ -export const RESTGetListPayoutsData = t.Array(APIPayout); +export const RESTGetListTransparentsData = APIResponseWithCursorBasedPagination( + t.Array(t.Union([APIQRCodePIX, APIBoleto])), +); /** - * https://api.abacatepay.com/v2/payouts/list + * https://api.abacatepay.com/v2/transparents/list * - * @reference https://docs.abacatepay.com/pages/payouts/list + * @reference https://docs.abacatepay.com/pages/transparents/list */ -export type RESTGetListPayoutsData = Static; +export type RESTGetListTransparentsData = Static< + typeof RESTGetListTransparentsData +>; /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export const RESTPostCreateQRCodePixBody = t.Intersect([ - t.Pick(RESTPostCreateNewCheckoutBody, ['customer', 'metadata']), - t.Object({ - amount: t.Integer({ - examples: [1234], - description: 'Charge amount in cents.', - }), - expiresIn: t.Optional( - t.Integer({ - description: 'Billing expiration time in seconds.', - }), - ), - description: t.Optional( - t.String({ - description: 'Message that will appear when paying the PIX.', - }), - ), +export const RESTPostRefundTransparentBody = t.Object({ + id: t.String({ + description: + 'Public ID of the resource to refund (prefixes: `char_`, `pix_char_`, `card_`, `bill_`).', }), -]); + reason: t.Optional( + t.String({ + maxLength: 500, + description: 'Refund reason, shown in the transaction history.', + }), + ), +}); /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export type RESTPostCreateQRCodePixBody = Static< - typeof RESTPostCreateQRCodePixBody +export type RESTPostRefundTransparentBody = Static< + typeof RESTPostRefundTransparentBody >; /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export const RESTPostCreateQRCodePixData = APIQRCodePIX; +export const RESTPostRefundTransparentData = APIResponse( + t.Object({ + refundPublicId: t.String({ + description: 'Public ID of the refund transaction that was created.', + }), + }), +); /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export type RESTPostCreateQRCodePixData = Static< - typeof RESTPostCreateQRCodePixData +export type RESTPostRefundTransparentData = Static< + typeof RESTPostRefundTransparentData >; /** @@ -709,7 +1312,7 @@ export type RESTPostSimulateQRCodePixPaymentBody = Static< * * @reference https://docs.abacatepay.com/pages/transparents/simulate-payment */ -export const RESTPostSimulateQRCodePixPaymentData = APIQRCodePIX; +export const RESTPostSimulateQRCodePixPaymentData = APIResponse(APIQRCodePIX); /** * https://api.abacatepay.com/v2/transparents/simulate-payment @@ -721,9 +1324,9 @@ export type RESTPostSimulateQRCodePixPaymentData = Static< >; /** - * https://api.abacatepay.com/v2/pixQrCode/check + * https://api.abacatepay.com/v2/transparents/check * - * @reference https://docs.abacatepay.com/pages/pix-qrcode/check + * @reference https://docs.abacatepay.com/pages/transparents/check */ export const RESTGetCheckQRCodePixStatusQueryParams = t.Object({ id: t.String({ @@ -733,9 +1336,9 @@ export const RESTGetCheckQRCodePixStatusQueryParams = t.Object({ }); /** - * https://api.abacatepay.com/v2/pixQrCode/check + * https://api.abacatepay.com/v2/transparents/check * - * @reference https://docs.abacatepay.com/pages/pix-qrcode/check + * @reference https://docs.abacatepay.com/pages/transparents/check */ export type RESTGetCheckQRCodePixStatusQueryParams = Static< typeof RESTGetCheckQRCodePixStatusQueryParams @@ -746,13 +1349,15 @@ export type RESTGetCheckQRCodePixStatusQueryParams = Static< * * @reference https://docs.abacatepay.com/pages/transparents/check */ -export const RESTGetCheckQRCodePixStatusData = t.Object({ - expiresAt: t.Date({ - examples: [new Date()], - description: 'QRCode Pix expiration date.', +export const RESTGetCheckQRCodePixStatusData = APIResponse( + t.Object({ + expiresAt: t.Date({ + examples: [new Date()], + description: 'QRCode Pix expiration date.', + }), + status: PaymentStatus, }), - status: PaymentStatus, -}); +); /** * https://api.abacatepay.com/v2/transparents/check @@ -793,7 +1398,7 @@ export type RESTPostCreateProductBody = Static< * * @reference https://docs.abacatepay.com/pages/products/create */ -export const RESTPostCreateProductData = APIProduct; +export const RESTPostCreateProductData = APIResponse(APIProduct); /** * https://api.abacatepay.com/v2/products/create @@ -841,7 +1446,9 @@ export type RESTGetListProductsQueryParams = Static< * * @reference https://docs.abacatepay.com/pages/products/list */ -export const RESTGetListProductsData = t.Array(APIProduct); +export const RESTGetListProductsData = APIResponseWithPagination( + t.Array(APIProduct), +); /** * https://api.abacatepay.com/v2/products/list @@ -882,7 +1489,7 @@ export type RESTGetProductQueryParams = Static< * * @reference https://docs.abacatepay.com/pages/products/get */ -export const RESTGetProductData = APIProduct; +export const RESTGetProductData = APIResponse(APIProduct); /** * https://api.abacatepay.com/v2/products/get @@ -891,12 +1498,44 @@ export const RESTGetProductData = APIProduct; */ export type RESTGetProductData = Static; +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export const RESTDeleteProductQueryParams = t.Object({ + id: t.String({ description: 'The product ID.' }), +}); + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export type RESTDeleteProductQueryParams = Static< + typeof RESTDeleteProductQueryParams +>; + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export const RESTDeleteProductData = APIResponse(APIProduct); + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export type RESTDeleteProductData = Static; + /** * https://api.abacatepay.com/v2/store/get * * @reference https://docs.abacatepay.com/pages/store/get */ -export const RESTGetStoreDetailsData = APIStore; +export const RESTGetStoreDetailsData = APIResponse(APIStore); /** * https://api.abacatepay.com/v2/store/get @@ -910,20 +1549,22 @@ export type RESTGetStoreDetailsData = Static; * * @reference https://docs.abacatepay.com/pages/trustMRR/mrr */ -export const RESTGetMRRData = t.Object({ - mrr: t.Integer({ - minimum: 0, - examples: [100], - description: - 'Monthly recurring revenue in cents. Value 0 indicates that there is no recurring revenue at the moment.', - }), - totalActiveSubscriptions: t.Integer({ - minimum: 0, - examples: [1], - description: - 'Total active subscriptions. Value 0 indicates that there are no currently active subscriptions.', +export const RESTGetMRRData = APIResponse( + t.Object({ + mrr: t.Integer({ + minimum: 0, + examples: [100], + description: + 'Monthly recurring revenue in cents. Value 0 indicates that there is no recurring revenue at the moment.', + }), + totalActiveSubscriptions: t.Integer({ + minimum: 0, + examples: [1], + description: + 'Total active subscriptions. Value 0 indicates that there are no currently active subscriptions.', + }), }), -}); +); /** * https://api.abacatepay.com/v2/public-mrr/mrr @@ -970,7 +1611,7 @@ export type RESTPostCreateSubscriptionBody = Static< * * @reference https://docs.abacatepay.com/pages/subscriptions/create */ -export const RESTPostCreateSubscriptionData = APISubscription; +export const RESTPostCreateSubscriptionData = APIResponse(APISubscription); /** * https://api.abacatepay.com/v2/subscriptions/create @@ -1015,7 +1656,8 @@ export type RESTGetListSubscriptionsQueryParams = Static< * * @reference https://docs.abacatepay.com/pages/subscriptions/list */ -export const RESTGetListSubscriptionsData = t.Array(APISubscription); +export const RESTGetListSubscriptionsData = + APIResponseWithCursorBasedPagination(t.Array(APISubscription)); /** * https://api.abacatepay.com/v2/subscriptions/list @@ -1026,6 +1668,281 @@ export type RESTGetListSubscriptionsData = Static< typeof RESTGetListSubscriptionsData >; +/** + * https://api.abacatepay.com/v2/subscriptions/cancel + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export const RESTPostCancelSubscriptionBody = t.Object({ + id: t.String({ description: 'Unique subscription identifier.' }), +}); + +/** + * https://api.abacatepay.com/v2/subscriptions/cancel + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export type RESTPostCancelSubscriptionBody = Static< + typeof RESTPostCancelSubscriptionBody +>; + +/** + * Cancellation is immediate (`cancelPolicy: NOW`) — there is no grace period. + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export const RESTPostCancelSubscriptionData = APIResponse(APISubscription); + +/** + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export type RESTPostCancelSubscriptionData = Static< + typeof RESTPostCancelSubscriptionData +>; + +/** + * https://api.abacatepay.com/v2/subscriptions/change-plan + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export const RESTPostChangeSubscriptionPlanBody = t.Object({ + id: t.String({ description: 'Unique subscription identifier.' }), + productId: t.String({ + description: + 'ID of the new product. It must have a billing cycle configured.', + }), + quantity: t.Integer({ description: 'New quantity for the product.' }), +}); + +/** + * https://api.abacatepay.com/v2/subscriptions/change-plan + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type RESTPostChangeSubscriptionPlanBody = Static< + typeof RESTPostChangeSubscriptionPlanBody +>; + +/** + * Only one `PENDING` change can exist per subscription — calling this again + * replaces the prior unapplied change. The change is applied at the next + * billing cycle. + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export const RESTPostChangeSubscriptionPlanData = APIResponse( + APISubscriptionPlanChange, +); + +/** + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type RESTPostChangeSubscriptionPlanData = Static< + typeof RESTPostChangeSubscriptionPlanData +>; + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export const RESTPostRecordSubscriptionUsageBody = t.Object({ + id: t.String({ description: 'Unique subscription identifier.' }), + productId: t.String({ + description: + 'ID of the pay-as-you-go product (Must not have a billing cycle).', + }), + units: t.Integer({ description: 'Number of units to record.' }), + action: StringEnum(['add', 'subtract'], { + description: 'Whether to add or subtract the units from the current cycle.', + }), +}); + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export type RESTPostRecordSubscriptionUsageBody = Static< + typeof RESTPostRecordSubscriptionUsageBody +>; + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export const RESTPostRecordSubscriptionUsageData = APIResponse( + APISubscriptionUsageRecord, +); + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export type RESTPostRecordSubscriptionUsageData = Static< + typeof RESTPostRecordSubscriptionUsageData +>; + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export const RESTPostCreateWebhookBody = t.Object({ + name: t.String({ description: 'Webhook name, for your own identification.' }), + endpoint: t.String({ + format: 'uri', + description: 'HTTPS endpoint that will receive the events.', + }), + secret: t.String({ + description: 'Secret used to sign the payloads sent to `endpoint`.', + }), + events: t.Array(WebhookEventType, { + description: 'Event types this webhook should be notified about.', + }), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export type RESTPostCreateWebhookBody = Static< + typeof RESTPostCreateWebhookBody +>; + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export const RESTPostCreateWebhookData = APIResponse(APIWebhook); + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export type RESTPostCreateWebhookData = Static< + typeof RESTPostCreateWebhookData +>; + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export const RESTGetListWebhooksQueryParams = t.Object({ + search: t.Optional( + t.String({ description: 'Search by webhook name, ID, or endpoint.' }), + ), + after: t.Optional(t.String({ description: 'Cursor for the next page.' })), + before: t.Optional( + t.String({ description: 'Cursor for the previous page.' }), + ), + limit: t.Optional( + t.Integer({ + minimum: 1, + maximum: 100, + description: 'Number of items per page (1-100).', + }), + ), + id: t.Optional(t.String({ description: 'Filter by a specific webhook ID.' })), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export type RESTGetListWebhooksQueryParams = Static< + typeof RESTGetListWebhooksQueryParams +>; + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export const RESTGetListWebhooksData = APIResponseWithCursorBasedPagination( + t.Array(APIWebhook), +); + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export type RESTGetListWebhooksData = Static; + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export const RESTGetWebhookQueryParams = t.Object({ + id: t.String({ description: 'Unique webhook identifier.' }), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export type RESTGetWebhookQueryParams = Static< + typeof RESTGetWebhookQueryParams +>; + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export const RESTGetWebhookData = APIResponse(APIWebhook); + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export type RESTGetWebhookData = Static; + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export const RESTPostDeleteWebhookBody = t.Object({ + id: t.String({ description: 'Unique webhook identifier.' }), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export type RESTPostDeleteWebhookBody = Static< + typeof RESTPostDeleteWebhookBody +>; + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export const RESTPostDeleteWebhookData = APIResponse(APIWebhook); + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export type RESTPostDeleteWebhookData = Static< + typeof RESTPostDeleteWebhookData +>; + /** * https://api.abacatepay.com/v2/customers/create * @@ -1052,7 +1969,7 @@ export type RESTPostCreateCustomerBody = Static< * * @reference https://docs.abacatepay.com/pages/client/create */ -export const RESTPostCreateCustomerData = APICustomer; +export const RESTPostCreateCustomerData = APIResponse(APICustomer); /** * https://api.abacatepay.com/v2/customers/create @@ -1068,7 +1985,9 @@ export type RESTPostCreateCustomerData = Static< * * @reference https://docs.abacatepay.com/pages/client/list */ -export const RESTGetListCustomersData = t.Array(APICustomer); +export const RESTGetListCustomersData = APIResponseWithPagination( + t.Array(APICustomer), +); /** * https://api.abacatepay.com/v2/customers/list @@ -1135,7 +2054,9 @@ export type RESTGetCustomerQueryParams = Static< * * @reference https://docs.abacatepay.com/pages/client/get */ -export const RESTGetCustomerData = t.Omit(APICustomer, ['country', 'zipCode']); +export const RESTGetCustomerData = APIResponse( + t.Omit(APICustomer, ['country', 'zipCode']), +); /** * https://api.abacatepay.com/v2/customers/get @@ -1168,10 +2089,9 @@ export type RESTDeleteCustomerBody = Static; * * @reference https://docs.abacatepay.com/pages/client/delete */ -export const RESTDeleteCustomerData = t.Omit(APICustomer, [ - 'country', - 'zipCode', -]); +export const RESTDeleteCustomerData = APIResponse( + t.Omit(APICustomer, ['country', 'zipCode']), +); /** * https://api.abacatepay.com/v2/customers/delete @@ -1210,33 +2130,35 @@ export type RESTGetRevenueByPeriodQueryParams = Static< * * @reference https://docs.abacatepay.com/pages/trustMRR/list */ -export const RESTGetRevenueByPeriodData = t.Object({ - totalRevenue: t.Integer({ - examples: [10000], - description: 'Total revenue for the period in cents.', - }), - totalTransactions: t.Integer({ - examples: [39], - description: 'Total transactions in the period.', - }), - transactionsPerDay: t.Record( - t.String(), - t.Object({ - amount: t.Integer({ - examples: [3200], - description: "Total value of the day's transactions in cents.", - }), - count: t.Integer({ - examples: [12], - description: 'Number of transactions for the day.', - }), +export const RESTGetRevenueByPeriodData = APIResponse( + t.Object({ + totalRevenue: t.Integer({ + examples: [10000], + description: 'Total revenue for the period in cents.', }), - { - description: - 'Object with transactions grouped by day (key is the date in YYYY-MM-DD format).', - }, - ), -}); + totalTransactions: t.Integer({ + examples: [39], + description: 'Total transactions in the period.', + }), + transactionsPerDay: t.Record( + t.String(), + t.Object({ + amount: t.Integer({ + examples: [3200], + description: "Total value of the day's transactions in cents.", + }), + count: t.Integer({ + examples: [12], + description: 'Number of transactions for the day.', + }), + }), + { + description: + 'Object with transactions grouped by day (key is the date in YYYY-MM-DD format).', + }, + ), + }), +); /** * https://api.abacatepay.com/v2/public-mrr/revenue @@ -1252,21 +2174,23 @@ export type RESTGetRevenueByPeriodData = Static< * * @reference https://docs.abacatepay.com/pages/trustMRR/get */ -export const RESTGetMerchantData = t.Object({ - name: t.String({ - examples: ['Summer Store'], - description: 'Store name.', - }), - website: t.String({ - format: 'uri', - examples: ['https://summer-store.com/'], - description: 'Store website.', - }), - createdAt: t.Date({ - examples: [new Date()], - description: 'Store creation date.', +export const RESTGetMerchantData = APIResponse( + t.Object({ + name: t.String({ + examples: ['Summer Store'], + description: 'Store name.', + }), + website: t.String({ + format: 'uri', + examples: ['https://summer-store.com/'], + description: 'Store website.', + }), + createdAt: t.Date({ + examples: [new Date()], + description: 'Store creation date.', + }), }), -}); +); /** * https://api.abacatepay.com/v2/public-mrr/merchant-info diff --git a/packages/typebox/src/v2/webhook.ts b/packages/typebox/src/v2/webhook.ts index c313c6a..7359fc8 100644 --- a/packages/typebox/src/v2/webhook.ts +++ b/packages/typebox/src/v2/webhook.ts @@ -3,18 +3,81 @@ import { StringEnum } from '../utils'; import { APIPayout, PaymentMethod } from '.'; /** - * https://docs.abacatepay.com/pages/webhooks + * https://docs.abacatepay.com/pages/webhooks/reference */ export const WebhookEventType = StringEnum( - ['payout.failed', 'payout.done', 'billing.paid'], - { examples: ['payout.failed'], description: 'Webhook event type.' }, + [ + 'checkout.completed', + 'checkout.refunded', + 'checkout.disputed', + 'checkout.lost', + 'transparent.completed', + 'transparent.refunded', + 'transparent.disputed', + 'transparent.lost', + 'subscription.completed', + 'subscription.cancelled', + 'subscription.renewed', + 'subscription.trial_started', + 'payout.completed', + 'payout.failed', + 'transfer.completed', + 'transfer.failed', + ], + { examples: ['checkout.completed'], description: 'Webhook event type.' }, ); /** - * https://docs.abacatepay.com/pages/webhooks + * https://docs.abacatepay.com/pages/webhooks/reference */ export type WebhookEventType = Static; +/** + * The webhook resource itself, as returned by the `webhooks/*` endpoints. + * + * https://docs.abacatepay.com/pages/webhooks/reference + */ +export const APIWebhook = t.Object({ + id: t.String({ + examples: ['webh_123'], + description: 'Unique webhook identifier.', + }), + name: t.String({ + examples: ['Order fulfillment'], + description: 'Webhook name.', + }), + endpoint: t.String({ + format: 'uri', + examples: ['https://myshop.com/webhooks/abacatepay'], + description: 'HTTPS endpoint that receives the events.', + }), + events: t.Array(WebhookEventType, { + description: 'Event types this webhook is subscribed to.', + }), + devMode: t.Boolean({ + examples: [false], + description: + 'Indicates whether the webhook was created in a testing environment.', + }), + v2: t.Boolean({ + examples: [true], + description: 'Indicates whether this webhook targets the v2 API.', + }), + createdAt: t.Date({ + examples: ['2025-01-01T00:00:00Z'], + description: 'Webhook creation date and time.', + }), + updatedAt: t.Date({ + examples: ['2025-01-01T00:00:00Z'], + description: 'Webhook last updated date and time.', + }), +}); + +/** + * https://docs.abacatepay.com/pages/webhooks/reference + */ +export type APIWebhook = Static; + export const BaseWebhookEvent = < Type extends Static, Schema extends TAnySchema, @@ -68,10 +131,10 @@ export const WebhookPayoutFailedEvent = BaseWebhookEvent( export type WebhookPayoutFailedEvent = Static; /** - * https://docs.abacatepay.com/pages/webhooks#payout-done + * https://docs.abacatepay.com/pages/webhooks#payout-completed */ -export const WebhookPayoutDoneEvent = BaseWebhookEvent( - 'payout.done', +export const WebhookPayoutCompletedEvent = BaseWebhookEvent( + 'payout.completed', t.Object({ transaction: t.Intersect( [ @@ -91,98 +154,148 @@ export const WebhookPayoutDoneEvent = BaseWebhookEvent( ); /** - * https://docs.abacatepay.com/pages/webhooks#payout-done + * https://docs.abacatepay.com/pages/webhooks#payout-completed */ -export type WebhookPayoutDoneEvent = Static; +export type WebhookPayoutCompletedEvent = Static< + typeof WebhookPayoutCompletedEvent +>; + +const paymentData = t.Object({ + amount: t.Integer({ + examples: [4000], + description: 'Charge amount in cents (e.g. 4000 = R$40.00).', + }), + fee: t.Literal(80, { + examples: [80], + description: 'The fee charged by AbacatePay.', + }), + method: PaymentMethod, +}); /** - * https://docs.abacatepay.com/pages/webhooks#billing-paid + * https://docs.abacatepay.com/pages/webhooks#checkout-completed */ -export const WebhookBillingPaidEvent = BaseWebhookEvent( - 'billing.paid', +export const WebhookCheckoutCompletedEvent = BaseWebhookEvent( + 'checkout.completed', t.Object({ - payment: t.Intersect([ - t.Object( - { - payment: t.Object({ - amount: t.Integer({ - examples: [4000], - description: 'Charge amount in cents (e.g. 4000 = R$40.00).', - }), - fee: t.Integer({ - examples: [200], - description: 'The fee charged by AbacatePay.', - }), - method: PaymentMethod, - }), - }, - { - description: 'Payment data.', - }, - ), - t.Union([ - t.Object({ - pixQrCode: t.Object({ - amount: t.Integer({ - examples: [4000], - description: 'Charge amount in cents (e.g. 4000 = R$40.00).', - }), - id: t.String({ - examples: ['bill_1234567890abcdef'], - description: 'Unique billing identifier.', - }), - kind: t.Literal('PIX', { - examples: ['PIX'], - description: 'Kind of the payment', - }), - status: t.Literal('PAID', { - examples: ['PAID'], - description: 'Billing status, can only be `PAID` here', - }), - }), - }), - t.Object({ - billing: t.Object({ - amount: t.Integer({ - examples: [4000], - description: 'Charge amount in cents (e.g. 4000 = R$40.00).', - }), - id: t.String({ - examples: ['bill_1234567890abcdef'], - description: 'Unique billing identifier.', - }), - externalId: t.String({ - examples: ['my-invoice-0001'], - description: 'Bill ID in your system.', - }), - status: t.Literal('PAID', { - examples: ['PAID'], - description: 'Status of the payment. Always `PAID`.', - }), - url: t.String({ - format: 'uri', - examples: ['https://abacatepay.com/pay/bill_1234567890abcdef'], - description: 'URL where the user can complete the payment.', - }), - }), - }), - ]), - ]), + payment: t.Object(paymentData.properties, { description: 'Payment data.' }), + billing: t.Object({ + amount: t.Integer({ + examples: [4000], + description: 'Charge amount in cents (e.g. 4000 = R$40.00).', + }), + id: t.String({ + examples: ['bill_1234567890abcdef'], + description: 'Unique billing identifier.', + }), + externalId: t.String({ + examples: ['my-invoice-0001'], + description: 'Bill ID in your system.', + }), + status: t.Literal('PAID', { + examples: ['PAID'], + description: 'Status of the payment. Always `PAID`.', + }), + url: t.String({ + format: 'uri', + examples: ['https://abacatepay.com/pay/bill_1234567890abcdef'], + description: 'URL where the user can complete the payment.', + }), + }), + }), +); + +/** + * https://docs.abacatepay.com/pages/webhooks#checkout-completed + */ +export type WebhookCheckoutCompletedEvent = Static< + typeof WebhookCheckoutCompletedEvent +>; + +/** + * https://docs.abacatepay.com/pages/webhooks#transparent-completed + */ +export const WebhookTransparentCompletedEvent = BaseWebhookEvent( + 'transparent.completed', + t.Object({ + payment: t.Object(paymentData.properties, { description: 'Payment data.' }), + pixQrCode: t.Object({ + amount: t.Integer({ + examples: [4000], + description: 'Charge amount in cents (e.g. 4000 = R$40.00).', + }), + id: t.String({ + examples: ['pix_char_1234567890abcdef'], + description: 'Unique billing identifier.', + }), + kind: t.Literal('PIX', { + examples: ['PIX'], + description: 'Kind of the payment.', + }), + status: t.Literal('PAID', { + examples: ['PAID'], + description: 'Billing status, can only be `PAID` here.', + }), + }), }), ); /** - * https://docs.abacatepay.com/pages/webhooks#billing-paid + * https://docs.abacatepay.com/pages/webhooks#transparent-completed + */ +export type WebhookTransparentCompletedEvent = Static< + typeof WebhookTransparentCompletedEvent +>; + +/** + * Event types whose payload AbacatePay does not document in detail yet. + * + * @unstable The `data` shape is a best-effort placeholder until AbacatePay documents it. + */ +export const WebhookUndocumentedEvent = t.Object({ + data: t.Record(t.String(), t.Unknown()), + id: t.String({ description: 'Unique identifier for the webhook.' }), + event: StringEnum( + [ + 'checkout.refunded', + 'checkout.disputed', + 'checkout.lost', + 'transparent.refunded', + 'transparent.disputed', + 'transparent.lost', + 'subscription.completed', + 'subscription.cancelled', + 'subscription.renewed', + 'subscription.trial_started', + 'transfer.completed', + 'transfer.failed', + ], + { description: 'This field identifies the type of event received.' }, + ), + devMode: t.Boolean({ + description: + 'Indicates whether the event occurred in the development environment.', + }), +}); + +/** + * Event types whose payload AbacatePay does not document in detail yet. + * + * @unstable */ -export type WebhookBillingPaidEvent = Static; +export type WebhookUndocumentedEvent = Static; /** * https://docs.abacatepay.com/pages/webhooks + * + * Any field that contains the tag "@unstable" means that the field is an assumption, it is uncertain (Since AbacatePay does not provide any information about). */ export const WebhookEvent = t.Union([ - WebhookPayoutDoneEvent, - WebhookBillingPaidEvent, + WebhookPayoutCompletedEvent, WebhookPayoutFailedEvent, + WebhookCheckoutCompletedEvent, + WebhookTransparentCompletedEvent, + WebhookUndocumentedEvent, ]); /** diff --git a/packages/typebox/src/version.ts b/packages/typebox/src/version.ts index 760f90e..c59f384 100644 --- a/packages/typebox/src/version.ts +++ b/packages/typebox/src/version.ts @@ -3,7 +3,7 @@ /** * Current version of [@abacatepay/typebox](https://www.npmjs.com/package/@abacatepay/typebox). */ -export const version = '1.0.1' as const; +export const version = '2.0.0' as const; /** * Current version of the AbacatePay API. */ diff --git a/packages/typebox/test/index.test.ts b/packages/typebox/test/index.test.ts index e69de29..cb9ea68 100644 --- a/packages/typebox/test/index.test.ts +++ b/packages/typebox/test/index.test.ts @@ -0,0 +1,164 @@ +import { describe, expect, test } from 'bun:test'; +import { FormatRegistry } from '@sinclair/typebox'; +import { Value } from '@sinclair/typebox/value'; +import { + APIPaymentLink, + APIWebhook, + RESTGetListWebhooksData, + RESTPostChangeSubscriptionPlanData, + RESTPostCreateBoletoBody, + RESTPostCreateNewCheckoutBody, + RESTPostCreateTransparentBody, + RESTPostCreateWebhookBody, + RESTPostSendPixTransferBody, + WebhookEvent, +} from '../src/v2'; + +// The package doesn't register a `uri` format validator itself (pre-existing +// gap, not specific to these tests), so `Value.Check` would otherwise reject +// every `format: 'uri'` field with "Unknown format 'uri'". +if (!FormatRegistry.Has('uri')) FormatRegistry.Set('uri', () => true); + +describe('v2 schemas', () => { + test('checkout create body accepts an array of methods and the new fields', () => { + const body = { + methods: ['PIX', 'CARD', 'BOLETO'], + items: [{ id: 'prod_123', quantity: 1 }], + frequency: 'SUBSCRIPTION', + interest: { value: 100 }, + fine: { value: 200, type: 'PERCENTAGE' }, + }; + + expect(Value.Check(RESTPostCreateNewCheckoutBody, body)).toBe(true); + }); + + test('transparent create body wire envelope accepts PIX and BOLETO variants', () => { + const pix = { method: 'PIX', data: { amount: 1000 } }; + const boleto = { + method: 'BOLETO', + data: { + amount: 1000, + customer: { name: 'Daniel Lima', taxId: '012.345.678-90' }, + }, + }; + + expect(Value.Check(RESTPostCreateTransparentBody, pix)).toBe(true); + expect(Value.Check(RESTPostCreateTransparentBody, boleto)).toBe(true); + }); + + test('RESTPostCreateBoletoBody requires name and taxId', () => { + expect( + Value.Check(RESTPostCreateBoletoBody, { amount: 1000, customer: {} }), + ).toBe(false); + }); + + test('pix transfer send body validates the destination key', () => { + const body = { + amount: 100, + externalId: 'trx_123', + pix: { key: 'foo@bar.com', type: 'EMAIL' }, + }; + + expect(Value.Check(RESTPostSendPixTransferBody, body)).toBe(true); + }); + + test('APIPaymentLink requires frequency to be MULTIPLE_PAYMENTS', () => { + const invalid = { + id: 'bill_123', + url: 'https://myshop.com/pay', + amount: 1000, + paidAmount: null, + status: 'PENDING', + frequency: 'ONE_TIME', + items: [{ id: 'prod_123', quantity: 1 }], + externalId: null, + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(Value.Check(APIPaymentLink, invalid)).toBe(false); + }); + + test('APIWebhook parses a full webhook resource', () => { + const webhook = { + id: 'webh_123', + name: 'Order fulfillment', + endpoint: 'https://myshop.com/webhooks/abacatepay', + events: ['checkout.completed', 'payout.failed'], + devMode: false, + v2: true, + createdAt: new Date(), + updatedAt: new Date(), + }; + + expect(Value.Check(APIWebhook, webhook)).toBe(true); + }); + + test('RESTPostCreateWebhookBody accepts the full v2 event taxonomy', () => { + const body = { + name: 'My webhook', + endpoint: 'https://myshop.com/hook', + secret: 'whsec_123', + events: ['transfer.completed', 'subscription.trial_started'], + }; + + expect(Value.Check(RESTPostCreateWebhookBody, body)).toBe(true); + }); + + test('RESTGetListWebhooksData accepts the cursor-based pagination envelope', () => { + const result = { + data: [], + error: null, + success: true, + pagination: { + limit: 100, + hasNext: false, + hasPrevious: false, + nextCursor: null, + }, + }; + + expect(Value.Check(RESTGetListWebhooksData, result)).toBe(true); + }); + + test('RESTPostChangeSubscriptionPlanData parses a plan-change result', () => { + const result = { + data: { + id: 'sub_change_123', + subscriptionId: 'subs_123', + status: 'PENDING', + productId: 'prod_123', + quantity: 2, + newAmount: 8000, + requestedAt: new Date(), + }, + error: null, + success: true, + }; + + expect(Value.Check(RESTPostChangeSubscriptionPlanData, result)).toBe(true); + }); + + test('WebhookEvent discriminates payout.completed from payout.failed', () => { + const completed = { + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date(), + updatedAt: new Date(), + status: 'COMPLETE', + }, + }, + }; + + expect(Value.Check(WebhookEvent, completed)).toBe(true); + }); +}); diff --git a/packages/typebox/tsconfig.json b/packages/typebox/tsconfig.json index a47cede..12445ec 100644 --- a/packages/typebox/tsconfig.json +++ b/packages/typebox/tsconfig.json @@ -1,10 +1,8 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] } diff --git a/packages/types/package.json b/packages/types/package.json index 7611b44..9f04396 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,80 +1,80 @@ { - "version": "2.0.3", - "name": "@abacatepay/types", - "description": "Abacate Pay API typings that are always up to date.", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "license": "Apache-2.0", - "exports": { - ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" - }, - "./v1": { - "import": "./dist/v1/index.js", - "types": "./dist/v1/index.d.ts" - }, - "./v1/*": { - "import": "./dist/v1/*.js", - "types": "./dist/v1/*.d.ts" - }, - "./v2": { - "import": "./dist/v2/index.js", - "types": "./dist/v2/index.d.ts" - }, - "./v2/*": { - "import": "./dist/v2/*.js", - "types": "./dist/v2/*.d.ts" - }, - "./v*": null - }, - "typesVersions": { - "*": { - "v1": [ - "dist/v1/index.d.ts" - ], - "v2": [ - "dist/v2/index.d.ts" - ] - } - }, - "files": [ - "dist" - ], - "scripts": { - "build": "bun run gen:version && tsc", - "prepublishOnly": "bun run build", - "gen:version": "bun run scripts/version.ts", - "test": "bun test", - "fmt": "bunx biome check --write package.json types && clear" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.8", - "typescript": "^5.9.3" - }, - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - } - ], - "keywords": [ - "api", - "abacatepay", - "types", - "typescript" - ], - "publishConfig": { - "access": "public" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - } + "version": "3.0.0", + "name": "@abacatepay/types", + "description": "Abacate Pay API typings that are always up to date.", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "license": "Apache-2.0", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./v1": { + "import": "./dist/v1/index.js", + "types": "./dist/v1/index.d.ts" + }, + "./v1/*": { + "import": "./dist/v1/*.js", + "types": "./dist/v1/*.d.ts" + }, + "./v2": { + "import": "./dist/v2/index.js", + "types": "./dist/v2/index.d.ts" + }, + "./v2/*": { + "import": "./dist/v2/*.js", + "types": "./dist/v2/*.d.ts" + }, + "./v*": null + }, + "typesVersions": { + "*": { + "v1": [ + "dist/v1/index.d.ts" + ], + "v2": [ + "dist/v2/index.d.ts" + ] + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "bun run gen:version && tsc", + "prepublishOnly": "bun run build", + "gen:version": "bun run scripts/version.ts", + "test": "bun test", + "fmt": "bunx biome check --write package.json types && clear" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.8", + "typescript": "^5.9.3" + }, + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + } + ], + "keywords": [ + "api", + "abacatepay", + "types", + "typescript" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + } } diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json index c82d104..16ca11e 100644 --- a/packages/types/tsconfig.json +++ b/packages/types/tsconfig.json @@ -1,13 +1,11 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "emitDeclarationOnly": false, - "outDir": "./dist", - "rootDir": "./types", - "resolveJsonModule": true, - "esModuleInterop": true - }, - "include": [ - "types" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "emitDeclarationOnly": false, + "outDir": "./dist", + "rootDir": "./types", + "resolveJsonModule": true, + "esModuleInterop": true + }, + "include": ["types"] } diff --git a/packages/types/types/v2/entities/checkout.ts b/packages/types/types/v2/entities/checkout.ts index fcd4bf3..edb1058 100644 --- a/packages/types/types/v2/entities/checkout.ts +++ b/packages/types/types/v2/entities/checkout.ts @@ -77,6 +77,38 @@ export interface APICheckout { * Charge last updated date and time. */ updatedAt: string; + /** + * Billing frequency. Defaults to `ONE_TIME`. + * + * @see {@link PaymentFrequency} + */ + frequency?: PaymentFrequency | 'SUBSCRIPTION'; + /** + * ID of an additional product offered as an upsell. + */ + upSellProductId?: string | null; + /** + * Late interest configuration (Applies to BOLETO). + */ + interest?: { + /** + * Monthly interest rate, in hundredths of a percent. + */ + value: number; + } | null; + /** + * Late fine configuration (Applies to BOLETO). + */ + fine?: { + /** + * Fine value. + */ + value: number; + /** + * Type of fine applied. + */ + type: 'PERCENTAGE' | 'FIXED'; + } | null; } /** @@ -96,14 +128,14 @@ export enum PaymentStatus { export enum PaymentMethod { Pix = 'PIX', Card = 'CARD', + Boleto = 'BOLETO', } /** * https://docs.abacatepay.com/pages/payment/create#body-frequency - * - * @deprecated */ export enum PaymentFrequency { OneTime = 'ONE_TIME', Multiple = 'MULTIPLE_PAYMENTS', + Subscription = 'SUBSCRIPTION', } diff --git a/packages/types/types/v2/entities/paymentLink.ts b/packages/types/types/v2/entities/paymentLink.ts new file mode 100644 index 0000000..3c378f3 --- /dev/null +++ b/packages/types/types/v2/entities/paymentLink.ts @@ -0,0 +1,61 @@ +import type { PaymentStatus } from './checkout'; + +/** + * A reusable payment link — unlike a Checkout, the same link can be paid by + * multiple customers instead of being tied to a single transaction. + * + * https://docs.abacatepay.com/pages/payment-links/reference + */ +export interface APIPaymentLink { + /** + * Unique payment link identifier. + */ + id: string; + /** + * Shareable checkout URL. + */ + url: string; + /** + * Total amount in cents. + */ + amount: number; + /** + * Amount paid in cents. `null` if it has not yet been paid. + */ + paidAmount: number | null; + /** + * Payment link status. + * + * @see {@link PaymentStatus} + */ + status: PaymentStatus; + /** + * Always `MULTIPLE_PAYMENTS` for payment links. + */ + frequency: 'MULTIPLE_PAYMENTS'; + /** + * List of items included in the payment link. + */ + items: { + /** + * Product ID. + */ + id: string; + /** + * Item quantity. + */ + quantity: number; + }[]; + /** + * Reference ID in your system. + */ + externalId: string | null; + /** + * Payment link creation date and time. + */ + createdAt: string; + /** + * Payment link last updated date and time. + */ + updatedAt: string; +} diff --git a/packages/types/types/v2/entities/pix.ts b/packages/types/types/v2/entities/pix.ts index 78ec144..8bb8efa 100644 --- a/packages/types/types/v2/entities/pix.ts +++ b/packages/types/types/v2/entities/pix.ts @@ -46,4 +46,146 @@ export interface APIQRCodePIX { * QRCode expiration date and time. */ expiresAt: string; + /** + * Additional metadata attached to the charge. + */ + metadata?: Record; +} + +/** + * https://docs.abacatepay.com/pages/transparents/boleto + */ +export interface APIBoleto { + /** + * Unique Boleto identifier. + */ + id: string; + /** + * Charge amount in cents. + */ + amount: number; + /** + * Boleto status. + * + * @see {@link PaymentStatus} + */ + status: PaymentStatus; + /** + * Indicates whether the charge is in a testing (true) or production (false) environment. + */ + devMode: boolean; + /** + * Boleto barcode. + */ + barCode: string; + /** + * URL to view/print the Boleto. + */ + url: string; + /** + * PIX alternative for paying the same Boleto. + */ + pix: { + /** + * PIX code (copy-and-paste) for payment. + */ + brCode: string; + /** + * PIX code in Base64 format (Useful for displaying in images). + */ + brCodeBase64: string; + }; + /** + * Platform fee in cents. + */ + platformFee: number; + /** + * Payment receipt URL. + */ + receiptUrl: string | null; + /** + * Boleto creation date and time. + */ + createdAt: string; + /** + * Boleto last updated date and time. + */ + updatedAt: string; + /** + * Boleto expiration date and time. + */ + expiresAt: string; + /** + * Additional metadata attached to the charge. + */ + metadata?: Record; +} + +/** + * Outbound PIX transfer to a third-party PIX key (Sending money out). + * + * https://docs.abacatepay.com/pages/pix/reference + */ +export interface APIPixTransfer { + /** + * Unique transfer identifier. + */ + id: string; + /** + * Transfer status. + * + * @see {@link PixTransferStatus} + */ + status: PixTransferStatus; + /** + * Indicates whether the transfer is in a testing (true) or production (false) environment. + */ + devMode: boolean; + /** + * Proof-of-transfer URL, available once completed. + */ + receiptUrl: string | null; + /** + * Transfer amount in cents. + */ + amount: number; + /** + * Platform fee in cents. + */ + platformFee: number; + /** + * Unique transfer identifier in your system. + */ + externalId: string | null; + /** + * Transfer creation date and time. + */ + createdAt: string; + /** + * Transfer last updated date and time. + */ + updatedAt: string; +} + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export enum PixTransferStatus { + Pending = 'PENDING', + Expired = 'EXPIRED', + Cancelled = 'CANCELLED', + Complete = 'COMPLETE', + Refunded = 'REFUNDED', +} + +/** + * https://docs.abacatepay.com/pages/pix/create + */ +export enum PixKeyType { + CPF = 'CPF', + CNPJ = 'CNPJ', + Phone = 'PHONE', + Email = 'EMAIL', + Random = 'RANDOM', + BRCode = 'BR_CODE', } diff --git a/packages/types/types/v2/entities/subscription.ts b/packages/types/types/v2/entities/subscription.ts index fec8b8a..97fb51c 100644 --- a/packages/types/types/v2/entities/subscription.ts +++ b/packages/types/types/v2/entities/subscription.ts @@ -92,6 +92,82 @@ export enum SubscriptionStatus { Failed = 'FAILED', } +/** + * Result of `POST /subscriptions/change-plan`. + * + * https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export interface APISubscriptionPlanChange { + /** + * Unique identifier of this plan-change request. + */ + id: string; + /** + * ID of the subscription being changed. + */ + subscriptionId: string; + /** + * Status of the plan-change request. + */ + status: 'PENDING' | 'APPLIED' | 'CANCELLED'; + /** + * ID of the new product. + */ + productId: string; + /** + * New quantity for the product. + */ + quantity: number; + /** + * Amount that will be charged once the change is applied, in cents. + */ + newAmount: number; + /** + * When the change was requested. + */ + requestedAt: string; +} + +/** + * Result of `POST /subscriptions/record-usage`. + * + * https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export interface APISubscriptionUsageRecord { + /** + * Unique identifier of this usage record. + */ + id: string; + /** + * ID of the subscription the usage was recorded against. + */ + subscriptionId: string; + /** + * ID of the pay-as-you-go product the usage applies to. + */ + productId: string; + /** + * Number of units recorded. + */ + units: number; + /** + * Price per unit, in cents. + */ + unitPrice: number; + /** + * Whether the units were added to or subtracted from the current cycle. + */ + action: 'add' | 'subtract'; + /** + * The pending installment this usage record was attached to. + */ + installmentNumber: number; + /** + * When the usage was recorded. + */ + recordedAt: string; +} + /** * https://docs.abacatepay.com/pages/subscriptions/reference#estrutura */ diff --git a/packages/types/types/v2/index.ts b/packages/types/types/v2/index.ts index cb8507d..2190c08 100644 --- a/packages/types/types/v2/index.ts +++ b/packages/types/types/v2/index.ts @@ -1,6 +1,7 @@ export * from './entities/checkout'; export * from './entities/coupon'; export * from './entities/customer'; +export * from './entities/paymentLink'; export * from './entities/payout'; export * from './entities/pix'; export * from './entities/products'; diff --git a/packages/types/types/v2/rest.ts b/packages/types/types/v2/rest.ts index d125e84..d34ae3d 100644 --- a/packages/types/types/v2/rest.ts +++ b/packages/types/types/v2/rest.ts @@ -1,16 +1,26 @@ import type { + APIBoleto, APICheckout, APICoupon, APICustomer, APIPayout, + APIPixTransfer, APIQRCodePIX, APIStore, + APIWebhook, CouponDiscountKind, PaymentMethod, PaymentStatus, + PixKeyType, + WebhookEventType, } from '.'; +import type { APIPaymentLink } from './entities/paymentLink'; import type { APIProduct } from './entities/products'; -import type { APISubscription } from './entities/subscription'; +import type { + APISubscription, + APISubscriptionPlanChange, + APISubscriptionUsageRecord, +} from './entities/subscription'; /** * Any response returned by the AbacatePay API. @@ -150,20 +160,20 @@ export type RESTPostCreateCustomerBody = Pick & * * @reference https://docs.abacatepay.com/pages/client/create */ -export type RESTPostCreateCustomerData = APICustomer; +export type RESTPostCreateCustomerData = APIResponse; /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkout/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export interface RESTPostCreateNewCheckoutBody { /** - * Payment method that will be used. + * Payment methods that will be accepted (Defaults to `[PIX, CARD]`). * * @see {@link PaymentMethod} */ - methods?: PaymentMethod; + methods?: PaymentMethod[]; /** * URL to redirect the customer if they click on the "Back" option. */ @@ -198,17 +208,75 @@ export interface RESTPostCreateNewCheckoutBody { * This is the only required field — the total value is calculated from these items. */ items: APICheckout['items']; + /** + * Billing frequency. Defaults to `ONE_TIME`. + */ + frequency?: 'ONE_TIME' | 'MULTIPLE_PAYMENTS' | 'SUBSCRIPTION'; + /** + * ID of an additional product offered as an upsell. + */ + upSellProductId?: string; + /** + * Late interest configuration (Applies to BOLETO). + */ + interest?: { + /** + * Monthly interest rate, in hundredths of a percent. + */ + value: number; + }; + /** + * Late fine configuration (Applies to BOLETO). + */ + fine?: { + /** + * Fine value. + */ + value: number; + /** + * Type of fine applied. + */ + type: 'PERCENTAGE' | 'FIXED'; + }; } /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkouts/create + * @reference https://docs.abacatepay.com/pages/payment/create */ -export type RESTPostCreateNewCheckoutData = APICheckout; +export type RESTPostCreateNewCheckoutData = APIResponse; /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export interface RESTPostRefundCheckoutBody { + /** + * Public ID of the resource to refund (prefixes: `bill_`, `char_`, `pix_char_`, `card_`). + */ + id: string; + /** + * Refund reason, shown in the transaction history (Max 500 characters). + */ + reason?: string; +} + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export type RESTPostRefundCheckoutData = APIResponse<{ + /** + * Public ID of the refund transaction that was created. + */ + refundPublicId: string; +}>; + +/** + * Inner `data` payload sent to `POST /transparents/create` when `method` is `PIX`. * * @reference https://docs.abacatepay.com/pages/transparents/create */ @@ -233,7 +301,117 @@ export interface RESTPostCreateQRCodePixBody * * @reference https://docs.abacatepay.com/pages/transparents/create */ -export type RESTPostCreateQRCodePixData = APIQRCodePIX; +export type RESTPostCreateQRCodePixData = APIResponse; + +/** + * Inner `data` payload sent to `POST /transparents/create` when `method` is `BOLETO`. + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export interface RESTPostCreateBoletoBody { + /** + * Charge amount in cents. + */ + amount: number; + /** + * Message that will appear on the Boleto. + */ + description?: string; + /** + * Customer data. `name` and `taxId` are mandatory for Boleto. + */ + customer: Pick & + Partial>; + /** + * Optional charge metadata. + */ + metadata?: Record; +} + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export type RESTPostCreateBoletoData = APIResponse; + +/** + * Wire-level request body for `POST /transparents/create` — the SDK builds this + * from {@link RESTPostCreateQRCodePixBody} / {@link RESTPostCreateBoletoBody}, callers + * of the SDK never construct it directly. + * + * @reference https://docs.abacatepay.com/pages/transparents/reference + */ +export type RESTPostCreateTransparentBody = + | { method: 'PIX'; data: RESTPostCreateQRCodePixBody } + | { method: 'BOLETO'; data: RESTPostCreateBoletoBody }; + +/** + * https://api.abacatepay.com/v2/transparents/list + * + * @reference https://docs.abacatepay.com/pages/transparents/list + */ +export interface RESTGetListTransparentsQueryParams { + /** + * Cursor for the next page. + */ + after?: string; + /** + * Cursor for the previous page. + */ + before?: string; + /** + * Number of items per page (1-100). + * + * @default 100 + */ + limit?: number; + /** + * Filter by QRCode/Boleto identifier. + */ + id?: string; + /** + * Filter by status. + */ + status?: PaymentStatus; +} + +/** + * https://api.abacatepay.com/v2/transparents/list + * + * @reference https://docs.abacatepay.com/pages/transparents/list + */ +export type RESTGetListTransparentsData = APIResponseWithCursorBasedPagination< + (APIQRCodePIX | APIBoleto)[] +>; + +/** + * https://api.abacatepay.com/v2/transparents/refund + * + * @reference https://docs.abacatepay.com/pages/transparents/refund + */ +export interface RESTPostRefundTransparentBody { + /** + * Public ID of the resource to refund (prefixes: `char_`, `pix_char_`, `card_`, `bill_`). + */ + id: string; + /** + * Refund reason, shown in the transaction history (Max 500 characters). + */ + reason?: string; +} + +/** + * https://api.abacatepay.com/v2/transparents/refund + * + * @reference https://docs.abacatepay.com/pages/transparents/refund + */ +export type RESTPostRefundTransparentData = APIResponse<{ + /** + * Public ID of the refund transaction that was created. + */ + refundPublicId: string; +}>; /** * https://api.abacatepay.com/v2/transparents/simulate-payment @@ -264,12 +442,12 @@ export interface RESTPostSimulateQRCodePixPaymentBody { * * @reference https://docs.abacatepay.com/pages/transparents/simulate-payment */ -export type RESTPostSimulateQRCodePixPaymentData = APIQRCodePIX; +export type RESTPostSimulateQRCodePixPaymentData = APIResponse; /** - * https://api.abacatepay.com/v2/pixQrCode/check + * https://api.abacatepay.com/v2/transparents/check * - * @reference https://docs.abacatepay.com/pages/pix-qrcode/check + * @reference https://docs.abacatepay.com/pages/transparents/check */ export interface RESTGetCheckQRCodePixStatusQueryParams { /** @@ -283,7 +461,7 @@ export interface RESTGetCheckQRCodePixStatusQueryParams { * * @reference https://docs.abacatepay.com/pages/transparents/check */ -export interface RESTGetCheckQRCodePixStatusData { +export type RESTGetCheckQRCodePixStatusData = APIResponse<{ /** * QRCode Pix expiration date. */ @@ -292,12 +470,12 @@ export interface RESTGetCheckQRCodePixStatusData { * Information about the progress of QRCode Pix. */ status: PaymentStatus; -} +}>; /** * https://api.abacatepay.com/v2/checkouts/list * - * @reference https://docs.abacatepay.com/pages/checkouts/list + * @reference https://docs.abacatepay.com/pages/payment/list */ export interface RESTGetListCheckoutsQueryParams { /** @@ -317,21 +495,21 @@ export interface RESTGetListCheckoutsQueryParams { /** * https://api.abacatepay.com/v2/checkouts/list * - * @reference https://docs.abacatepay.com/pages/checkouts/list + * @reference https://docs.abacatepay.com/pages/payment/list */ -export type RESTGetListCheckoutsData = APICheckout[]; +export type RESTGetListCheckoutsData = APIResponseWithPagination; /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ -export type RESTGetCheckoutData = APICheckout; +export type RESTGetCheckoutData = APIResponse; /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export interface RESTGetCheckoutQueryParams { /** @@ -340,12 +518,105 @@ export interface RESTGetCheckoutQueryParams { id: string; } +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export type RESTPostCreatePaymentLinkBody = Omit< + RESTPostCreateNewCheckoutBody, + 'customerId' | 'customer' | 'frequency' | 'upSellProductId' +>; + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export type RESTPostCreatePaymentLinkData = APIResponse; + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export interface RESTGetListPaymentLinksQueryParams { + /** + * Number of the page. + * + * @default 1 + */ + page?: number; + /** + * Number of items per page. + * + * @default 20 + */ + limit?: number; +} + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export type RESTGetListPaymentLinksData = APIResponseWithPagination< + APIPaymentLink[] +>; + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export interface RESTGetPaymentLinkQueryParams { + /** + * Unique payment link identifier. + */ + id: string; +} + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export type RESTGetPaymentLinkData = APIResponse; + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export interface RESTPostRefundPaymentLinkBody { + /** + * Public ID of the resource to refund. + */ + id: string; + /** + * Refund reason, shown in the transaction history (Max 500 characters). + */ + reason?: string; +} + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export type RESTPostRefundPaymentLinkData = APIResponse<{ + /** + * Public ID of the refund transaction that was created. + */ + refundPublicId: string; +}>; + /** * https://api.abacatepay.com/v2/customers/list * * @reference https://docs.abacatepay.com/pages/client/list */ -export type RESTGetListCustomersData = APICustomer[]; +export type RESTGetListCustomersData = APIResponseWithPagination; /** * https://api.abacatepay.com/v2/customers/list @@ -380,7 +651,9 @@ export interface RESTGetCustomerQueryParams { * * @reference https://docs.abacatepay.com/pages/client/get */ -export type RESTGetCustomerData = Omit; +export type RESTGetCustomerData = APIResponse< + Omit +>; /** * https://api.abacatepay.com/v2/customers/delete @@ -399,7 +672,9 @@ export interface RESTDeleteCustomerBody { * * @reference https://docs.abacatepay.com/pages/client/delete */ -export type RESTDeleteCustomerData = Omit; +export type RESTDeleteCustomerData = APIResponse< + Omit +>; /** * https://api.abacatepay.com/v2/coupons/create @@ -440,11 +715,11 @@ export interface RESTPostCreateCouponBody { } /** - * https://api.abacatepay.com/v2/coupon/create + * https://api.abacatepay.com/v2/coupons/create * - * @reference https://docs.abacatepay.com/pages/coupon/create + * @reference https://docs.abacatepay.com/pages/coupons/create */ -export type RESTPostCreateCouponData = APICoupon; +export type RESTPostCreateCouponData = APIResponse; /** * https://api.abacatepay.com/v2/payouts/create @@ -471,7 +746,7 @@ export interface RESTPostCreateNewPayoutBody { * * @reference https://docs.abacatepay.com/pages/payouts/create */ -export type RESTPostCreateNewWPayoutData = APIPayout; +export type RESTPostCreateNewWPayoutData = APIResponse; /** * https://api.abacatepay.com/v2/payouts/get @@ -490,7 +765,7 @@ export interface RESTGetSearchPayoutQueryParams { * * @reference https://docs.abacatepay.com/pages/payouts/get */ -export type RESTGetSearchPayoutData = APIPayout; +export type RESTGetSearchPayoutData = APIResponse; /** * https://api.abacatepay.com/v2/payouts/list @@ -512,6 +787,121 @@ export interface RESTGetListPayoutsQueryParams { limit?: number; } +/** + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export type RESTGetListPayoutsData = APIResponseWithPagination; + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export interface RESTPostSendPixTransferBody { + /** + * Transfer amount in cents (Min 100). + */ + amount: number; + /** + * Unique identifier of the transfer in your system. + */ + externalId: string; + /** + * Optional transfer description. + */ + description?: string; + /** + * Destination PIX key. + */ + pix: { + /** + * The PIX key itself. + */ + key: string; + /** + * Type of the PIX key. + * + * @see {@link PixKeyType} + */ + type: PixKeyType; + }; +} + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export type RESTPostSendPixTransferData = APIResponse; + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export interface RESTGetPixTransferQueryParams { + /** + * Unique transfer identifier in AbacatePay. At least one of `id`/`externalId` is required. + */ + id?: string; + /** + * Unique transfer identifier in your system. At least one of `id`/`externalId` is required. + */ + externalId?: string; +} + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export type RESTGetPixTransferData = APIResponse; + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export interface RESTGetListPixTransfersQueryParams { + /** + * Number of items per page (1-100). + * + * @default 100 + */ + limit?: number; + /** + * Cursor for the next page. + */ + after?: string; + /** + * Cursor for the previous page. + */ + before?: string; + /** + * Filter by AbacatePay transaction ID. + */ + id?: string; + /** + * Filter by external system ID. + */ + externalId?: string; + /** + * Filter by transaction status. + */ + status?: string; +} + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export type RESTGetListPixTransfersData = APIResponseWithCursorBasedPagination< + APIPixTransfer[] +>; + /** * https://api.abacatepay.com/v2/public-mrr/revenue * @@ -533,7 +923,7 @@ export interface RESTGetRevenueByPeriodQueryParams { * * @reference https://docs.abacatepay.com/pages/trustMRR/list */ -export interface RESTGetRevenueByPeriodData { +export type RESTGetRevenueByPeriodData = APIResponse<{ /** * Total revenue for the period in cents. */ @@ -558,14 +948,14 @@ export interface RESTGetRevenueByPeriodData { count: number; } >; -} +}>; /** * https://api.abacatepay.com/v2/public-mrr/merchant-info * * @reference https://docs.abacatepay.com/pages/trustMRR/get */ -export interface RESTGetMerchantData { +export type RESTGetMerchantData = APIResponse<{ /** * Store name. */ @@ -578,14 +968,14 @@ export interface RESTGetMerchantData { * Store creation date. */ createdAt: string; -} +}>; /** * https://api.abacatepay.com/v2/public-mrr/mrr * * @reference https://docs.abacatepay.com/pages/trustMRR/mrr */ -export interface RESTGetMRRData { +export type RESTGetMRRData = APIResponse<{ /** * Monthly recurring revenue in cents. Value 0 indicates that there is no recurring revenue at the moment. */ @@ -594,28 +984,21 @@ export interface RESTGetMRRData { * Total active subscriptions. Value 0 indicates that there are no currently active subscriptions. */ totalActiveSubscriptions: number; -} +}>; /** * https://api.abacatepay.com/v2/store/get * * @reference https://docs.abacatepay.com/pages/store/get */ -export type RESTGetStoreDetailsData = APIStore; - -/** - * https://api.abacatepay.com/v2/payouts/list - * - * @reference https://docs.abacatepay.com/pages/payouts/list - */ -export type RESTGetListPayoutsData = APIPayout[]; +export type RESTGetStoreDetailsData = APIResponse; /** * https://api.abacatepay.com/v2/coupons/list * * @reference https://docs.abacatepay.com/pages/coupons/list */ -export type RESTGetListCouponsData = APICoupon[]; +export type RESTGetListCouponsData = APIResponseWithPagination; /** * https://api.abacatepay.com/v2/coupons/list @@ -652,7 +1035,7 @@ export interface RESTGetCouponQueryParams { * * @reference https://docs.abacatepay.com/pages/coupons/get */ -export type RESTGetCouponData = APICoupon; +export type RESTGetCouponData = APIResponse; /** * https://api.abacatepay.com/v2/coupons/delete @@ -671,14 +1054,14 @@ export interface RESTDeleteCouponBody { * * @reference https://docs.abacatepay.com/pages/coupons/delete */ -export type RESTDeleteCouponData = APICoupon; +export type RESTDeleteCouponData = APIResponse; /** * https://api.abacatepay.com/v2/coupons/toggle * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export interface RESTPatchToggleCouponStatusBody { +export interface RESTPostToggleCouponStatusBody { /** * The ID of the coupon. */ @@ -690,7 +1073,7 @@ export interface RESTPatchToggleCouponStatusBody { * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export type RESTPatchToggleCouponStatusData = APICoupon; +export type RESTPostToggleCouponStatusData = APIResponse; /** * https://api.abacatepay.com/v2/products/create @@ -710,7 +1093,7 @@ export interface RESTPostCreateProductBody * * @reference https://docs.abacatepay.com/pages/products/create */ -export type RESTPostCreateProductData = APIProduct; +export type RESTPostCreateProductData = APIResponse; /** * https://api.abacatepay.com/v2/products/list @@ -733,7 +1116,7 @@ export interface RESTGetListProductsQueryParams { * * @reference https://docs.abacatepay.com/pages/products/list */ -export type RESTGetListProductsData = APIProduct[]; +export type RESTGetListProductsData = APIResponseWithPagination; /** * https://api.abacatepay.com/v2/products/get @@ -756,7 +1139,26 @@ export interface RESTGetProductQueryParams { * * @reference https://docs.abacatepay.com/pages/products/get */ -export type RESTGetProductData = APIProduct; +export type RESTGetProductData = APIResponse; + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export interface RESTDeleteProductQueryParams { + /** + * The product ID. + */ + id: string; +} + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export type RESTDeleteProductData = APIResponse; /** * https://api.abacatepay.com/v2/subscriptions/create @@ -785,7 +1187,7 @@ export interface RESTPostCreateSubscriptionBody * * @reference https://docs.abacatepay.com/pages/subscriptions/create */ -export type RESTPostCreateSubscriptionData = APISubscription; +export type RESTPostCreateSubscriptionData = APIResponse; /** * https://api.abacatepay.com/v2/subscriptions/list @@ -810,4 +1212,197 @@ export interface RESTGetListSubscriptionsQueryParams { * * @reference https://docs.abacatepay.com/pages/subscriptions/list */ -export type RESTGetListSubscriptionsData = APISubscription[]; +export type RESTGetListSubscriptionsData = APIResponseWithCursorBasedPagination< + APISubscription[] +>; + +/** + * https://api.abacatepay.com/v2/subscriptions/cancel + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export interface RESTPostCancelSubscriptionBody { + /** + * Unique subscription identifier. + */ + id: string; +} + +/** + * Cancellation is immediate (`cancelPolicy: NOW`) — there is no grace period. + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export type RESTPostCancelSubscriptionData = APIResponse; + +/** + * https://api.abacatepay.com/v2/subscriptions/change-plan + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export interface RESTPostChangeSubscriptionPlanBody { + /** + * Unique subscription identifier. + */ + id: string; + /** + * ID of the new product. It must have a billing cycle configured. + */ + productId: string; + /** + * New quantity for the product. + */ + quantity: number; +} + +/** + * Only one `PENDING` change can exist per subscription — calling this again + * replaces the prior unapplied change. The change is applied at the next + * billing cycle. + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type RESTPostChangeSubscriptionPlanData = + APIResponse; + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export interface RESTPostRecordSubscriptionUsageBody { + /** + * Unique subscription identifier. + */ + id: string; + /** + * ID of the pay-as-you-go product (Must not have a billing cycle). + */ + productId: string; + /** + * Number of units to record. + */ + units: number; + /** + * Whether to add or subtract the units from the current cycle. + */ + action: 'add' | 'subtract'; +} + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export type RESTPostRecordSubscriptionUsageData = + APIResponse; + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export interface RESTPostCreateWebhookBody { + /** + * Webhook name, for your own identification. + */ + name: string; + /** + * HTTPS endpoint that will receive the events. + */ + endpoint: string; + /** + * Secret used to sign the payloads sent to `endpoint`. + */ + secret: string; + /** + * Event types this webhook should be notified about. + * + * @see {@link WebhookEventType} + */ + events: WebhookEventType[]; +} + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export type RESTPostCreateWebhookData = APIResponse; + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export interface RESTGetListWebhooksQueryParams { + /** + * Search by webhook name, ID, or endpoint. + */ + search?: string; + /** + * Cursor for the next page. + */ + after?: string; + /** + * Cursor for the previous page. + */ + before?: string; + /** + * Number of items per page (1-100). + * + * @default 100 + */ + limit?: number; + /** + * Filter by a specific webhook ID. + */ + id?: string; +} + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export type RESTGetListWebhooksData = APIResponseWithCursorBasedPagination< + APIWebhook[] +>; + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export interface RESTGetWebhookQueryParams { + /** + * Unique webhook identifier. + */ + id: string; +} + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export type RESTGetWebhookData = APIResponse; + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export interface RESTPostDeleteWebhookBody { + /** + * Unique webhook identifier. + */ + id: string; +} + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export type RESTPostDeleteWebhookData = APIResponse; diff --git a/packages/types/types/v2/routes.ts b/packages/types/types/v2/routes.ts index f492585..70287a3 100644 --- a/packages/types/types/v2/routes.ts +++ b/packages/types/types/v2/routes.ts @@ -2,9 +2,14 @@ import type { RESTGetListCheckoutsQueryParams, RESTGetListCouponsQueryParams, RESTGetListCustomersQueryParams, + RESTGetListPaymentLinksQueryParams, RESTGetListPayoutsQueryParams, + RESTGetListPixTransfersQueryParams, RESTGetListProductsQueryParams, RESTGetListSubscriptionsQueryParams, + RESTGetListTransparentsQueryParams, + RESTGetListWebhooksQueryParams, + RESTGetPixTransferQueryParams, RESTGetProductQueryParams, } from './rest'; @@ -30,7 +35,7 @@ export const Routes = { }, /** - * DELETE - https://api.abacatepay.com/v2/customers/delete + * POST - https://api.abacatepay.com/v2/customers/delete */ delete: '/customers/delete', }, @@ -53,12 +58,62 @@ export const Routes = { get(id: string) { return `/checkouts/get?id=${id}` as const; }, + + /** + * POST - https://api.abacatepay.com/v2/checkouts/refund + */ + refund: '/checkouts/refund', + }, + paymentLinks: { + /** + * POST - https://api.abacatepay.com/v2/payment-links/create + */ + create: '/payment-links/create', + + /** + * GET - https://api.abacatepay.com/v2/payment-links/list + */ + list({ page = 1, limit = 20 }: RESTGetListPaymentLinksQueryParams = {}) { + return `/payment-links/list?page=${page}&limit=${limit}` as const; + }, + + /** + * GET - https://api.abacatepay.com/v2/payment-links/one + */ + get(id: string) { + return `/payment-links/one?id=${id}` as const; + }, + + /** + * POST - https://api.abacatepay.com/v2/payment-links/refund + */ + refund: '/payment-links/refund', }, transparents: { /** * POST - https://api.abacatepay.com/v2/transparents/create */ - createQRCode: '/transparents/create', + create: '/transparents/create', + + /** + * GET - https://api.abacatepay.com/v2/transparents/list + */ + list({ + limit = 100, + after, + before, + id, + status, + }: RESTGetListTransparentsQueryParams = {}) { + const query = new URLSearchParams({ limit: `${limit}` }); + + if (after) query.append('after', after); + if (before) query.append('before', before); + if (id) query.append('id', id); + if (status) query.append('status', status); + + return `/transparents/list?${query}` as const; + }, /** * POST - https://api.abacatepay.com/v2/transparents/simulate-payment @@ -73,6 +128,51 @@ export const Routes = { checkStatus(id: string) { return `/transparents/check?id=${id}` as const; }, + + /** + * POST - https://api.abacatepay.com/v2/transparents/refund + */ + refund: '/transparents/refund', + }, + transfers: { + /** + * POST - https://api.abacatepay.com/v2/pix/send + */ + send: '/pix/send', + + /** + * GET - https://api.abacatepay.com/v2/pix/get + */ + get({ id, externalId }: RESTGetPixTransferQueryParams = {}) { + const query = new URLSearchParams(); + + if (id) query.append('id', id); + if (externalId) query.append('externalId', externalId); + + return `/pix/get?${query}` as const; + }, + + /** + * GET - https://api.abacatepay.com/v2/pix/list + */ + list({ + limit = 100, + after, + before, + id, + externalId, + status, + }: RESTGetListPixTransfersQueryParams = {}) { + const query = new URLSearchParams({ limit: `${limit}` }); + + if (after) query.append('after', after); + if (before) query.append('before', before); + if (id) query.append('id', id); + if (externalId) query.append('externalId', externalId); + if (status) query.append('status', status); + + return `/pix/list?${query}` as const; + }, }, coupons: { /** @@ -95,12 +195,12 @@ export const Routes = { }, /** - * DELETE - https://api.abacatepay.com/v2/coupons/delete + * POST - https://api.abacatepay.com/v2/coupons/delete */ delete: '/coupons/delete', /** - * PATCH - https://api.abacatepay.com/v2/coupons/toggle + * POST - https://api.abacatepay.com/v2/coupons/toggle */ toggleStatus: '/coupons/toggle', }, @@ -164,6 +264,21 @@ export const Routes = { return `/subscriptions/list?${query}` as const; }, + + /** + * POST - https://api.abacatepay.com/v2/subscriptions/cancel + */ + cancel: '/subscriptions/cancel', + + /** + * POST - https://api.abacatepay.com/v2/subscriptions/change-plan + */ + changePlan: '/subscriptions/change-plan', + + /** + * POST - https://api.abacatepay.com/v2/subscriptions/record-usage + */ + recordUsage: '/subscriptions/record-usage', }, products: { @@ -190,5 +305,50 @@ export const Routes = { return `/products/get?${query}` as const; }, + + /** + * POST - https://api.abacatepay.com/v2/products/delete + */ + delete(id: string) { + return `/products/delete?id=${id}` as const; + }, + }, + webhooks: { + /** + * POST - https://api.abacatepay.com/v2/webhooks/create + */ + create: '/webhooks/create', + + /** + * GET - https://api.abacatepay.com/v2/webhooks/list + */ + list({ + limit = 100, + after, + before, + search, + id, + }: RESTGetListWebhooksQueryParams = {}) { + const query = new URLSearchParams({ limit: `${limit}` }); + + if (after) query.append('after', after); + if (before) query.append('before', before); + if (search) query.append('search', search); + if (id) query.append('id', id); + + return `/webhooks/list?${query}` as const; + }, + + /** + * GET - https://api.abacatepay.com/v2/webhooks/get + */ + get(id: string) { + return `/webhooks/get?id=${id}` as const; + }, + + /** + * POST - https://api.abacatepay.com/v2/webhooks/delete + */ + delete: '/webhooks/delete', }, } as const; diff --git a/packages/types/types/v2/utils.ts b/packages/types/types/v2/utils.ts index 720aaff..b4691cd 100644 --- a/packages/types/types/v2/utils.ts +++ b/packages/types/types/v2/utils.ts @@ -1,12 +1,12 @@ import { type WebhookEvent, WebhookEventType } from './webhook'; /** - * A type guard check for `payout.done` webhook events. + * A type guard check for `payout.completed` webhook events. * @param event - The webhook event to check against. - * @returns A boolean that indicates if the webhook is a payout done webhook. + * @returns A boolean that indicates if the webhook is a payout completed webhook. */ -export function isPayoutDoneWebhookEvent(event: WebhookEvent) { - return event.event === WebhookEventType.PayoutDone; +export function isPayoutCompletedWebhookEvent(event: WebhookEvent) { + return event.event === WebhookEventType.PayoutCompleted; } /** @@ -19,10 +19,19 @@ export function isPayoutFailedWebhookEvent(event: WebhookEvent) { } /** - * A type guard check for `billing.paid` webhook events. + * A type guard check for `checkout.completed` webhook events. * @param event - The webhook event to check against. - * @returns A boolean that indicates if the webhook is a billing paid webhook. + * @returns A boolean that indicates if the webhook is a checkout completed webhook. */ -export function isBillingPaidWebhookEvent(event: WebhookEvent) { - return event.event === WebhookEventType.BillingPaid; +export function isCheckoutCompletedWebhookEvent(event: WebhookEvent) { + return event.event === WebhookEventType.CheckoutCompleted; +} + +/** + * A type guard check for `transparent.completed` webhook events. + * @param event - The webhook event to check against. + * @returns A boolean that indicates if the webhook is a transparent checkout completed webhook. + */ +export function isTransparentCompletedWebhookEvent(event: WebhookEvent) { + return event.event === WebhookEventType.TransparentCompleted; } diff --git a/packages/types/types/v2/webhook.ts b/packages/types/types/v2/webhook.ts index e371c89..2252ae9 100644 --- a/packages/types/types/v2/webhook.ts +++ b/packages/types/types/v2/webhook.ts @@ -1,5 +1,47 @@ import type { APIPayout, PaymentMethod, PaymentStatus, PayoutStatus } from '.'; +/** + * The webhook resource itself, as returned by the `webhooks/*` endpoints. + * + * https://docs.abacatepay.com/pages/webhooks/reference + */ +export interface APIWebhook { + /** + * Unique webhook identifier. + */ + id: string; + /** + * Webhook name. + */ + name: string; + /** + * HTTPS endpoint that receives the events. + */ + endpoint: string; + /** + * Event types this webhook is subscribed to. + * + * @see {@link WebhookEventType} + */ + events: WebhookEventType[]; + /** + * Indicates whether the webhook was created in a testing environment. + */ + devMode: boolean; + /** + * Indicates whether this webhook targets the v2 API. + */ + v2: boolean; + /** + * Webhook creation date and time. + */ + createdAt: string; + /** + * Webhook last updated date and time. + */ + updatedAt: string; +} + export interface BaseWebhookEvent< Type extends WebhookEventType, Data extends object, @@ -43,10 +85,10 @@ export type WebhookPayoutFailedEvent = BaseWebhookEvent< >; /** - * https://docs.abacatepay.com/pages/webhooks#payout-done + * https://docs.abacatepay.com/pages/webhooks#payout-completed */ -export type WebhookPayoutDoneEvent = BaseWebhookEvent< - WebhookEventType.PayoutDone, +export type WebhookPayoutCompletedEvent = BaseWebhookEvent< + WebhookEventType.PayoutCompleted, { /** * Transaction data. @@ -63,10 +105,10 @@ export type WebhookPayoutDoneEvent = BaseWebhookEvent< >; /** - * https://docs.abacatepay.com/pages/webhooks#billing-paid + * https://docs.abacatepay.com/pages/webhooks#checkout-completed */ -export type WebhookBillingPaidEvent = BaseWebhookEvent< - WebhookEventType.BillingPaid, +export type WebhookCheckoutCompletedEvent = BaseWebhookEvent< + WebhookEventType.CheckoutCompleted, { /** * Payment data. @@ -87,56 +129,95 @@ export type WebhookBillingPaidEvent = BaseWebhookEvent< */ method: PaymentMethod; }; - } & ( - | { - pixQrCode: { - /** - * Charge amount in cents (e.g. 4000 = R$40.00). - */ - amount: number; - /** - * Unique billing identifier. - */ - id: string; - /** - * Kind of the payment. - */ - kind: 'PIX'; - /** - * Billing status, can only be `PAID` here. - * - * @see {@link PaymentStatus.Paid} - */ - status: PaymentStatus.Paid; - }; - } - | { - billing: { - /** - * Charge amount in cents (e.g. 4000 = R$40.00). - */ - amount: number; - /** - * Unique billing identifier. - */ - id: string; - /** - * Bill ID in your system. - */ - externalId: string; - /** - * Status of the payment. Always `PaymentStatus.Paid`. - * - * @see {@link PaymentStatus.Paid} - */ - status: PaymentStatus.Paid; - /** - * URL where the user can complete the payment. - */ - url: string; - }; - } - ) + billing: { + /** + * Charge amount in cents (e.g. 4000 = R$40.00). + */ + amount: number; + /** + * Unique billing identifier. + */ + id: string; + /** + * Bill ID in your system. + */ + externalId: string; + /** + * Status of the payment. Always `PaymentStatus.Paid`. + * + * @see {@link PaymentStatus.Paid} + */ + status: PaymentStatus.Paid; + /** + * URL where the user can complete the payment. + */ + url: string; + }; + } +>; + +/** + * https://docs.abacatepay.com/pages/webhooks#transparent-completed + */ +export type WebhookTransparentCompletedEvent = BaseWebhookEvent< + WebhookEventType.TransparentCompleted, + { + /** + * Payment data. + */ + payment: { + /** + * Charge amount in cents (e.g. 4000 = R$40.00). + */ + amount: number; + /** + * The fee charged by AbacatePay. + */ + fee: 80; + /** + * Payment method. + * + * @see {@link PaymentMethod} + */ + method: PaymentMethod; + }; + pixQrCode: { + /** + * Charge amount in cents (e.g. 4000 = R$40.00). + */ + amount: number; + /** + * Unique billing identifier. + */ + id: string; + /** + * Kind of the payment. + */ + kind: 'PIX'; + /** + * Billing status, can only be `PAID` here. + * + * @see {@link PaymentStatus.Paid} + */ + status: PaymentStatus.Paid; + }; + } +>; + +/** + * Event types whose payload AbacatePay does not document in detail yet. + * + * @unstable The `data` shape is a best-effort placeholder until AbacatePay documents it. + */ +export type WebhookUndocumentedEvent = BaseWebhookEvent< + Exclude< + WebhookEventType, + | WebhookEventType.PayoutFailed + | WebhookEventType.PayoutCompleted + | WebhookEventType.CheckoutCompleted + | WebhookEventType.TransparentCompleted + >, + Record >; /** @@ -145,15 +226,30 @@ export type WebhookBillingPaidEvent = BaseWebhookEvent< * Any field that contains the tag "@unstable" means that the field is an assumption, it is uncertain (Since AbacatePay does not provide any information about). */ export type WebhookEvent = - | WebhookPayoutDoneEvent + | WebhookPayoutCompletedEvent | WebhookPayoutFailedEvent - | WebhookBillingPaidEvent; + | WebhookCheckoutCompletedEvent + | WebhookTransparentCompletedEvent + | WebhookUndocumentedEvent; /** - * https://docs.abacatepay.com/pages/webhooks + * https://docs.abacatepay.com/pages/webhooks/reference */ export enum WebhookEventType { + CheckoutCompleted = 'checkout.completed', + CheckoutRefunded = 'checkout.refunded', + CheckoutDisputed = 'checkout.disputed', + CheckoutLost = 'checkout.lost', + TransparentCompleted = 'transparent.completed', + TransparentRefunded = 'transparent.refunded', + TransparentDisputed = 'transparent.disputed', + TransparentLost = 'transparent.lost', + SubscriptionCompleted = 'subscription.completed', + SubscriptionCancelled = 'subscription.cancelled', + SubscriptionRenewed = 'subscription.renewed', + SubscriptionTrialStarted = 'subscription.trial_started', + PayoutCompleted = 'payout.completed', PayoutFailed = 'payout.failed', - PayoutDone = 'payout.done', - BillingPaid = 'billing.paid', + TransferCompleted = 'transfer.completed', + TransferFailed = 'transfer.failed', } diff --git a/packages/types/types/version.ts b/packages/types/types/version.ts index a72d8c6..45e5988 100644 --- a/packages/types/types/version.ts +++ b/packages/types/types/version.ts @@ -3,7 +3,7 @@ /** * Current version of [@abacatepay/types](https://www.npmjs.com/package/@abacatepay/types). */ -export const version = '2.0.3' as const; +export const version = '3.0.0' as const; /** * Current version of the AbacatePay API. */ diff --git a/packages/zod/package.json b/packages/zod/package.json index db21022..8d8623c 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,95 +1,95 @@ { - "name": "@abacatepay/zod", - "version": "1.0.1", - "description": "Official Zod schemas for the AbacatePay API, including requests, responses, and webhooks.", - "type": "module", - "sideEffects": false, - "license": "Apache-2.0", - "author": { - "name": "AbacatePay", - "email": "opensource@abacatepay.com", - "url": "https://abacatepay.com" - }, - "maintainers": [ - { - "name": "Almeida", - "url": "https://github.com/almeidazs" - }, - { - "name": "Albuquerque", - "url": "https://github.com/albuquerquesz" - } - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - }, - "./v1": { - "types": "./dist/v1/index.d.ts", - "import": "./dist/v1/index.js" - }, - "./v1/*": { - "types": "./dist/v1/*.d.ts", - "import": "./dist/v1/*.js" - }, - "./v2": { - "types": "./dist/v2/index.d.ts", - "import": "./dist/v2/index.js" - }, - "./v2/*": { - "types": "./dist/v2/*.d.ts", - "import": "./dist/v2/*.js" - }, - "./v*": null - }, - "typesVersions": { - "*": { - "v1": [ - "dist/v1/index.d.ts" - ], - "v2": [ - "dist/v2/index.d.ts" - ] - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist" - ], - "scripts": { - "test": "bun test", - "build": "rm -rf dist && bun run gen:version && tsc", - "types": "bunx tsc --emitDeclarationOnly", - "prepublishOnly": "bun run build", - "gen:version": "bun run scripts/version.ts" - }, - "engines": { - "node": ">=18", - "bun": ">=1.0.0" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/AbacatePay/ecosystem.git" - }, - "keywords": [ - "abacatepay", - "payments", - "zod", - "api", - "typescript" - ], - "publishConfig": { - "access": "public" - }, - "devDependencies": { - "@biomejs/biome": "^2.3.11", - "typescript": "^5.0.0" - }, - "peerDependencies": { - "zod": "^4.3.6", - "typescript": "^5" - } + "name": "@abacatepay/zod", + "version": "2.0.0", + "description": "Official Zod schemas for the AbacatePay API, including requests, responses, and webhooks.", + "type": "module", + "sideEffects": false, + "license": "Apache-2.0", + "author": { + "name": "AbacatePay", + "email": "opensource@abacatepay.com", + "url": "https://abacatepay.com" + }, + "maintainers": [ + { + "name": "Almeida", + "url": "https://github.com/almeidazs" + }, + { + "name": "Albuquerque", + "url": "https://github.com/albuquerquesz" + } + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./v1": { + "types": "./dist/v1/index.d.ts", + "import": "./dist/v1/index.js" + }, + "./v1/*": { + "types": "./dist/v1/*.d.ts", + "import": "./dist/v1/*.js" + }, + "./v2": { + "types": "./dist/v2/index.d.ts", + "import": "./dist/v2/index.js" + }, + "./v2/*": { + "types": "./dist/v2/*.d.ts", + "import": "./dist/v2/*.js" + }, + "./v*": null + }, + "typesVersions": { + "*": { + "v1": [ + "dist/v1/index.d.ts" + ], + "v2": [ + "dist/v2/index.d.ts" + ] + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "test": "bun test", + "build": "rm -rf dist && bun run gen:version && tsc", + "types": "bunx tsc --emitDeclarationOnly", + "prepublishOnly": "bun run build", + "gen:version": "bun run scripts/version.ts" + }, + "engines": { + "node": ">=18", + "bun": ">=1.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/AbacatePay/ecosystem.git" + }, + "keywords": [ + "abacatepay", + "payments", + "zod", + "api", + "typescript" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.11", + "typescript": "^5.0.0" + }, + "peerDependencies": { + "zod": "^4.3.6", + "typescript": "^5" + } } diff --git a/packages/zod/src/v2/index.ts b/packages/zod/src/v2/index.ts index 82a3be9..c9f20f5 100644 --- a/packages/zod/src/v2/index.ts +++ b/packages/zod/src/v2/index.ts @@ -1,6 +1,7 @@ export * from './resources/checkout'; export * from './resources/coupon'; export * from './resources/customer'; +export * from './resources/paymentLink'; export * from './resources/payout'; export * from './resources/pix'; export * from './resources/product'; diff --git a/packages/zod/src/v2/resources/checkout.ts b/packages/zod/src/v2/resources/checkout.ts index 605004d..83278aa 100644 --- a/packages/zod/src/v2/resources/checkout.ts +++ b/packages/zod/src/v2/resources/checkout.ts @@ -18,7 +18,7 @@ export type PaymentStatus = z.infer; * https://docs.abacatepay.com/pages/payment/create#body-methods */ export const PaymentMethod = StringEnum( - ['PIX', 'CARD'], + ['PIX', 'CARD', 'BOLETO'], 'Payment method.', ).meta({ example: 'PIX' }); @@ -27,6 +27,19 @@ export const PaymentMethod = StringEnum( */ export type PaymentMethod = z.infer; +/** + * https://docs.abacatepay.com/pages/payment/create#body-frequency + */ +export const PaymentFrequency = StringEnum( + ['ONE_TIME', 'MULTIPLE_PAYMENTS', 'SUBSCRIPTION'], + 'Billing frequency.', +).meta({ example: 'ONE_TIME' }); + +/** + * https://docs.abacatepay.com/pages/payment/create#body-frequency + */ +export type PaymentFrequency = z.infer; + /** * https://docs.abacatepay.com/pages/checkouts/reference#estrutura */ @@ -116,6 +129,40 @@ export const APICheckout = z.object({ .date() .describe('Charge last updated date and time.') .meta({ example: new Date() }), + frequency: PaymentFrequency.describe( + 'Billing frequency. Defaults to `ONE_TIME`.', + ).optional(), + upSellProductId: z + .union([z.null(), z.string()]) + .meta({ example: null }) + .describe('ID of an additional product offered as an upsell.') + .optional(), + interest: z + .union([ + z.null(), + z.object({ + value: z + .int() + .meta({ example: 100 }) + .describe('Monthly interest rate, in hundredths of a percent.'), + }), + ]) + .meta({ example: null }) + .describe('Late interest configuration (Applies to BOLETO).') + .optional(), + fine: z + .union([ + z.null(), + z.object({ + value: z.int().meta({ example: 200 }).describe('Fine value.'), + type: StringEnum(['PERCENTAGE', 'FIXED'], 'Type of fine applied.').meta( + { example: 'PERCENTAGE' }, + ), + }), + ]) + .meta({ example: null }) + .describe('Late fine configuration (Applies to BOLETO).') + .optional(), }); /** diff --git a/packages/zod/src/v2/resources/paymentLink.ts b/packages/zod/src/v2/resources/paymentLink.ts new file mode 100644 index 0000000..1350a0d --- /dev/null +++ b/packages/zod/src/v2/resources/paymentLink.ts @@ -0,0 +1,55 @@ +import { z } from 'zod'; +import { PaymentStatus } from './checkout'; + +/** + * A reusable payment link — unlike a Checkout, the same link can be paid by + * multiple customers instead of being tied to a single transaction. + * + * https://docs.abacatepay.com/pages/payment-links/reference + */ +export const APIPaymentLink = z.object({ + id: z + .string() + .describe('Unique payment link identifier.') + .meta({ example: 'bill_123' }), + url: z + .url() + .describe('Shareable checkout URL.') + .meta({ example: 'https://myshop.com/premium' }), + amount: z.int().describe('Total amount in cents.').meta({ example: 4000 }), + paidAmount: z + .union([z.null(), z.int()]) + .meta({ example: null }) + .describe('Amount paid in cents. `null` if it has not yet been paid.'), + status: PaymentStatus, + frequency: z + .literal('MULTIPLE_PAYMENTS') + .meta({ example: 'MULTIPLE_PAYMENTS' }) + .describe('Always `MULTIPLE_PAYMENTS` for payment links.'), + items: z + .array( + z.object({ + id: z.string().describe('Product ID.'), + quantity: z.int().min(1).describe('Item quantity.'), + }), + ) + .meta({ example: [{ id: 'prod_123', quantity: 1 }] }) + .describe('List of items included in the payment link.'), + externalId: z + .union([z.null(), z.string()]) + .meta({ example: null }) + .describe('Reference ID in your system.'), + createdAt: z.coerce + .date() + .describe('Payment link creation date and time.') + .meta({ example: new Date() }), + updatedAt: z.coerce + .date() + .describe('Payment link last updated date and time.') + .meta({ example: new Date() }), +}); + +/** + * https://docs.abacatepay.com/pages/payment-links/reference + */ +export type APIPaymentLink = z.infer; diff --git a/packages/zod/src/v2/resources/pix.ts b/packages/zod/src/v2/resources/pix.ts index 93ed4cf..b0c42e3 100644 --- a/packages/zod/src/v2/resources/pix.ts +++ b/packages/zod/src/v2/resources/pix.ts @@ -1,4 +1,5 @@ import { z } from 'zod'; +import { StringEnum } from '../../utils'; import { PaymentStatus } from './checkout'; /** @@ -32,6 +33,11 @@ export const APIQRCodePIX = z.object({ .int() .meta({ example: 80 }) .describe('Platform fee in cents. Example: 80 means R$0.80.'), + receiptUrl: z + .union([z.null(), z.url()]) + .meta({ example: null }) + .describe('Payment receipt URL.') + .optional(), createdAt: z.coerce .date() .describe('QRCode PIX creation date and time.') @@ -44,9 +50,149 @@ export const APIQRCodePIX = z.object({ .date() .describe('QRCode expiration date and time.') .meta({ example: new Date() }), + metadata: z + .record(z.string(), z.any()) + .meta({ example: {} }) + .describe('Additional metadata attached to the charge.') + .optional(), }); /** * https://docs.abacatepay.com/pages/transparents/reference */ export type APIQRCodePIX = z.infer; + +/** + * https://docs.abacatepay.com/pages/transparents/boleto + */ +export const APIBoleto = z.object({ + id: z + .string() + .describe('Unique Boleto identifier.') + .meta({ example: 'bill_char_123' }), + amount: z.int().describe('Charge amount in cents.').meta({ example: 4000 }), + status: PaymentStatus, + devMode: z + .boolean() + .meta({ example: false }) + .describe( + 'Indicates whether the charge is in a testing (true) or production (false) environment.', + ), + barCode: z + .string() + .describe('Boleto barcode.') + .meta({ example: '00190.00009 03384.318207' }), + url: z + .url() + .describe('URL to view/print the Boleto.') + .meta({ example: 'https://myshop.com/boleto/bill_char_123' }), + pix: z + .object({ + brCode: z + .string() + .describe('PIX code (copy-and-paste) for payment.') + .meta({ example: '00020101021226950014br.gov.bcb.pix' }), + brCodeBase64: z + .base64() + .meta({ example: 'data:image/png;base64,iVBORw0KGgoAAA' }) + .describe( + 'PIX code in Base64 format (Useful for displaying in images).', + ), + }) + .describe('PIX alternative for paying the same Boleto.'), + platformFee: z.int().meta({ example: 80 }).describe('Platform fee in cents.'), + receiptUrl: z + .union([z.null(), z.url()]) + .meta({ example: null }) + .describe('Payment receipt URL.'), + createdAt: z.coerce + .date() + .describe('Boleto creation date and time.') + .meta({ example: new Date() }), + updatedAt: z.coerce + .date() + .describe('Boleto last updated date and time.') + .meta({ example: new Date() }), + expiresAt: z.coerce + .date() + .describe('Boleto expiration date and time.') + .meta({ example: new Date() }), + metadata: z + .record(z.string(), z.any()) + .meta({ example: {} }) + .describe('Additional metadata attached to the charge.') + .optional(), +}); + +/** + * https://docs.abacatepay.com/pages/transparents/boleto + */ +export type APIBoleto = z.infer; + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export const PixTransferStatus = StringEnum( + ['PENDING', 'EXPIRED', 'CANCELLED', 'COMPLETE', 'REFUNDED'], + 'Transfer status.', +).meta({ example: 'PENDING' }); + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export type PixTransferStatus = z.infer; + +/** + * https://docs.abacatepay.com/pages/pix/create + */ +export const PixKeyType = StringEnum( + ['CPF', 'CNPJ', 'PHONE', 'EMAIL', 'RANDOM', 'BR_CODE'], + 'Type of the destination PIX key.', +).meta({ example: 'EMAIL' }); + +/** + * https://docs.abacatepay.com/pages/pix/create + */ +export type PixKeyType = z.infer; + +/** + * Outbound PIX transfer to a third-party PIX key (Sending money out). + * + * https://docs.abacatepay.com/pages/pix/reference + */ +export const APIPixTransfer = z.object({ + id: z + .string() + .describe('Unique transfer identifier.') + .meta({ example: 'pix_transfer_123' }), + status: PixTransferStatus, + devMode: z + .boolean() + .meta({ example: false }) + .describe( + 'Indicates whether the transfer is in a testing (true) or production (false) environment.', + ), + receiptUrl: z + .union([z.null(), z.url()]) + .meta({ example: null }) + .describe('Proof-of-transfer URL, available once completed.'), + amount: z.int().meta({ example: 4000 }).describe('Transfer amount in cents.'), + platformFee: z.int().meta({ example: 80 }).describe('Platform fee in cents.'), + externalId: z + .union([z.null(), z.string()]) + .meta({ example: null }) + .describe('Unique transfer identifier in your system.'), + createdAt: z.coerce + .date() + .describe('Transfer creation date and time.') + .meta({ example: new Date() }), + updatedAt: z.coerce + .date() + .describe('Transfer last updated date and time.') + .meta({ example: new Date() }), +}); + +/** + * https://docs.abacatepay.com/pages/pix/reference + */ +export type APIPixTransfer = z.infer; diff --git a/packages/zod/src/v2/resources/subscription.ts b/packages/zod/src/v2/resources/subscription.ts index f26564e..3109f90 100644 --- a/packages/zod/src/v2/resources/subscription.ts +++ b/packages/zod/src/v2/resources/subscription.ts @@ -85,14 +85,14 @@ export const APISubscription = z.object({ ['MONTHLY', 'YEARLY', 'WEEKLY', 'DAILY'], 'Subscription billing cycle.', ).meta({ example: 'MONTHLY' }), + dayOfProcessing: z + .int() + .min(1) + .max(31) + .meta({ example: 3 }) + .describe('Day of the month the charge will be processed (1-31).'), }) .describe('Billing frequency configuration.'), - dayOfProcessing: z - .int() - .min(1) - .max(31) - .meta({ example: 3 }) - .describe('Day of the month the charge will be processed (1-31).'), customerId: z .string() .meta({ example: 'cust_123' }) @@ -127,3 +127,92 @@ export const APISubscription = z.object({ * https://docs.abacatepay.com/pages/subscriptions/reference#estrutura */ export type APISubscription = z.infer; + +/** + * Result of `POST /subscriptions/change-plan`. + * + * https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export const APISubscriptionPlanChange = z.object({ + id: z + .string() + .describe('Unique identifier of this plan-change request.') + .meta({ example: 'sub_change_123' }), + subscriptionId: z + .string() + .describe('ID of the subscription being changed.') + .meta({ example: 'subs_abc123xyz' }), + status: StringEnum( + ['PENDING', 'APPLIED', 'CANCELLED'], + 'Status of the plan-change request.', + ).meta({ example: 'PENDING' }), + productId: z + .string() + .describe('ID of the new product.') + .meta({ example: 'prod_123' }), + quantity: z + .int() + .describe('New quantity for the product.') + .meta({ example: 1 }), + newAmount: z + .int() + .describe( + 'Amount that will be charged once the change is applied, in cents.', + ) + .meta({ example: 4000 }), + requestedAt: z.coerce + .date() + .describe('When the change was requested.') + .meta({ example: new Date() }), +}); + +/** + * https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type APISubscriptionPlanChange = z.infer< + typeof APISubscriptionPlanChange +>; + +/** + * Result of `POST /subscriptions/record-usage`. + * + * https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export const APISubscriptionUsageRecord = z.object({ + id: z + .string() + .describe('Unique identifier of this usage record.') + .meta({ example: 'usage_123' }), + subscriptionId: z + .string() + .describe('ID of the subscription the usage was recorded against.') + .meta({ example: 'subs_abc123xyz' }), + productId: z + .string() + .describe('ID of the pay-as-you-go product the usage applies to.') + .meta({ example: 'prod_123' }), + units: z.int().describe('Number of units recorded.').meta({ example: 5 }), + unitPrice: z + .int() + .describe('Price per unit, in cents.') + .meta({ example: 100 }), + action: StringEnum( + ['add', 'subtract'], + 'Whether the units were added to or subtracted from the current cycle.', + ).meta({ example: 'add' }), + installmentNumber: z + .int() + .describe('The pending installment this usage record was attached to.') + .meta({ example: 1 }), + recordedAt: z.coerce + .date() + .describe('When the usage was recorded.') + .meta({ example: new Date() }), +}); + +/** + * https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export type APISubscriptionUsageRecord = z.infer< + typeof APISubscriptionUsageRecord +>; diff --git a/packages/zod/src/v2/rest.ts b/packages/zod/src/v2/rest.ts index 7ca85ca..7a9cdd9 100644 --- a/packages/zod/src/v2/rest.ts +++ b/packages/zod/src/v2/rest.ts @@ -1,17 +1,28 @@ import { type _ZodType, z } from 'zod'; +import { StringEnum } from '../utils'; import { + APIBoleto, APICheckout, APICoupon, APICustomer, + APIPaymentLink, APIPayout, + APIPixTransfer, APIProduct, APIQRCodePIX, APIStore, APISubscription, + APISubscriptionPlanChange, + APISubscriptionUsageRecord, CouponDiscountKind, PaymentMethod, PaymentStatus, + PixKeyType, } from '.'; +// Imported directly (not via the barrel) so module init order doesn't matter: +// `rest.ts` is exported before `webhook.ts` in `./index`, and importing +// these from '.' would hit a TDZ error at module-eval time. +import { APIWebhook, WebhookEventType } from './webhook'; /** * Any response returned by the AbacatePay API @@ -20,11 +31,13 @@ export const APIResponse = (schema: Schema) => z.discriminatedUnion('success', [ z.object({ data: schema, + error: z.null(), success: z .literal([true]) .describe('Whether the request was successfull or not.'), }), z.object({ + data: z.null(), error: z.string().describe('Error message returned from the API.'), success: z .literal([false]) @@ -49,6 +62,7 @@ export const APIResponseWithPagination = ( z.discriminatedUnion('success', [ z.object({ data: schema, + error: z.null(), success: z .literal([true]) .describe('Whether the request was successfull or not.'), @@ -60,6 +74,7 @@ export const APIResponseWithPagination = ( }), }), z.object({ + data: z.null(), error: z.string().describe('Error message returned from the API.'), success: z .literal([false]) @@ -84,6 +99,7 @@ export const APIResponseWithCursorBasedPagination = ( z.discriminatedUnion('success', [ z.object({ data: schema, + error: z.null(), success: z .literal([true]) .describe('Whether the request was successfull or not.'), @@ -101,6 +117,7 @@ export const APIResponseWithCursorBasedPagination = ( }), }), z.object({ + data: z.null(), error: z.string().describe('Error message returned from the API.'), success: z .literal([false]) @@ -114,13 +131,60 @@ export const APIResponseWithCursorBasedPagination = ( export type APIResponseWithCursorBasedPagination = z.infer>>; +/** + * https://api.abacatepay.com/v2/customers/create + * + * @reference https://docs.abacatepay.com/pages/client/create + */ +export const RESTPostCreateCustomerBody = APICustomer.pick({ + email: true, +}).and( + APICustomer.pick({ + name: true, + taxId: true, + zipCode: true, + cellphone: true, + metadata: true, + }).partial(), +); + +/** + * https://api.abacatepay.com/v2/customers/create + * + * @reference https://docs.abacatepay.com/pages/client/create + */ +export type RESTPostCreateCustomerBody = z.infer< + typeof RESTPostCreateCustomerBody +>; + +/** + * https://api.abacatepay.com/v2/customers/create + * + * @reference https://docs.abacatepay.com/pages/client/create + */ +export const RESTPostCreateCustomerData = APIResponse(APICustomer); + +/** + * https://api.abacatepay.com/v2/customers/create + * + * @reference https://docs.abacatepay.com/pages/client/create + */ +export type RESTPostCreateCustomerData = z.infer< + typeof RESTPostCreateCustomerData +>; + /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkout/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export const RESTPostCreateNewCheckoutBody = z.object({ - methods: PaymentMethod, + methods: z + .array(PaymentMethod) + .describe( + 'Payment methods that will be accepted (Defaults to `[PIX, CARD]`).', + ) + .optional(), returnUrl: z .url() .describe( @@ -159,12 +223,35 @@ export const RESTPostCreateNewCheckoutBody = z.object({ .describe('Optional billing metadata.') .optional(), items: APICheckout.shape.items, + frequency: StringEnum( + ['ONE_TIME', 'MULTIPLE_PAYMENTS', 'SUBSCRIPTION'], + 'Billing frequency. Defaults to `ONE_TIME`.', + ).optional(), + upSellProductId: z + .string() + .describe('ID of an additional product offered as an upsell.') + .optional(), + interest: z + .object({ + value: z + .int() + .describe('Monthly interest rate, in hundredths of a percent.'), + }) + .describe('Late interest configuration (Applies to BOLETO).') + .optional(), + fine: z + .object({ + value: z.int().describe('Fine value.'), + type: StringEnum(['PERCENTAGE', 'FIXED'], 'Type of fine applied.'), + }) + .describe('Late fine configuration (Applies to BOLETO).') + .optional(), }); /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkout/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export type RESTPostCreateNewCheckoutBody = z.infer< typeof RESTPostCreateNewCheckoutBody @@ -173,51 +260,121 @@ export type RESTPostCreateNewCheckoutBody = z.infer< /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkouts/create + * @reference https://docs.abacatepay.com/pages/payment/create */ -export const RESTPostCreateNewCheckoutData = APICheckout; +export const RESTPostCreateNewCheckoutData = APIResponse(APICheckout); /** * https://api.abacatepay.com/v2/checkouts/create * - * @reference https://docs.abacatepay.com/pages/checkouts/create + * @reference https://docs.abacatepay.com/pages/payment/create */ export type RESTPostCreateNewCheckoutData = z.infer< typeof RESTPostCreateNewCheckoutData >; +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export const RESTPostRefundCheckoutBody = z.object({ + id: z + .string() + .describe( + 'Public ID of the resource to refund (prefixes: `bill_`, `char_`, `pix_char_`, `card_`).', + ), + reason: z + .string() + .max(500) + .describe('Refund reason, shown in the transaction history.') + .optional(), +}); + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export type RESTPostRefundCheckoutBody = z.infer< + typeof RESTPostRefundCheckoutBody +>; + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export const RESTPostRefundCheckoutData = APIResponse( + z.object({ + refundPublicId: z + .string() + .describe('Public ID of the refund transaction that was created.'), + }), +); + +/** + * https://api.abacatepay.com/v2/checkouts/refund + * + * @reference https://docs.abacatepay.com/pages/payment/refund + */ +export type RESTPostRefundCheckoutData = z.infer< + typeof RESTPostRefundCheckoutData +>; + +/** + * https://api.abacatepay.com/v2/checkouts/list + * + * @reference https://docs.abacatepay.com/pages/payment/list + */ +export const RESTGetListCheckoutsQueryParams = z.object({ + page: z.int().min(1).default(1).describe('Number of the page.').optional(), + limit: z.int().min(1).describe('Number of items per page.').optional(), +}); + /** * https://api.abacatepay.com/v2/checkouts/list * - * @reference https://docs.abacatepay.com/pages/checkouts/list + * @reference https://docs.abacatepay.com/pages/payment/list */ -export const RESTGetListCheckoutsData = z.array(APICheckout); +export type RESTGetListCheckoutsQueryParams = z.infer< + typeof RESTGetListCheckoutsQueryParams +>; /** * https://api.abacatepay.com/v2/checkouts/list * - * @reference https://docs.abacatepay.com/pages/checkouts/list + * @reference https://docs.abacatepay.com/pages/payment/list + */ +export const RESTGetListCheckoutsData = APIResponseWithPagination( + z.array(APICheckout), +); + +/** + * https://api.abacatepay.com/v2/checkouts/list + * + * @reference https://docs.abacatepay.com/pages/payment/list */ export type RESTGetListCheckoutsData = z.infer; /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ -export const RESTGetCheckoutData = APICheckout; +export const RESTGetCheckoutData = APIResponse(APICheckout); /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export type RESTGetCheckoutData = z.infer; /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export const RESTGetCheckoutQueryParams = z.object({ id: z.string().describe('Unique billing identifier.'), @@ -226,12 +383,160 @@ export const RESTGetCheckoutQueryParams = z.object({ /** * https://api.abacatepay.com/v2/checkouts/get * - * @reference https://docs.abacatepay.com/pages/checkouts/get + * @reference https://docs.abacatepay.com/pages/payment/one */ export type RESTGetCheckoutQueryParams = z.infer< typeof RESTGetCheckoutQueryParams >; +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export const RESTPostCreatePaymentLinkBody = RESTPostCreateNewCheckoutBody.omit( + { customerId: true, customer: true, frequency: true, upSellProductId: true }, +); + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export type RESTPostCreatePaymentLinkBody = z.infer< + typeof RESTPostCreatePaymentLinkBody +>; + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export const RESTPostCreatePaymentLinkData = APIResponse(APIPaymentLink); + +/** + * https://api.abacatepay.com/v2/payment-links/create + * + * @reference https://docs.abacatepay.com/pages/payment-links/create + */ +export type RESTPostCreatePaymentLinkData = z.infer< + typeof RESTPostCreatePaymentLinkData +>; + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export const RESTGetListPaymentLinksQueryParams = z.object({ + page: z.int().min(1).default(1).describe('Number of the page.').optional(), + limit: z.int().min(1).describe('Number of items per page.').optional(), +}); + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export type RESTGetListPaymentLinksQueryParams = z.infer< + typeof RESTGetListPaymentLinksQueryParams +>; + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export const RESTGetListPaymentLinksData = APIResponseWithPagination( + z.array(APIPaymentLink), +); + +/** + * https://api.abacatepay.com/v2/payment-links/list + * + * @reference https://docs.abacatepay.com/pages/payment-links/list + */ +export type RESTGetListPaymentLinksData = z.infer< + typeof RESTGetListPaymentLinksData +>; + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export const RESTGetPaymentLinkQueryParams = z.object({ + id: z.string().describe('Unique payment link identifier.'), +}); + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export type RESTGetPaymentLinkQueryParams = z.infer< + typeof RESTGetPaymentLinkQueryParams +>; + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export const RESTGetPaymentLinkData = APIResponse(APIPaymentLink); + +/** + * https://api.abacatepay.com/v2/payment-links/one + * + * @reference https://docs.abacatepay.com/pages/payment-links/one + */ +export type RESTGetPaymentLinkData = z.infer; + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export const RESTPostRefundPaymentLinkBody = z.object({ + id: z.string().describe('Public ID of the resource to refund.'), + reason: z + .string() + .max(500) + .describe('Refund reason, shown in the transaction history.') + .optional(), +}); + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export type RESTPostRefundPaymentLinkBody = z.infer< + typeof RESTPostRefundPaymentLinkBody +>; + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export const RESTPostRefundPaymentLinkData = APIResponse( + z.object({ + refundPublicId: z + .string() + .describe('Public ID of the refund transaction that was created.'), + }), +); + +/** + * https://api.abacatepay.com/v2/payment-links/refund + * + * @reference https://docs.abacatepay.com/pages/payment-links/refund + */ +export type RESTPostRefundPaymentLinkData = z.infer< + typeof RESTPostRefundPaymentLinkData +>; + /** * https://api.abacatepay.com/v2/coupons/create * @@ -262,16 +567,16 @@ export const RESTPostCreateCouponBody = z.object({ export type RESTPostCreateCouponBody = z.infer; /** - * https://api.abacatepay.com/v2/coupon/create + * https://api.abacatepay.com/v2/coupons/create * - * @reference https://docs.abacatepay.com/pages/coupon/create + * @reference https://docs.abacatepay.com/pages/coupons/create */ -export const RESTPostCreateCouponData = APICoupon; +export const RESTPostCreateCouponData = APIResponse(APICoupon); /** - * https://api.abacatepay.com/v2/coupon/create + * https://api.abacatepay.com/v2/coupons/create * - * @reference https://docs.abacatepay.com/pages/coupon/create + * @reference https://docs.abacatepay.com/pages/coupons/create */ export type RESTPostCreateCouponData = z.infer; @@ -280,7 +585,9 @@ export type RESTPostCreateCouponData = z.infer; * * @reference https://docs.abacatepay.com/pages/coupons/list */ -export const RESTGetListCouponsData = z.array(APICoupon); +export const RESTGetListCouponsData = APIResponseWithPagination( + z.array(APICoupon), +); /** * https://api.abacatepay.com/v2/coupons/list @@ -329,7 +636,7 @@ export type RESTGetCouponQueryParams = z.infer; * * @reference https://docs.abacatepay.com/pages/coupons/get */ -export const RESTGetCouponData = APICoupon; +export const RESTGetCouponData = APIResponse(APICoupon); /** * https://api.abacatepay.com/v2/coupons/get @@ -359,7 +666,7 @@ export type RESTDeleteCouponBody = z.infer; * * @reference https://docs.abacatepay.com/pages/coupons/delete */ -export const RESTDeleteCouponData = APICoupon; +export const RESTDeleteCouponData = APIResponse(APICoupon); /** * https://api.abacatepay.com/v2/coupons/delete @@ -373,7 +680,7 @@ export type RESTDeleteCouponData = z.infer; * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export const RESTPatchToggleCouponStatusBody = z.object({ +export const RESTPostToggleCouponStatusBody = z.object({ id: z.string().describe('The ID of the coupon.'), }); @@ -382,8 +689,8 @@ export const RESTPatchToggleCouponStatusBody = z.object({ * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export type RESTPatchToggleCouponStatusBody = z.infer< - typeof RESTPatchToggleCouponStatusBody +export type RESTPostToggleCouponStatusBody = z.infer< + typeof RESTPostToggleCouponStatusBody >; /** @@ -391,15 +698,15 @@ export type RESTPatchToggleCouponStatusBody = z.infer< * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export const RESTPatchToggleCouponStatusData = APICoupon; +export const RESTPostToggleCouponStatusData = APIResponse(APICoupon); /** * https://api.abacatepay.com/v2/coupons/toggle * * @reference https://docs.abacatepay.com/pages/coupons/toggle */ -export type RESTPatchToggleCouponStatusData = z.infer< - typeof RESTPatchToggleCouponStatusData +export type RESTPostToggleCouponStatusData = z.infer< + typeof RESTPostToggleCouponStatusData >; /** @@ -425,112 +732,413 @@ export type RESTPostCreateNewPayoutBody = z.infer< >; /** - * https://api.abacatepay.com/v2/payouts/create + * https://api.abacatepay.com/v2/payouts/create + * + * @reference https://docs.abacatepay.com/pages/payouts/create + */ +export const RESTPostCreateNewPayoutData = APIResponse(APIPayout); + +/** + * https://api.abacatepay.com/v2/payouts/create + * + * @reference https://docs.abacatepay.com/pages/payouts/create + */ +export type RESTPostCreateNewPayoutData = z.infer< + typeof RESTPostCreateNewPayoutData +>; + +/** + * https://api.abacatepay.com/v2/payouts/get + * + * @reference https://docs.abacatepay.com/pages/payouts/get + */ +export const RESTGetSearchPayoutQueryParams = z.object({ + externalId: z.string().describe('Unique payout identifier in your system.'), +}); + +/** + * https://api.abacatepay.com/v2/payouts/get + * + * @reference https://docs.abacatepay.com/pages/payouts/get + */ +export type RESTGetSearchPayoutQueryParams = z.infer< + typeof RESTGetSearchPayoutQueryParams +>; + +/** + * https://api.abacatepay.com/v2/payouts/get + * + * @reference https://docs.abacatepay.com/pages/payouts/get + */ +export const RESTGetSearchPayoutData = APIResponse(APIPayout); + +/** + * https://api.abacatepay.com/v2/payouts/get + * + * @reference https://docs.abacatepay.com/pages/payouts/get + */ +export type RESTGetSearchPayoutData = z.infer; + +/** + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export const RESTGetListPayoutsQueryParams = z.object({ + page: z.int().min(1).default(1).describe('Page number.').optional(), + limit: z.int().min(1).describe('Number of items per page.').optional(), +}); + +/** + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export type RESTGetListPayoutsQueryParams = z.infer< + typeof RESTGetListPayoutsQueryParams +>; + +/** + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export const RESTGetListPayoutsData = APIResponseWithPagination( + z.array(APIPayout), +); + +/** + * https://api.abacatepay.com/v2/payouts/list + * + * @reference https://docs.abacatepay.com/pages/payouts/list + */ +export type RESTGetListPayoutsData = z.infer; + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export const RESTPostSendPixTransferBody = z.object({ + amount: z.int().min(1).describe('Transfer amount in cents (Min 100).'), + externalId: z + .string() + .describe('Unique identifier of the transfer in your system.'), + description: z.string().describe('Optional transfer description.').optional(), + pix: z.object({ + key: z.string().describe('The PIX key itself.'), + type: PixKeyType, + }), +}); + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export type RESTPostSendPixTransferBody = z.infer< + typeof RESTPostSendPixTransferBody +>; + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export const RESTPostSendPixTransferData = APIResponse(APIPixTransfer); + +/** + * https://api.abacatepay.com/v2/pix/send + * + * @reference https://docs.abacatepay.com/pages/pix/create + */ +export type RESTPostSendPixTransferData = z.infer< + typeof RESTPostSendPixTransferData +>; + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export const RESTGetPixTransferQueryParams = z.object({ + id: z + .string() + .describe( + 'Unique transfer identifier in AbacatePay. At least one of `id`/`externalId` is required.', + ) + .optional(), + externalId: z + .string() + .describe( + 'Unique transfer identifier in your system. At least one of `id`/`externalId` is required.', + ) + .optional(), +}); + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export type RESTGetPixTransferQueryParams = z.infer< + typeof RESTGetPixTransferQueryParams +>; + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export const RESTGetPixTransferData = APIResponse(APIPixTransfer); + +/** + * https://api.abacatepay.com/v2/pix/get + * + * @reference https://docs.abacatepay.com/pages/pix/get + */ +export type RESTGetPixTransferData = z.infer; + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export const RESTGetListPixTransfersQueryParams = z.object({ + limit: z + .int() + .min(1) + .max(100) + .describe('Number of items per page (1-100).') + .optional(), + after: z.string().describe('Cursor for the next page.').optional(), + before: z.string().describe('Cursor for the previous page.').optional(), + id: z.string().describe('Filter by AbacatePay transaction ID.').optional(), + externalId: z.string().describe('Filter by external system ID.').optional(), + status: z.string().describe('Filter by transaction status.').optional(), +}); + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export type RESTGetListPixTransfersQueryParams = z.infer< + typeof RESTGetListPixTransfersQueryParams +>; + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export const RESTGetListPixTransfersData = APIResponseWithCursorBasedPagination( + z.array(APIPixTransfer), +); + +/** + * https://api.abacatepay.com/v2/pix/list + * + * @reference https://docs.abacatepay.com/pages/pix/list + */ +export type RESTGetListPixTransfersData = z.infer< + typeof RESTGetListPixTransfersData +>; + +/** + * Inner `data` payload sent to `POST /transparents/create` when `method` is `PIX`. + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export const RESTPostCreateQRCodePixBody = RESTPostCreateNewCheckoutBody.pick({ + customer: true, + metadata: true, +}).extend({ + amount: z.int().describe('Charge amount in cents.'), + expiresIn: z.int().describe('Billing expiration time in seconds.').optional(), + description: z + .string() + .describe('Message that will appear when paying the PIX.') + .optional(), +}); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export type RESTPostCreateQRCodePixBody = z.infer< + typeof RESTPostCreateQRCodePixBody +>; + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export const RESTPostCreateQRCodePixData = APIResponse(APIQRCodePIX); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/create + */ +export type RESTPostCreateQRCodePixData = z.infer< + typeof RESTPostCreateQRCodePixData +>; + +/** + * Inner `data` payload sent to `POST /transparents/create` when `method` is `BOLETO`. + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export const RESTPostCreateBoletoBody = z.object({ + amount: z.int().describe('Charge amount in cents.'), + description: z + .string() + .describe('Message that will appear on the Boleto.') + .optional(), + customer: APICustomer.pick({ name: true, taxId: true }) + .and(APICustomer.pick({ email: true, cellphone: true }).partial()) + .describe('Customer data. `name` and `taxId` are mandatory for Boleto.'), + metadata: z + .record(z.string(), z.any()) + .describe('Optional charge metadata.') + .optional(), +}); + +/** + * https://api.abacatepay.com/v2/transparents/create + * + * @reference https://docs.abacatepay.com/pages/transparents/boleto + */ +export type RESTPostCreateBoletoBody = z.infer; + +/** + * https://api.abacatepay.com/v2/transparents/create * - * @reference https://docs.abacatepay.com/pages/payouts/create + * @reference https://docs.abacatepay.com/pages/transparents/boleto */ -export const RESTPostCreateNewPayoutData = APIPayout; +export const RESTPostCreateBoletoData = APIResponse(APIBoleto); /** - * https://api.abacatepay.com/v2/payouts/create + * https://api.abacatepay.com/v2/transparents/create * - * @reference https://docs.abacatepay.com/pages/payouts/create + * @reference https://docs.abacatepay.com/pages/transparents/boleto */ -export type RESTPostCreateNewPayoutData = z.infer< - typeof RESTPostCreateNewPayoutData ->; +export type RESTPostCreateBoletoData = z.infer; /** - * https://api.abacatepay.com/v2/payouts/get + * Wire-level request body for `POST /transparents/create` — the SDK builds this + * from {@link RESTPostCreateQRCodePixBody} / {@link RESTPostCreateBoletoBody}, callers + * never construct it directly. * - * @reference https://docs.abacatepay.com/pages/payouts/get + * @reference https://docs.abacatepay.com/pages/transparents/reference */ -export const RESTGetSearchPayoutQueryParams = z.object({ - externalId: z.string().describe('Unique payout identifier in your system.'), -}); +export const RESTPostCreateTransparentBody = z.union([ + z.object({ method: z.literal('PIX'), data: RESTPostCreateQRCodePixBody }), + z.object({ method: z.literal('BOLETO'), data: RESTPostCreateBoletoBody }), +]); /** - * https://api.abacatepay.com/v2/payouts/get - * - * @reference https://docs.abacatepay.com/pages/payouts/get + * @reference https://docs.abacatepay.com/pages/transparents/reference */ -export type RESTGetSearchPayoutQueryParams = z.infer< - typeof RESTGetSearchPayoutQueryParams +export type RESTPostCreateTransparentBody = z.infer< + typeof RESTPostCreateTransparentBody >; /** - * https://api.abacatepay.com/v2/payouts/list + * https://api.abacatepay.com/v2/transparents/list * - * @reference https://docs.abacatepay.com/pages/payouts/list + * @reference https://docs.abacatepay.com/pages/transparents/list */ -export const RESTGetListPayoutsQueryParams = z.object({ - page: z.int().min(1).default(1).describe('Page number.').optional(), - limit: z.int().min(1).describe('Number of items per page.').optional(), +export const RESTGetListTransparentsQueryParams = z.object({ + after: z.string().describe('Cursor for the next page.').optional(), + before: z.string().describe('Cursor for the previous page.').optional(), + limit: z + .int() + .min(1) + .max(100) + .describe('Number of items per page (1-100).') + .optional(), + id: z.string().describe('Filter by QRCode/Boleto identifier.').optional(), + status: PaymentStatus.optional(), }); /** - * https://api.abacatepay.com/v2/payouts/list + * https://api.abacatepay.com/v2/transparents/list * - * @reference https://docs.abacatepay.com/pages/payouts/list + * @reference https://docs.abacatepay.com/pages/transparents/list */ -export type RESTGetListPayoutsQueryParams = z.infer< - typeof RESTGetListPayoutsQueryParams +export type RESTGetListTransparentsQueryParams = z.infer< + typeof RESTGetListTransparentsQueryParams >; /** - * https://api.abacatepay.com/v2/payouts/list + * https://api.abacatepay.com/v2/transparents/list * - * @reference https://docs.abacatepay.com/pages/payouts/list + * @reference https://docs.abacatepay.com/pages/transparents/list */ -export const RESTGetListPayoutsData = z.array(APIPayout); +export const RESTGetListTransparentsData = APIResponseWithCursorBasedPagination( + z.array(z.union([APIQRCodePIX, APIBoleto])), +); /** - * https://api.abacatepay.com/v2/payouts/list + * https://api.abacatepay.com/v2/transparents/list * - * @reference https://docs.abacatepay.com/pages/payouts/list + * @reference https://docs.abacatepay.com/pages/transparents/list */ -export type RESTGetListPayoutsData = z.infer; +export type RESTGetListTransparentsData = z.infer< + typeof RESTGetListTransparentsData +>; /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export const RESTPostCreateQRCodePixBody = RESTPostCreateNewCheckoutBody.pick({ - customer: true, - metadata: true, -}).extend({ - amount: z.int().describe('Charge amount in cents.'), - expiresIn: z.int().describe('Billing expiration time in seconds.').optional(), - description: z +export const RESTPostRefundTransparentBody = z.object({ + id: z .string() - .describe('Message that will appear when paying the PIX.') + .describe( + 'Public ID of the resource to refund (prefixes: `char_`, `pix_char_`, `card_`, `bill_`).', + ), + reason: z + .string() + .max(500) + .describe('Refund reason, shown in the transaction history.') .optional(), }); /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export type RESTPostCreateQRCodePixBody = z.infer< - typeof RESTPostCreateQRCodePixBody +export type RESTPostRefundTransparentBody = z.infer< + typeof RESTPostRefundTransparentBody >; /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export const RESTPostCreateQRCodePixData = APIQRCodePIX; +export const RESTPostRefundTransparentData = APIResponse( + z.object({ + refundPublicId: z + .string() + .describe('Public ID of the refund transaction that was created.'), + }), +); /** - * https://api.abacatepay.com/v2/transparents/create + * https://api.abacatepay.com/v2/transparents/refund * - * @reference https://docs.abacatepay.com/pages/transparents/create + * @reference https://docs.abacatepay.com/pages/transparents/refund */ -export type RESTPostCreateQRCodePixData = z.infer< - typeof RESTPostCreateQRCodePixData +export type RESTPostRefundTransparentData = z.infer< + typeof RESTPostRefundTransparentData >; /** @@ -576,7 +1184,7 @@ export type RESTPostSimulateQRCodePixPaymentBody = z.infer< * * @reference https://docs.abacatepay.com/pages/transparents/simulate-payment */ -export const RESTPostSimulateQRCodePixPaymentData = APIQRCodePIX; +export const RESTPostSimulateQRCodePixPaymentData = APIResponse(APIQRCodePIX); /** * https://api.abacatepay.com/v2/transparents/simulate-payment @@ -588,18 +1196,18 @@ export type RESTPostSimulateQRCodePixPaymentData = z.infer< >; /** - * https://api.abacatepay.com/v2/pixQrCode/check + * https://api.abacatepay.com/v2/transparents/check * - * @reference https://docs.abacatepay.com/pages/pix-qrcode/check + * @reference https://docs.abacatepay.com/pages/transparents/check */ export const RESTGetCheckQRCodePixStatusQueryParams = z.object({ id: z.string().describe('QRCode Pix ID.'), }); /** - * https://api.abacatepay.com/v2/pixQrCode/check + * https://api.abacatepay.com/v2/transparents/check * - * @reference https://docs.abacatepay.com/pages/pix-qrcode/check + * @reference https://docs.abacatepay.com/pages/transparents/check */ export type RESTGetCheckQRCodePixStatusQueryParams = z.infer< typeof RESTGetCheckQRCodePixStatusQueryParams @@ -610,10 +1218,12 @@ export type RESTGetCheckQRCodePixStatusQueryParams = z.infer< * * @reference https://docs.abacatepay.com/pages/transparents/check */ -export const RESTGetCheckQRCodePixStatusData = z.object({ - expiresAt: z.coerce.date().describe('QRCode Pix expiration date.'), - status: PaymentStatus, -}); +export const RESTGetCheckQRCodePixStatusData = APIResponse( + z.object({ + expiresAt: z.coerce.date().describe('QRCode Pix expiration date.'), + status: PaymentStatus, + }), +); /** * https://api.abacatepay.com/v2/transparents/check @@ -652,7 +1262,7 @@ export type RESTPostCreateProductBody = z.infer< * * @reference https://docs.abacatepay.com/pages/products/create */ -export const RESTPostCreateProductData = APIProduct; +export const RESTPostCreateProductData = APIResponse(APIProduct); /** * https://api.abacatepay.com/v2/products/create @@ -670,7 +1280,7 @@ export type RESTPostCreateProductData = z.infer< */ export const RESTGetListProductsQueryParams = z.object({ page: z.int().min(1).default(1).describe('Page number.').optional(), - limit: z.int().min(1).describe('Number of items per page.').optional(), + limit: z.int().min(1).describe('Limit of products to return.').optional(), }); /** @@ -687,7 +1297,9 @@ export type RESTGetListProductsQueryParams = z.infer< * * @reference https://docs.abacatepay.com/pages/products/list */ -export const RESTGetListProductsData = z.array(APIProduct); +export const RESTGetListProductsData = APIResponseWithPagination( + z.array(APIProduct), +); /** * https://api.abacatepay.com/v2/products/list @@ -720,7 +1332,7 @@ export type RESTGetProductQueryParams = z.infer< * * @reference https://docs.abacatepay.com/pages/products/get */ -export const RESTGetProductData = APIProduct; +export const RESTGetProductData = APIResponse(APIProduct); /** * https://api.abacatepay.com/v2/products/get @@ -729,12 +1341,44 @@ export const RESTGetProductData = APIProduct; */ export type RESTGetProductData = z.infer; +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export const RESTDeleteProductQueryParams = z.object({ + id: z.string().describe('The product ID.'), +}); + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export type RESTDeleteProductQueryParams = z.infer< + typeof RESTDeleteProductQueryParams +>; + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export const RESTDeleteProductData = APIResponse(APIProduct); + +/** + * https://api.abacatepay.com/v2/products/delete + * + * @reference https://docs.abacatepay.com/pages/products/delete + */ +export type RESTDeleteProductData = z.infer; + /** * https://api.abacatepay.com/v2/store/get * * @reference https://docs.abacatepay.com/pages/store/get */ -export const RESTGetStoreDetailsData = APIStore; +export const RESTGetStoreDetailsData = APIResponse(APIStore); /** * https://api.abacatepay.com/v2/store/get @@ -748,20 +1392,22 @@ export type RESTGetStoreDetailsData = z.infer; * * @reference https://docs.abacatepay.com/pages/trustMRR/mrr */ -export const RESTGetMRRData = z.object({ - mrr: z - .int() - .min(0) - .describe( - 'Monthly recurring revenue in cents. Value 0 indicates that there is no recurring revenue at the moment.', - ), - totalActiveSubscriptions: z - .int() - .min(0) - .describe( - 'Total active subscriptions. Value 0 indicates that there are no currently active subscriptions.', - ), -}); +export const RESTGetMRRData = APIResponse( + z.object({ + mrr: z + .int() + .min(0) + .describe( + 'Monthly recurring revenue in cents. Value 0 indicates that there is no recurring revenue at the moment.', + ), + totalActiveSubscriptions: z + .int() + .min(0) + .describe( + 'Total active subscriptions. Value 0 indicates that there are no currently active subscriptions.', + ), + }), +); /** * https://api.abacatepay.com/v2/public-mrr/mrr @@ -801,7 +1447,7 @@ export type RESTPostCreateSubscriptionBody = z.infer< * * @reference https://docs.abacatepay.com/pages/subscriptions/create */ -export const RESTPostCreateSubscriptionData = APISubscription; +export const RESTPostCreateSubscriptionData = APIResponse(APISubscription); /** * https://api.abacatepay.com/v2/subscriptions/create @@ -836,7 +1482,8 @@ export type RESTGetListSubscriptionsQueryParams = z.infer< * * @reference https://docs.abacatepay.com/pages/subscriptions/list */ -export const RESTGetListSubscriptionsData = z.array(APISubscription); +export const RESTGetListSubscriptionsData = + APIResponseWithCursorBasedPagination(z.array(APISubscription)); /** * https://api.abacatepay.com/v2/subscriptions/list @@ -848,45 +1495,276 @@ export type RESTGetListSubscriptionsData = z.infer< >; /** - * https://api.abacatepay.com/v2/customers/create + * https://api.abacatepay.com/v2/subscriptions/cancel * - * @reference https://docs.abacatepay.com/pages/client/create + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel */ -export const RESTPostCreateCustomerBody = APICustomer.pick({ - email: true, -}).and( - APICustomer.pick({ - name: true, - taxId: true, - zipCode: true, - cellphone: true, - metadata: true, - }).partial(), +export const RESTPostCancelSubscriptionBody = z.object({ + id: z.string().describe('Unique subscription identifier.'), +}); + +/** + * https://api.abacatepay.com/v2/subscriptions/cancel + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export type RESTPostCancelSubscriptionBody = z.infer< + typeof RESTPostCancelSubscriptionBody +>; + +/** + * Cancellation is immediate (`cancelPolicy: NOW`) — there is no grace period. + * + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export const RESTPostCancelSubscriptionData = APIResponse(APISubscription); + +/** + * @reference https://docs.abacatepay.com/pages/subscriptions/cancel + */ +export type RESTPostCancelSubscriptionData = z.infer< + typeof RESTPostCancelSubscriptionData +>; + +/** + * https://api.abacatepay.com/v2/subscriptions/change-plan + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export const RESTPostChangeSubscriptionPlanBody = z.object({ + id: z.string().describe('Unique subscription identifier.'), + productId: z + .string() + .describe( + 'ID of the new product. It must have a billing cycle configured.', + ), + quantity: z.int().describe('New quantity for the product.'), +}); + +/** + * https://api.abacatepay.com/v2/subscriptions/change-plan + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type RESTPostChangeSubscriptionPlanBody = z.infer< + typeof RESTPostChangeSubscriptionPlanBody +>; + +/** + * Only one `PENDING` change can exist per subscription — calling this again + * replaces the prior unapplied change. The change is applied at the next + * billing cycle. + * + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export const RESTPostChangeSubscriptionPlanData = APIResponse( + APISubscriptionPlanChange, ); /** - * https://api.abacatepay.com/v2/customers/create + * @reference https://docs.abacatepay.com/pages/subscriptions/change-plan + */ +export type RESTPostChangeSubscriptionPlanData = z.infer< + typeof RESTPostChangeSubscriptionPlanData +>; + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage * - * @reference https://docs.abacatepay.com/pages/client/create + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage */ -export type RESTPostCreateCustomerBody = z.infer< - typeof RESTPostCreateCustomerBody +export const RESTPostRecordSubscriptionUsageBody = z.object({ + id: z.string().describe('Unique subscription identifier.'), + productId: z + .string() + .describe( + 'ID of the pay-as-you-go product (Must not have a billing cycle).', + ), + units: z.int().describe('Number of units to record.'), + action: StringEnum( + ['add', 'subtract'], + 'Whether to add or subtract the units from the current cycle.', + ), +}); + +/** + * https://api.abacatepay.com/v2/subscriptions/record-usage + * + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage + */ +export type RESTPostRecordSubscriptionUsageBody = z.infer< + typeof RESTPostRecordSubscriptionUsageBody >; /** - * https://api.abacatepay.com/v2/customers/create + * https://api.abacatepay.com/v2/subscriptions/record-usage * - * @reference https://docs.abacatepay.com/pages/client/create + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage */ -export const RESTPostCreateCustomerData = APICustomer; +export const RESTPostRecordSubscriptionUsageData = APIResponse( + APISubscriptionUsageRecord, +); /** - * https://api.abacatepay.com/v2/customers/create + * https://api.abacatepay.com/v2/subscriptions/record-usage * - * @reference https://docs.abacatepay.com/pages/client/create + * @reference https://docs.abacatepay.com/pages/subscriptions/record-usage */ -export type RESTPostCreateCustomerData = z.infer< - typeof RESTPostCreateCustomerData +export type RESTPostRecordSubscriptionUsageData = z.infer< + typeof RESTPostRecordSubscriptionUsageData +>; + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export const RESTPostCreateWebhookBody = z.object({ + name: z.string().describe('Webhook name, for your own identification.'), + endpoint: z.url().describe('HTTPS endpoint that will receive the events.'), + secret: z + .string() + .describe('Secret used to sign the payloads sent to `endpoint`.'), + events: z + .array(WebhookEventType) + .describe('Event types this webhook should be notified about.'), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export type RESTPostCreateWebhookBody = z.infer< + typeof RESTPostCreateWebhookBody +>; + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export const RESTPostCreateWebhookData = APIResponse(APIWebhook); + +/** + * https://api.abacatepay.com/v2/webhooks/create + * + * @reference https://docs.abacatepay.com/pages/webhooks/create + */ +export type RESTPostCreateWebhookData = z.infer< + typeof RESTPostCreateWebhookData +>; + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export const RESTGetListWebhooksQueryParams = z.object({ + search: z + .string() + .describe('Search by webhook name, ID, or endpoint.') + .optional(), + after: z.string().describe('Cursor for the next page.').optional(), + before: z.string().describe('Cursor for the previous page.').optional(), + limit: z + .int() + .min(1) + .max(100) + .describe('Number of items per page (1-100).') + .optional(), + id: z.string().describe('Filter by a specific webhook ID.').optional(), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export type RESTGetListWebhooksQueryParams = z.infer< + typeof RESTGetListWebhooksQueryParams +>; + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export const RESTGetListWebhooksData = APIResponseWithCursorBasedPagination( + z.array(APIWebhook), +); + +/** + * https://api.abacatepay.com/v2/webhooks/list + * + * @reference https://docs.abacatepay.com/pages/webhooks/list + */ +export type RESTGetListWebhooksData = z.infer; + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export const RESTGetWebhookQueryParams = z.object({ + id: z.string().describe('Unique webhook identifier.'), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export type RESTGetWebhookQueryParams = z.infer< + typeof RESTGetWebhookQueryParams +>; + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export const RESTGetWebhookData = APIResponse(APIWebhook); + +/** + * https://api.abacatepay.com/v2/webhooks/get + * + * @reference https://docs.abacatepay.com/pages/webhooks/get + */ +export type RESTGetWebhookData = z.infer; + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export const RESTPostDeleteWebhookBody = z.object({ + id: z.string().describe('Unique webhook identifier.'), +}); + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export type RESTPostDeleteWebhookBody = z.infer< + typeof RESTPostDeleteWebhookBody +>; + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export const RESTPostDeleteWebhookData = APIResponse(APIWebhook); + +/** + * https://api.abacatepay.com/v2/webhooks/delete + * + * @reference https://docs.abacatepay.com/pages/webhooks/delete + */ +export type RESTPostDeleteWebhookData = z.infer< + typeof RESTPostDeleteWebhookData >; /** @@ -894,7 +1772,9 @@ export type RESTPostCreateCustomerData = z.infer< * * @reference https://docs.abacatepay.com/pages/client/list */ -export const RESTGetListCustomersData = z.array(APICustomer); +export const RESTGetListCustomersData = APIResponseWithPagination( + z.array(APICustomer), +); /** * https://api.abacatepay.com/v2/customers/list @@ -945,10 +1825,9 @@ export type RESTGetCustomerQueryParams = z.infer< * * @reference https://docs.abacatepay.com/pages/client/get */ -export const RESTGetCustomerData = APICustomer.omit({ - country: true, - zipCode: true, -}); +export const RESTGetCustomerData = APIResponse( + APICustomer.omit({ country: true, zipCode: true }), +); /** * https://api.abacatepay.com/v2/customers/get @@ -980,10 +1859,9 @@ export type RESTDeleteCustomerBody = z.infer; * * @reference https://docs.abacatepay.com/pages/client/delete */ -export const RESTDeleteCustomerData = APICustomer.omit({ - country: true, - zipCode: true, -}); +export const RESTDeleteCustomerData = APIResponse( + APICustomer.omit({ country: true, zipCode: true }), +); /** * https://api.abacatepay.com/v2/customers/delete @@ -1016,23 +1894,25 @@ export type RESTGetRevenueByPeriodQueryParams = z.infer< * * @reference https://docs.abacatepay.com/pages/trustMRR/list */ -export const RESTGetRevenueByPeriodData = z.object({ - totalRevenue: z.int().describe('Total revenue for the period in cents.'), - totalTransactions: z.int().describe('Total transactions in the period.'), - transactionsPerDay: z - .record( - z.string(), - z.object({ - amount: z - .int() - .describe("Total value of the day's transactions in cents."), - count: z.int().describe('Number of transactions for the day.'), - }), - ) - .describe( - 'Object with transactions grouped by day (key is the date in YYYY-MM-DD format).', - ), -}); +export const RESTGetRevenueByPeriodData = APIResponse( + z.object({ + totalRevenue: z.int().describe('Total revenue for the period in cents.'), + totalTransactions: z.int().describe('Total transactions in the period.'), + transactionsPerDay: z + .record( + z.string(), + z.object({ + amount: z + .int() + .describe("Total value of the day's transactions in cents."), + count: z.int().describe('Number of transactions for the day.'), + }), + ) + .describe( + 'Object with transactions grouped by day (key is the date in YYYY-MM-DD format).', + ), + }), +); /** * https://api.abacatepay.com/v2/public-mrr/revenue @@ -1048,11 +1928,13 @@ export type RESTGetRevenueByPeriodData = z.infer< * * @reference https://docs.abacatepay.com/pages/trustMRR/get */ -export const RESTGetMerchantData = z.object({ - name: z.string().describe('Store name.'), - website: z.url().describe('Store website.'), - createdAt: z.coerce.date().describe('Store creation date.'), -}); +export const RESTGetMerchantData = APIResponse( + z.object({ + name: z.string().describe('Store name.'), + website: z.url().describe('Store website.'), + createdAt: z.coerce.date().describe('Store creation date.'), + }), +); /** * https://api.abacatepay.com/v2/public-mrr/merchant-info diff --git a/packages/zod/src/v2/webhook.ts b/packages/zod/src/v2/webhook.ts index 15b7814..0f5dc71 100644 --- a/packages/zod/src/v2/webhook.ts +++ b/packages/zod/src/v2/webhook.ts @@ -3,18 +3,81 @@ import { StringEnum } from '../utils'; import { APIPayout, PaymentMethod } from '.'; /** - * https://docs.abacatepay.com/pages/webhooks + * https://docs.abacatepay.com/pages/webhooks/reference */ export const WebhookEventType = StringEnum( - ['payout.failed', 'payout.done', 'billing.paid'], + [ + 'checkout.completed', + 'checkout.refunded', + 'checkout.disputed', + 'checkout.lost', + 'transparent.completed', + 'transparent.refunded', + 'transparent.disputed', + 'transparent.lost', + 'subscription.completed', + 'subscription.cancelled', + 'subscription.renewed', + 'subscription.trial_started', + 'payout.completed', + 'payout.failed', + 'transfer.completed', + 'transfer.failed', + ], 'Webhook event type.', -).meta({ example: 'payout.done' }); +).meta({ example: 'checkout.completed' }); /** - * https://docs.abacatepay.com/pages/webhooks + * https://docs.abacatepay.com/pages/webhooks/reference */ export type WebhookEventType = z.infer; +/** + * The webhook resource itself, as returned by the `webhooks/*` endpoints. + * + * https://docs.abacatepay.com/pages/webhooks/reference + */ +export const APIWebhook = z.object({ + id: z + .string() + .describe('Unique webhook identifier.') + .meta({ example: 'webh_123' }), + name: z + .string() + .describe('Webhook name.') + .meta({ example: 'Order fulfillment' }), + endpoint: z + .url() + .describe('HTTPS endpoint that receives the events.') + .meta({ example: 'https://myshop.com/webhooks/abacatepay' }), + events: z + .array(WebhookEventType) + .describe('Event types this webhook is subscribed to.'), + devMode: z + .boolean() + .meta({ example: false }) + .describe( + 'Indicates whether the webhook was created in a testing environment.', + ), + v2: z + .boolean() + .meta({ example: true }) + .describe('Indicates whether this webhook targets the v2 API.'), + createdAt: z.coerce + .date() + .describe('Webhook creation date and time.') + .meta({ example: new Date() }), + updatedAt: z.coerce + .date() + .describe('Webhook last updated date and time.') + .meta({ example: new Date() }), +}); + +/** + * https://docs.abacatepay.com/pages/webhooks/reference + */ +export type APIWebhook = z.infer; + export const BaseWebhookEvent = < Type extends z.infer, Schema extends _ZodType, @@ -30,7 +93,7 @@ export const BaseWebhookEvent = < .meta({ example: 'log_123' }), event: z .literal(type) - .meta({ example: 'payout.done' }) + .meta({ example: type }) .describe('This field identifies the type of event received.'), devMode: z .boolean() @@ -63,10 +126,10 @@ export const WebhookPayoutFailedEvent = BaseWebhookEvent( export type WebhookPayoutFailedEvent = z.infer; /** - * https://docs.abacatepay.com/pages/webhooks#payout-done + * https://docs.abacatepay.com/pages/webhooks#payout-completed */ -export const WebhookPayoutDoneEvent = BaseWebhookEvent( - 'payout.done', +export const WebhookPayoutCompletedEvent = BaseWebhookEvent( + 'payout.completed', z.object({ transaction: APIPayout.omit({ status: true }) .extend({ @@ -80,95 +143,148 @@ export const WebhookPayoutDoneEvent = BaseWebhookEvent( ); /** - * https://docs.abacatepay.com/pages/webhooks#payout-done + * https://docs.abacatepay.com/pages/webhooks#payout-completed */ -export type WebhookPayoutDoneEvent = z.infer; +export type WebhookPayoutCompletedEvent = z.infer< + typeof WebhookPayoutCompletedEvent +>; + +const paymentData = z.object({ + amount: z + .int() + .meta({ example: 4000 }) + .describe('Charge amount in cents (e.g. 4000 = R$40.00).'), + fee: z + .literal(80) + .describe('The fee charged by AbacatePay.') + .meta({ example: 80 }), + method: PaymentMethod, +}); /** - * https://docs.abacatepay.com/pages/webhooks#billing-paid + * https://docs.abacatepay.com/pages/webhooks#checkout-completed */ -export const WebhookBillingPaidEvent = BaseWebhookEvent( - 'billing.paid', +export const WebhookCheckoutCompletedEvent = BaseWebhookEvent( + 'checkout.completed', z.object({ - payment: z - .object({ - payment: z - .object({ - amount: z - .int() - .meta({ example: 4000 }) - .describe('Charge amount in cents (e.g. 4000 = R$40.00).'), - fee: z - .int() - .describe('The fee charged by AbacatePay.') - .meta({ example: 80 }), - method: PaymentMethod, - }) - .describe('Payment data.'), - }) - .and( - z.union([ - z.object({ - pixQrCode: z.object({ - amount: z - .int() - .meta({ example: 4000 }) - .describe('Charge amount in cents (e.g. 4000 = R$40.00).'), - id: z - .string() - .describe('Unique billing identifier.') - .meta({ example: 'pix_char_123' }), - kind: z - .literal('PIX') - .describe('Kind of the payment') - .meta({ example: 'PIX' }), - status: z - .literal('PAID') - .meta({ example: 'PAID' }) - .describe('Billing status, can only be `PAID` here'), - }), - }), - z.object({ - billing: z.object({ - amount: z - .int() - .describe('Charge amount in cents (e.g. 4000 = R$40.00).') - .meta({ example: 4000 }), - id: z - .string() - .describe('Unique billing identifier.') - .meta({ example: 'bill_123' }), - externalId: z - .string() - .describe('Bill ID in your system.') - .meta({ example: 'order_123' }), - status: z - .literal('PAID') - .meta({ example: 'PAID' }) - .describe('Status of the payment. Always `PAID`.'), - url: z - .url() - .meta({ example: 'https://myshop.com/premium' }) - .describe('URL where the user can complete the payment.'), - }), - }), - ]), - ), + payment: paymentData.describe('Payment data.'), + billing: z.object({ + amount: z + .int() + .describe('Charge amount in cents (e.g. 4000 = R$40.00).') + .meta({ example: 4000 }), + id: z + .string() + .describe('Unique billing identifier.') + .meta({ example: 'bill_123' }), + externalId: z + .string() + .describe('Bill ID in your system.') + .meta({ example: 'order_123' }), + status: z + .literal('PAID') + .meta({ example: 'PAID' }) + .describe('Status of the payment. Always `PAID`.'), + url: z + .url() + .meta({ example: 'https://myshop.com/premium' }) + .describe('URL where the user can complete the payment.'), + }), + }), +); + +/** + * https://docs.abacatepay.com/pages/webhooks#checkout-completed + */ +export type WebhookCheckoutCompletedEvent = z.infer< + typeof WebhookCheckoutCompletedEvent +>; + +/** + * https://docs.abacatepay.com/pages/webhooks#transparent-completed + */ +export const WebhookTransparentCompletedEvent = BaseWebhookEvent( + 'transparent.completed', + z.object({ + payment: paymentData.describe('Payment data.'), + pixQrCode: z.object({ + amount: z + .int() + .meta({ example: 4000 }) + .describe('Charge amount in cents (e.g. 4000 = R$40.00).'), + id: z + .string() + .describe('Unique billing identifier.') + .meta({ example: 'pix_char_123' }), + kind: z + .literal('PIX') + .describe('Kind of the payment.') + .meta({ example: 'PIX' }), + status: z + .literal('PAID') + .meta({ example: 'PAID' }) + .describe('Billing status, can only be `PAID` here.'), + }), }), ); /** - * https://docs.abacatepay.com/pages/webhooks#billing-paid + * https://docs.abacatepay.com/pages/webhooks#transparent-completed + */ +export type WebhookTransparentCompletedEvent = z.infer< + typeof WebhookTransparentCompletedEvent +>; + +/** + * Event types whose payload AbacatePay does not document in detail yet. + * + * @unstable The `data` shape is a best-effort placeholder until AbacatePay documents it. + */ +export const WebhookUndocumentedEvent = z.object({ + data: z.record(z.string(), z.unknown()), + id: z.string().describe('Unique identifier for the webhook.'), + event: StringEnum( + [ + 'checkout.refunded', + 'checkout.disputed', + 'checkout.lost', + 'transparent.refunded', + 'transparent.disputed', + 'transparent.lost', + 'subscription.completed', + 'subscription.cancelled', + 'subscription.renewed', + 'subscription.trial_started', + 'transfer.completed', + 'transfer.failed', + ], + 'This field identifies the type of event received.', + ), + devMode: z + .boolean() + .describe( + 'Indicates whether the event occurred in the development environment.', + ), +}); + +/** + * Event types whose payload AbacatePay does not document in detail yet. + * + * @unstable */ -export type WebhookBillingPaidEvent = z.infer; +export type WebhookUndocumentedEvent = z.infer; /** * https://docs.abacatepay.com/pages/webhooks + * + * Any field that contains the tag "@unstable" means that the field is an assumption, it is uncertain (Since AbacatePay does not provide any information about). */ export const WebhookEvent = z.discriminatedUnion('event', [ - WebhookPayoutDoneEvent, - WebhookBillingPaidEvent, + WebhookPayoutCompletedEvent, WebhookPayoutFailedEvent, + WebhookCheckoutCompletedEvent, + WebhookTransparentCompletedEvent, + WebhookUndocumentedEvent, ]); /** diff --git a/packages/zod/src/version.ts b/packages/zod/src/version.ts index 0c3cb54..33fc726 100644 --- a/packages/zod/src/version.ts +++ b/packages/zod/src/version.ts @@ -3,7 +3,7 @@ /** * Current version of [@abacatepay/zod](https://www.npmjs.com/package/@abacatepay/zod). */ -export const version = '1.0.1' as const; +export const version = '2.0.0' as const; /** * Current version of the AbacatePay API. */ diff --git a/packages/zod/test/index.test.ts b/packages/zod/test/index.test.ts index e69de29..62902e1 100644 --- a/packages/zod/test/index.test.ts +++ b/packages/zod/test/index.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, test } from 'bun:test'; +import { + APIPaymentLink, + APIWebhook, + RESTGetListWebhooksData, + RESTPostChangeSubscriptionPlanData, + RESTPostCreateBoletoBody, + RESTPostCreateNewCheckoutBody, + RESTPostCreateTransparentBody, + RESTPostCreateWebhookBody, + RESTPostSendPixTransferBody, + WebhookEvent, +} from '../src/v2'; + +describe('v2 schemas', () => { + test('checkout create body accepts an array of methods and the new fields', () => { + const body = RESTPostCreateNewCheckoutBody.parse({ + methods: ['PIX', 'CARD', 'BOLETO'], + items: [{ id: 'prod_123', quantity: 1 }], + frequency: 'SUBSCRIPTION', + interest: { value: 100 }, + fine: { value: 200, type: 'PERCENTAGE' }, + }); + + expect(body.methods).toEqual(['PIX', 'CARD', 'BOLETO']); + }); + + test('transparent create body wire envelope accepts PIX and BOLETO variants', () => { + const pix = RESTPostCreateTransparentBody.parse({ + method: 'PIX', + data: { amount: 1000 }, + }); + const boleto = RESTPostCreateTransparentBody.parse({ + method: 'BOLETO', + data: { + amount: 1000, + customer: { name: 'Daniel Lima', taxId: '012.345.678-90' }, + }, + }); + + expect(pix.method).toBe('PIX'); + expect(boleto.method).toBe('BOLETO'); + }); + + test('RESTPostCreateBoletoBody requires name and taxId', () => { + expect(() => + RESTPostCreateBoletoBody.parse({ amount: 1000, customer: {} }), + ).toThrow(); + }); + + test('pix transfer send body validates the destination key', () => { + const body = RESTPostSendPixTransferBody.parse({ + amount: 100, + externalId: 'trx_123', + pix: { key: 'foo@bar.com', type: 'EMAIL' }, + }); + + expect(body.pix.type).toBe('EMAIL'); + }); + + test('APIPaymentLink requires frequency to be MULTIPLE_PAYMENTS', () => { + expect(() => + APIPaymentLink.parse({ + id: 'bill_123', + url: 'https://myshop.com/pay', + amount: 1000, + paidAmount: null, + status: 'PENDING', + frequency: 'ONE_TIME', + items: [{ id: 'prod_123', quantity: 1 }], + externalId: null, + createdAt: new Date(), + updatedAt: new Date(), + }), + ).toThrow(); + }); + + test('APIWebhook parses a full webhook resource', () => { + const webhook = APIWebhook.parse({ + id: 'webh_123', + name: 'Order fulfillment', + endpoint: 'https://myshop.com/webhooks/abacatepay', + events: ['checkout.completed', 'payout.failed'], + devMode: false, + v2: true, + createdAt: new Date(), + updatedAt: new Date(), + }); + + expect(webhook.events).toContain('checkout.completed'); + }); + + test('RESTPostCreateWebhookBody accepts the full v2 event taxonomy', () => { + const body = RESTPostCreateWebhookBody.parse({ + name: 'My webhook', + endpoint: 'https://myshop.com/hook', + secret: 'whsec_123', + events: ['transfer.completed', 'subscription.trial_started'], + }); + + expect(body.events).toHaveLength(2); + }); + + test('RESTGetListWebhooksData accepts the cursor-based pagination envelope', () => { + const result = RESTGetListWebhooksData.parse({ + data: [], + error: null, + success: true, + pagination: { + limit: 100, + hasNext: false, + hasPrevious: false, + nextCursor: null, + }, + }); + + expect(result.success).toBe(true); + }); + + test('RESTPostChangeSubscriptionPlanData parses a plan-change result', () => { + const result = RESTPostChangeSubscriptionPlanData.parse({ + data: { + id: 'sub_change_123', + subscriptionId: 'subs_123', + status: 'PENDING', + productId: 'prod_123', + quantity: 2, + newAmount: 8000, + requestedAt: new Date(), + }, + error: null, + success: true, + }); + + expect(result.success).toBe(true); + }); + + test('WebhookEvent discriminates payout.completed from payout.failed', () => { + const completed = WebhookEvent.parse({ + id: 'log_1', + event: 'payout.completed', + devMode: false, + data: { + transaction: { + id: 'payout_1', + devMode: false, + receiptUrl: null, + amount: 1000, + platformFee: 80, + externalId: 'trx_1', + createdAt: new Date(), + updatedAt: new Date(), + status: 'COMPLETE', + }, + }, + }); + + expect(completed.event).toBe('payout.completed'); + }); +}); diff --git a/packages/zod/tsconfig.json b/packages/zod/tsconfig.json index a47cede..12445ec 100644 --- a/packages/zod/tsconfig.json +++ b/packages/zod/tsconfig.json @@ -1,10 +1,8 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src" - ] + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] }