From 38ebc076a49d7b46e80ac7085def7764939dab90 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 14 Dec 2023 10:35:02 +0100 Subject: [PATCH 01/33] chore: :construction_worker: change ports on dev --- example/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/package.json b/example/package.json index 873a365..18974d1 100644 --- a/example/package.json +++ b/example/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "next dev --port 8001", "build": "next build", "start": "next start", "lint": "next lint --fix --dir src", diff --git a/package.json b/package.json index 24e1051..d4266d3 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ }, "type": "module", "scripts": { - "dev": "pnpm generate && next dev", + "dev": "pnpm generate && next dev --port 8000", "build": "pnpm generate && next build", "deploy": "tsx ./src/deploy.ts", "start": "next start", From 75e03f4436d16c28189fc229e6bdca68e91d536f Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 14 Dec 2023 13:15:33 +0100 Subject: [PATCH 02/33] Update @types/node to version 20.10.4 --- package.json | 2 +- pnpm-lock.yaml | 125 +++++++++++++++++++++++++------------------------ 2 files changed, 66 insertions(+), 61 deletions(-) diff --git a/package.json b/package.json index d4266d3..91b90a1 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "@types/authorizenet": "1.0.1", "@types/bluebird": "3.5.38", "@types/lodash-es": "4.17.8", - "@types/node": "20.5.4", + "@types/node": "20.10.4", "@types/omit-deep-lodash": "1.1.1", "@types/react": "18.2.21", "@types/react-dom": "18.2.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30b7a00..17c1217 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - overrides: '@urql/exchange-auth>@urql/core': 3.2.2 @@ -52,7 +48,7 @@ dependencies: version: 1.13.0 '@vanilla-extract/next-plugin': specifier: 2.3.0 - version: 2.3.0(@types/node@20.5.4)(next@13.4.19)(webpack@5.88.2) + version: 2.3.0(@types/node@20.10.4)(next@13.4.19)(webpack@5.88.2) '@vanilla-extract/recipes': specifier: 0.5.0 version: 0.5.0(@vanilla-extract/css@1.13.0) @@ -139,7 +135,7 @@ dependencies: version: 0.6.2 vite: specifier: 4.4.9 - version: 4.4.9(@types/node@20.5.4) + version: 4.4.9(@types/node@20.10.4) vitest: specifier: 0.34.2 version: 0.34.2(jsdom@22.1.0) @@ -165,7 +161,7 @@ devDependencies: version: 5.0.0(graphql@16.8.1) '@graphql-codegen/cli': specifier: 5.0.0 - version: 5.0.0(@types/node@20.5.4)(graphql@16.8.1) + version: 5.0.0(@types/node@20.10.4)(graphql@16.8.1) '@graphql-codegen/introspection': specifier: 4.0.0 version: 4.0.0(graphql@16.8.1) @@ -224,8 +220,8 @@ devDependencies: specifier: 4.17.8 version: 4.17.8 '@types/node': - specifier: 20.5.4 - version: 20.5.4 + specifier: 20.10.4 + version: 20.10.4 '@types/omit-deep-lodash': specifier: 1.1.1 version: 1.1.1 @@ -249,7 +245,7 @@ devDependencies: version: 6.4.1(eslint@8.47.0)(typescript@5.1.6) '@vanilla-extract/vite-plugin': specifier: 3.9.0 - version: 3.9.0(@types/node@20.5.4)(ts-node@10.9.1)(vite@4.4.9) + version: 3.9.0(@types/node@20.10.4)(ts-node@10.9.1)(vite@4.4.9) '@vitest/coverage-v8': specifier: 0.34.2 version: 0.34.2(vitest@0.34.2) @@ -297,7 +293,7 @@ devDependencies: version: 0.11.0(@pollyjs/core@6.0.6) ts-node: specifier: 10.9.1 - version: 10.9.1(@types/node@20.5.4)(typescript@5.1.6) + version: 10.9.1(@types/node@20.10.4)(typescript@5.1.6) typescript: specifier: 5.1.6 version: 5.1.6 @@ -1811,7 +1807,7 @@ packages: tslib: 2.5.3 dev: true - /@graphql-codegen/cli@5.0.0(@types/node@20.5.4)(graphql@16.8.1): + /@graphql-codegen/cli@5.0.0(@types/node@20.10.4)(graphql@16.8.1): resolution: {integrity: sha512-A7J7+be/a6e+/ul2KI5sfJlpoqeqwX8EzktaKCeduyVKgOLA6W5t+NUGf6QumBDXU8PEOqXk3o3F+RAwCWOiqA==} hasBin: true peerDependencies: @@ -1829,12 +1825,12 @@ packages: '@graphql-tools/apollo-engine-loader': 8.0.0(graphql@16.8.1) '@graphql-tools/code-file-loader': 8.0.2(graphql@16.8.1) '@graphql-tools/git-loader': 8.0.2(graphql@16.8.1) - '@graphql-tools/github-loader': 8.0.0(@types/node@20.5.4)(graphql@16.8.1) + '@graphql-tools/github-loader': 8.0.0(@types/node@20.10.4)(graphql@16.8.1) '@graphql-tools/graphql-file-loader': 8.0.0(graphql@16.8.1) '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) '@graphql-tools/load': 8.0.0(graphql@16.8.1) - '@graphql-tools/prisma-loader': 8.0.1(@types/node@20.5.4)(graphql@16.8.1) - '@graphql-tools/url-loader': 8.0.0(@types/node@20.5.4)(graphql@16.8.1) + '@graphql-tools/prisma-loader': 8.0.1(@types/node@20.10.4)(graphql@16.8.1) + '@graphql-tools/url-loader': 8.0.0(@types/node@20.10.4)(graphql@16.8.1) '@graphql-tools/utils': 10.0.5(graphql@16.8.1) '@whatwg-node/fetch': 0.8.8 chalk: 4.1.2 @@ -1842,7 +1838,7 @@ packages: debounce: 1.2.1 detect-indent: 6.1.0 graphql: 16.8.1 - graphql-config: 5.0.2(@types/node@20.5.4)(graphql@16.8.1) + graphql-config: 5.0.2(@types/node@20.10.4)(graphql@16.8.1) inquirer: 8.2.6 is-glob: 4.0.3 jiti: 1.19.3 @@ -2126,7 +2122,7 @@ packages: - utf-8-validate dev: true - /@graphql-tools/executor-http@1.0.2(@types/node@20.5.4)(graphql@16.8.1): + /@graphql-tools/executor-http@1.0.2(@types/node@20.10.4)(graphql@16.8.1): resolution: {integrity: sha512-JKTB4E3kdQM2/1NEcyrVPyQ8057ZVthCV5dFJiKktqY9IdmF00M8gupFcW3jlbM/Udn78ickeUBsUzA3EouqpA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -2137,7 +2133,7 @@ packages: '@whatwg-node/fetch': 0.9.9 extract-files: 11.0.0 graphql: 16.8.1 - meros: 1.3.0(@types/node@20.5.4) + meros: 1.3.0(@types/node@20.10.4) tslib: 2.6.2 value-or-promise: 1.0.12 transitivePeerDependencies: @@ -2192,14 +2188,14 @@ packages: - supports-color dev: true - /@graphql-tools/github-loader@8.0.0(@types/node@20.5.4)(graphql@16.8.1): + /@graphql-tools/github-loader@8.0.0(@types/node@20.10.4)(graphql@16.8.1): resolution: {integrity: sha512-VuroArWKcG4yaOWzV0r19ElVIV6iH6UKDQn1MXemND0xu5TzrFme0kf3U9o0YwNo0kUYEk9CyFM0BYg4he17FA==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: '@ardatan/sync-fetch': 0.0.1 - '@graphql-tools/executor-http': 1.0.2(@types/node@20.5.4)(graphql@16.8.1) + '@graphql-tools/executor-http': 1.0.2(@types/node@20.10.4)(graphql@16.8.1) '@graphql-tools/graphql-tag-pluck': 8.0.2(graphql@16.8.1) '@graphql-tools/utils': 10.0.5(graphql@16.8.1) '@whatwg-node/fetch': 0.9.9 @@ -2312,13 +2308,13 @@ packages: tslib: 2.6.2 dev: true - /@graphql-tools/prisma-loader@8.0.1(@types/node@20.5.4)(graphql@16.8.1): + /@graphql-tools/prisma-loader@8.0.1(@types/node@20.10.4)(graphql@16.8.1): resolution: {integrity: sha512-bl6e5sAYe35Z6fEbgKXNrqRhXlCJYeWKBkarohgYA338/SD9eEhXtg3Cedj7fut3WyRLoQFpHzfiwxKs7XrgXg==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: - '@graphql-tools/url-loader': 8.0.0(@types/node@20.5.4)(graphql@16.8.1) + '@graphql-tools/url-loader': 8.0.0(@types/node@20.10.4)(graphql@16.8.1) '@graphql-tools/utils': 10.0.5(graphql@16.8.1) '@types/js-yaml': 4.0.5 '@types/json-stable-stringify': 1.0.34 @@ -2387,7 +2383,7 @@ packages: value-or-promise: 1.0.12 dev: true - /@graphql-tools/url-loader@8.0.0(@types/node@20.5.4)(graphql@16.8.1): + /@graphql-tools/url-loader@8.0.0(@types/node@20.10.4)(graphql@16.8.1): resolution: {integrity: sha512-rPc9oDzMnycvz+X+wrN3PLrhMBQkG4+sd8EzaFN6dypcssiefgWKToXtRKI8HHK68n2xEq1PyrOpkjHFJB+GwA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -2396,7 +2392,7 @@ packages: '@ardatan/sync-fetch': 0.0.1 '@graphql-tools/delegate': 10.0.2(graphql@16.8.1) '@graphql-tools/executor-graphql-ws': 1.1.0(graphql@16.8.1) - '@graphql-tools/executor-http': 1.0.2(@types/node@20.5.4)(graphql@16.8.1) + '@graphql-tools/executor-http': 1.0.2(@types/node@20.10.4)(graphql@16.8.1) '@graphql-tools/executor-legacy-ws': 1.0.1(graphql@16.8.1) '@graphql-tools/utils': 10.0.5(graphql@16.8.1) '@graphql-tools/wrap': 10.0.0(graphql@16.8.1) @@ -4128,7 +4124,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 20.5.4 + '@types/node': 20.10.4 dev: true /@types/is-ci@3.0.0: @@ -4175,8 +4171,10 @@ packages: /@types/node@12.20.55: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - /@types/node@20.5.4: - resolution: {integrity: sha512-Y9vbIAoM31djQZrPYjpTLo0XlaSwOIsrlfE3LpulZeRblttsLQRFRlBAppW0LOxyT3ALj2M5vU1ucQQayQH3jA==} + /@types/node@20.10.4: + resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} + dependencies: + undici-types: 5.26.5 /@types/normalize-package-data@2.4.1: resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==} @@ -4213,7 +4211,7 @@ packages: /@types/set-cookie-parser@2.4.3: resolution: {integrity: sha512-7QhnH7bi+6KAhBB+Auejz1uV9DHiopZqu7LfR/5gZZTkejJV5nYeZZpgfFoE0N8aDsXuiYpfKyfyMatCwQhyTQ==} dependencies: - '@types/node': 20.5.4 + '@types/node': 20.10.4 dev: true /@types/setup-polly-jest@0.5.2: @@ -4227,7 +4225,7 @@ packages: /@types/ws@8.5.5: resolution: {integrity: sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==} dependencies: - '@types/node': 20.5.4 + '@types/node': 20.10.4 dev: true /@typescript-eslint/eslint-plugin@6.4.1(@typescript-eslint/parser@6.4.1)(eslint@8.47.0)(typescript@5.1.6): @@ -4476,7 +4474,7 @@ packages: media-query-parser: 2.0.2 outdent: 0.8.0 - /@vanilla-extract/integration@6.2.2(@types/node@20.5.4): + /@vanilla-extract/integration@6.2.2(@types/node@20.10.4): resolution: {integrity: sha512-gV3qPFjFap1+IrPeuFy+tEZOq7l7ifJf1ik/kluDWhPr1ffsFG9puq1/jjJ4rod1BIC79Q5ZWPNvBInHyxfCew==} dependencies: '@babel/core': 7.22.10 @@ -4490,8 +4488,8 @@ packages: lodash: 4.17.21 mlly: 1.4.0 outdent: 0.8.0 - vite: 4.4.9(@types/node@20.5.4) - vite-node: 0.28.5(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) + vite-node: 0.28.5(@types/node@20.10.4) transitivePeerDependencies: - '@types/node' - less @@ -4502,12 +4500,12 @@ packages: - supports-color - terser - /@vanilla-extract/next-plugin@2.3.0(@types/node@20.5.4)(next@13.4.19)(webpack@5.88.2): + /@vanilla-extract/next-plugin@2.3.0(@types/node@20.10.4)(next@13.4.19)(webpack@5.88.2): resolution: {integrity: sha512-l7ZzcL1G9zuJTAhBbOy6NKW/FPDOhDzfUXPhFGSTN1sdtO7qBmGLtOGFZATAYboAuIWXhGgnrfkyNgQh2adXZw==} peerDependencies: next: '>=12.1.7' dependencies: - '@vanilla-extract/webpack-plugin': 2.3.0(@types/node@20.5.4)(webpack@5.88.2) + '@vanilla-extract/webpack-plugin': 2.3.0(@types/node@20.10.4)(webpack@5.88.2) browserslist: 4.21.10 next: 13.4.19(@babel/core@7.22.10)(react-dom@18.2.0)(react@18.2.0) transitivePeerDependencies: @@ -4533,16 +4531,16 @@ packages: '@vanilla-extract/css': 1.13.0 dev: false - /@vanilla-extract/vite-plugin@3.9.0(@types/node@20.5.4)(ts-node@10.9.1)(vite@4.4.9): + /@vanilla-extract/vite-plugin@3.9.0(@types/node@20.10.4)(ts-node@10.9.1)(vite@4.4.9): resolution: {integrity: sha512-Q2HYAyEJ93Uy7GHQPPiP8SXwPMHGpd4d0YnrIQqB0YZccYbGJR/WFIln9Dmbzx2pdngQUoOfhwEg6kJF8sQrog==} peerDependencies: vite: ^2.2.3 || ^3.0.0 || ^4.0.3 dependencies: - '@vanilla-extract/integration': 6.2.2(@types/node@20.5.4) + '@vanilla-extract/integration': 6.2.2(@types/node@20.10.4) outdent: 0.8.0 postcss: 8.4.28 postcss-load-config: 3.1.4(postcss@8.4.28)(ts-node@10.9.1) - vite: 4.4.9(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) transitivePeerDependencies: - '@types/node' - less @@ -4555,12 +4553,12 @@ packages: - ts-node dev: true - /@vanilla-extract/webpack-plugin@2.3.0(@types/node@20.5.4)(webpack@5.88.2): + /@vanilla-extract/webpack-plugin@2.3.0(@types/node@20.10.4)(webpack@5.88.2): resolution: {integrity: sha512-c+oaozLGNu+dqLNattJ9nVmy6t2OZw6qEW0xJkPS4bRXlpMSNrPwkKB1Lpov2yd2/eDuxTFi760zTZygwFNBVA==} peerDependencies: webpack: ^4.30.0 || ^5.20.2 dependencies: - '@vanilla-extract/integration': 6.2.2(@types/node@20.5.4) + '@vanilla-extract/integration': 6.2.2(@types/node@20.10.4) chalk: 4.1.2 debug: 4.3.4 loader-utils: 2.0.4 @@ -4586,7 +4584,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.22.5(@babel/core@7.22.10) '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.22.10) react-refresh: 0.14.0 - vite: 4.4.9(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) transitivePeerDependencies: - supports-color dev: false @@ -6671,7 +6669,7 @@ packages: dependencies: '@typescript-eslint/utils': 6.4.1(eslint@8.47.0)(typescript@5.1.6) eslint: 8.47.0 - vite: 4.4.9(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) vitest: 0.34.2(jsdom@22.1.0) transitivePeerDependencies: - supports-color @@ -6803,7 +6801,7 @@ packages: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} dependencies: - '@types/node': 20.5.4 + '@types/node': 20.10.4 require-like: 0.1.2 /event-emitter@0.3.5: @@ -7348,7 +7346,7 @@ packages: /graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - /graphql-config@5.0.2(@types/node@20.5.4)(graphql@16.8.1): + /graphql-config@5.0.2(@types/node@20.10.4)(graphql@16.8.1): resolution: {integrity: sha512-7TPxOrlbiG0JplSZYCyxn2XQtqVhXomEjXUmWJVSS5ET1nPhOJSsIb/WTwqWhcYX6G0RlHXSj9PLtGTKmxLNGg==} engines: {node: '>= 16.0.0'} peerDependencies: @@ -7362,7 +7360,7 @@ packages: '@graphql-tools/json-file-loader': 8.0.0(graphql@16.8.1) '@graphql-tools/load': 8.0.0(graphql@16.8.1) '@graphql-tools/merge': 9.0.0(graphql@16.8.1) - '@graphql-tools/url-loader': 8.0.0(@types/node@20.5.4)(graphql@16.8.1) + '@graphql-tools/url-loader': 8.0.0(@types/node@20.10.4)(graphql@16.8.1) '@graphql-tools/utils': 10.0.5(graphql@16.8.1) cosmiconfig: 8.2.0 graphql: 16.8.1 @@ -8037,7 +8035,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.5.4 + '@types/node': 20.10.4 merge-stream: 2.0.0 supports-color: 8.1.1 dev: false @@ -8567,7 +8565,7 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - /meros@1.3.0(@types/node@20.5.4): + /meros@1.3.0(@types/node@20.10.4): resolution: {integrity: sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==} engines: {node: '>=13'} peerDependencies: @@ -8576,7 +8574,7 @@ packages: '@types/node': optional: true dependencies: - '@types/node': 20.5.4 + '@types/node': 20.10.4 dev: true /methods@1.1.2: @@ -9328,7 +9326,7 @@ packages: dependencies: lilconfig: 2.1.0 postcss: 8.4.28 - ts-node: 10.9.1(@types/node@20.5.4)(typescript@5.1.6) + ts-node: 10.9.1(@types/node@20.10.4)(typescript@5.1.6) yaml: 1.10.2 dev: true @@ -10695,7 +10693,7 @@ packages: resolution: {integrity: sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA==} dev: true - /ts-node@10.9.1(@types/node@20.5.4)(typescript@5.1.6): + /ts-node@10.9.1(@types/node@20.10.4)(typescript@5.1.6): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -10714,7 +10712,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 20.5.4 + '@types/node': 20.10.4 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -10946,6 +10944,9 @@ packages: engines: {node: '>=0.10.0'} dev: true + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -11146,7 +11147,7 @@ packages: extsprintf: 1.3.0 dev: false - /vite-node@0.28.5(@types/node@20.5.4): + /vite-node@0.28.5(@types/node@20.10.4): resolution: {integrity: sha512-LmXb9saMGlrMZbXTvOveJKwMTBTNUH66c8rJnQ0ZPNX+myPEol64+szRzXtV5ORb0Hb/91yq+/D3oERoyAt6LA==} engines: {node: '>=v14.16.0'} hasBin: true @@ -11158,7 +11159,7 @@ packages: picocolors: 1.0.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.4.9(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) transitivePeerDependencies: - '@types/node' - less @@ -11169,7 +11170,7 @@ packages: - supports-color - terser - /vite-node@0.34.2(@types/node@20.5.4): + /vite-node@0.34.2(@types/node@20.10.4): resolution: {integrity: sha512-JtW249Zm3FB+F7pQfH56uWSdlltCo1IOkZW5oHBzeQo0iX4jtC7o1t9aILMGd9kVekXBP2lfJBEQt9rBh07ebA==} engines: {node: '>=v14.18.0'} hasBin: true @@ -11179,7 +11180,7 @@ packages: mlly: 1.4.0 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.9(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) transitivePeerDependencies: - '@types/node' - less @@ -11201,13 +11202,13 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.1.6) - vite: 4.4.9(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@4.4.9(@types/node@20.5.4): + /vite@4.4.9(@types/node@20.10.4): resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true @@ -11235,7 +11236,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.5.4 + '@types/node': 20.10.4 esbuild: 0.18.20 postcss: 8.4.28 rollup: 3.28.1 @@ -11275,7 +11276,7 @@ packages: dependencies: '@types/chai': 4.3.5 '@types/chai-subset': 1.3.3 - '@types/node': 20.5.4 + '@types/node': 20.10.4 '@vitest/expect': 0.34.2 '@vitest/runner': 0.34.2 '@vitest/snapshot': 0.34.2 @@ -11295,8 +11296,8 @@ packages: strip-literal: 1.3.0 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.4.9(@types/node@20.5.4) - vite-node: 0.34.2(@types/node@20.5.4) + vite: 4.4.9(@types/node@20.10.4) + vite-node: 0.34.2(@types/node@20.10.4) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -11686,3 +11687,7 @@ packages: react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false From a6e7ab882487735e1266b664f699581c7a3e5d30 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 14 Dec 2023 13:19:39 +0100 Subject: [PATCH 03/33] Add signature key to app config and authorize-net config --- src/lib/env.mjs | 8 ++++++++ .../authorize-net/authorize-net-config.ts | 1 + .../active-provider-resolver.test.ts | 2 ++ .../configuration/app-config-resolver.test.ts | 1 + .../configuration/app-config-resolver.ts | 1 + .../configuration/app-configurator.test.ts | 1 + .../provider/provider-configurator.test.ts | 20 +++++++++++++++++++ 7 files changed, 34 insertions(+) diff --git a/src/lib/env.mjs b/src/lib/env.mjs index 0c8f939..4faa5bf 100644 --- a/src/lib/env.mjs +++ b/src/lib/env.mjs @@ -37,6 +37,13 @@ export const env = createEnv({ .describe( "Transaction key needed to authenticate Authorize.net SDK. You can generate it in Account → API Credentials & Keys.", ), + AUTHORIZE_SIGNATURE_KEY: z + .string() + .min(1) + // .optional() + .describe( + "Signature Key needed to verify webhooks. You can generate it in Account → API Credentials & Keys.", + ), AUTHORIZE_PUBLIC_CLIENT_KEY: z .string() .min(1) @@ -91,6 +98,7 @@ export const env = createEnv({ REST_APL_TOKEN: process.env.REST_APL_TOKEN, AUTHORIZE_API_LOGIN_ID: process.env.AUTHORIZE_API_LOGIN_ID, AUTHORIZE_TRANSACTION_KEY: process.env.AUTHORIZE_TRANSACTION_KEY, + AUTHORIZE_SIGNATURE_KEY: process.env.AUTHORIZE_SIGNATURE_KEY, AUTHORIZE_PUBLIC_CLIENT_KEY: process.env.AUTHORIZE_PUBLIC_CLIENT_KEY, AUTHORIZE_ENVIRONMENT: process.env.AUTHORIZE_ENVIRONMENT, AUTHORIZE_SALEOR_CHANNEL_SLUG: process.env.AUTHORIZE_SALEOR_CHANNEL_SLUG, diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index a20cb02..d14c45e 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -6,6 +6,7 @@ const inputSchema = z.object({ apiLoginId: z.string().min(1), publicClientKey: z.string().min(1), transactionKey: z.string().min(1), + signatureKey: z.string().min(1), environment: authorizeEnvironmentSchema, }); diff --git a/src/modules/configuration/active-provider-resolver.test.ts b/src/modules/configuration/active-provider-resolver.test.ts index f300185..737443f 100644 --- a/src/modules/configuration/active-provider-resolver.test.ts +++ b/src/modules/configuration/active-provider-resolver.test.ts @@ -13,6 +13,7 @@ describe("ActiveProviderResolver", () => { publicClientKey: "publicClientKey1", transactionKey: "transactionKey1", environment: "sandbox", + signatureKey: "signatureKey1", }, { id: "provider2", @@ -20,6 +21,7 @@ describe("ActiveProviderResolver", () => { publicClientKey: "publicClientKey2", transactionKey: "transactionKey2", environment: "sandbox", + signatureKey: "signatureKey2", }, ], connections: [ diff --git a/src/modules/configuration/app-config-resolver.test.ts b/src/modules/configuration/app-config-resolver.test.ts index 3257f85..b820dd1 100644 --- a/src/modules/configuration/app-config-resolver.test.ts +++ b/src/modules/configuration/app-config-resolver.test.ts @@ -19,6 +19,7 @@ const appConfig: AppConfig.Shape = { id: "provider1", publicClientKey: "publicClientKey1", transactionKey: "transactionKey1", + signatureKey: "signatureKey1", }, ], }; diff --git a/src/modules/configuration/app-config-resolver.ts b/src/modules/configuration/app-config-resolver.ts index 2c1dfcc..747109a 100644 --- a/src/modules/configuration/app-config-resolver.ts +++ b/src/modules/configuration/app-config-resolver.ts @@ -32,6 +32,7 @@ export class AppConfigResolver { publicClientKey: env.AUTHORIZE_PUBLIC_CLIENT_KEY, transactionKey: env.AUTHORIZE_TRANSACTION_KEY, environment: env.AUTHORIZE_ENVIRONMENT, + signatureKey: env.AUTHORIZE_SIGNATURE_KEY, }; const connectionInput = { diff --git a/src/modules/configuration/app-configurator.test.ts b/src/modules/configuration/app-configurator.test.ts index a509efe..1e6bea2 100644 --- a/src/modules/configuration/app-configurator.test.ts +++ b/src/modules/configuration/app-configurator.test.ts @@ -11,6 +11,7 @@ describe("AppConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }); const parsedConfig = JSON.parse(configurator.serialize()); diff --git a/src/modules/provider/provider-configurator.test.ts b/src/modules/provider/provider-configurator.test.ts index 680baa0..a8e8a1b 100644 --- a/src/modules/provider/provider-configurator.test.ts +++ b/src/modules/provider/provider-configurator.test.ts @@ -18,6 +18,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, { id: "2", @@ -25,6 +26,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key-2", }, ]); @@ -35,6 +37,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, { id: "2", @@ -42,6 +45,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key-2", }, ]); }); @@ -56,6 +60,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, ]); @@ -65,6 +70,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }); }); @@ -76,6 +82,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, ]); @@ -92,6 +99,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }); expect(configurator.getProviders()).toEqual([ @@ -101,6 +109,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, ]); }); @@ -116,6 +125,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }); expect(configurator.getProviders()).toEqual(rootData); @@ -128,6 +138,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, ]); @@ -137,6 +148,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "new-transaction-key", environment: "sandbox", publicClientKey: "new-public-client-key", + signatureKey: "new-signature-key", }); expect(configurator.getProviders()).toEqual([ @@ -146,6 +158,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "new-transaction-key", environment: "sandbox", publicClientKey: "new-public-client-key", + signatureKey: "new-signature-key", }, ]); }); @@ -160,6 +173,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, { id: "2", @@ -167,6 +181,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key-2", }, ]); @@ -179,6 +194,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key-2", }, ]); }); @@ -191,6 +207,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, { id: "2", @@ -198,6 +215,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key-2", }, ]); @@ -210,6 +228,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", }, { id: "2", @@ -217,6 +236,7 @@ describe("ProvidersConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key-2", }, ]); }); From 085966f35995fde131cf59fcd14ffb3d1ae8ef90 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 14 Dec 2023 13:24:46 +0100 Subject: [PATCH 04/33] Add webhooks to provider configuration --- src/modules/authorize-net/authorize-net-config.ts | 10 ++++++++++ .../active-provider-resolver.test.ts | 2 ++ .../configuration/app-config-resolver.test.ts | 1 + .../configuration/app-configurator.test.ts | 2 ++ .../provider/provider-configurator.test.ts | 15 +++++++++++++++ 5 files changed, 30 insertions(+) diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index d14c45e..9bffda8 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -2,12 +2,22 @@ import { z } from "zod"; export const authorizeEnvironmentSchema = z.enum(["sandbox", "production"]); +const authorizeNetEventSchema = z.enum(["net.authorize.payment.authcapture.created"]); // todo: add more + +export const webhookSchema = z.object({ + name: z.string(), + url: z.string(), + eventTypes: z.array(authorizeNetEventSchema), + status: z.enum(["active", "inactive"]), +}); + const inputSchema = z.object({ apiLoginId: z.string().min(1), publicClientKey: z.string().min(1), transactionKey: z.string().min(1), signatureKey: z.string().min(1), environment: authorizeEnvironmentSchema, + webhooks: z.array(webhookSchema), }); const fullSchema = inputSchema.extend({ diff --git a/src/modules/configuration/active-provider-resolver.test.ts b/src/modules/configuration/active-provider-resolver.test.ts index 737443f..cb10abb 100644 --- a/src/modules/configuration/active-provider-resolver.test.ts +++ b/src/modules/configuration/active-provider-resolver.test.ts @@ -14,6 +14,7 @@ describe("ActiveProviderResolver", () => { transactionKey: "transactionKey1", environment: "sandbox", signatureKey: "signatureKey1", + webhooks: [], }, { id: "provider2", @@ -22,6 +23,7 @@ describe("ActiveProviderResolver", () => { transactionKey: "transactionKey2", environment: "sandbox", signatureKey: "signatureKey2", + webhooks: [], }, ], connections: [ diff --git a/src/modules/configuration/app-config-resolver.test.ts b/src/modules/configuration/app-config-resolver.test.ts index b820dd1..8063492 100644 --- a/src/modules/configuration/app-config-resolver.test.ts +++ b/src/modules/configuration/app-config-resolver.test.ts @@ -20,6 +20,7 @@ const appConfig: AppConfig.Shape = { publicClientKey: "publicClientKey1", transactionKey: "transactionKey1", signatureKey: "signatureKey1", + webhooks: [], }, ], }; diff --git a/src/modules/configuration/app-configurator.test.ts b/src/modules/configuration/app-configurator.test.ts index 1e6bea2..66c5496 100644 --- a/src/modules/configuration/app-configurator.test.ts +++ b/src/modules/configuration/app-configurator.test.ts @@ -12,6 +12,7 @@ describe("AppConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }); const parsedConfig = JSON.parse(configurator.serialize()); @@ -24,6 +25,7 @@ describe("AppConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + webhooks: [], }, ], connections: [], diff --git a/src/modules/provider/provider-configurator.test.ts b/src/modules/provider/provider-configurator.test.ts index a8e8a1b..a1bfcb7 100644 --- a/src/modules/provider/provider-configurator.test.ts +++ b/src/modules/provider/provider-configurator.test.ts @@ -19,6 +19,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, { id: "2", @@ -27,6 +28,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", + webhooks: [], }, ]); @@ -38,6 +40,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, { id: "2", @@ -46,6 +49,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", + webhooks: [], }, ]); }); @@ -61,6 +65,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, ]); @@ -71,6 +76,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }); }); @@ -83,6 +89,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, ]); @@ -100,6 +107,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }); expect(configurator.getProviders()).toEqual([ @@ -126,6 +134,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }); expect(configurator.getProviders()).toEqual(rootData); @@ -139,6 +148,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, ]); @@ -149,6 +159,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "new-public-client-key", signatureKey: "new-signature-key", + webhooks: [], }); expect(configurator.getProviders()).toEqual([ @@ -174,6 +185,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, { id: "2", @@ -182,6 +194,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", + webhooks: [], }, ]); @@ -208,6 +221,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", + webhooks: [], }, { id: "2", @@ -216,6 +230,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", + webhooks: [], }, ]); From 9f5d1b340efee8c5d1ef7913195582f11d721103 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 14 Dec 2023 14:15:56 +0100 Subject: [PATCH 05/33] Add createAuthorizeWebhooksFetch function for Authorize.net webhooks client --- .../create-authorize-webhooks-fetch.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts diff --git a/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts b/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts new file mode 100644 index 0000000..8e8ae23 --- /dev/null +++ b/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts @@ -0,0 +1,36 @@ +import { type AuthorizeProviderConfig } from "@/modules/authorize-net/authorize-net-config"; + +/** + * @description Create a value for Authorization: basic header for Authorize.net webhooks + * @see https://developer.authorize.net/api/reference/features/webhooks.html + */ +function createAuthorizeAuthenticationKey(config: AuthorizeProviderConfig.FullShape): string { + const concatenatedKey = `${config.apiLoginId}:${config.transactionKey}`; + const encodedKey = Buffer.from(concatenatedKey).toString("base64"); + + return encodedKey; +} + +type AuthorizeWebhooksFetchParams = { + path?: string; + body?: Record; + method: Required; +} & Omit; + +export function createAuthorizeWebhooksFetch(config: AuthorizeProviderConfig.FullShape) { + const authenticationKey = createAuthorizeAuthenticationKey(config); + const url = + config.environment === "sandbox" + ? "https://apitest.authorize.net/rest/v1/webhooks" + : "https://api.authorize.net/rest/v1/webhooks"; + + return ({ path, body }: AuthorizeWebhooksFetchParams) => { + const apiUrl = path ? `${url}/${path}` : url; + return fetch(apiUrl, { + headers: { + Authorization: `Basic ${authenticationKey}`, + }, + body: JSON.stringify(body), + }); + }; +} From 879fb5a21cec3691e75484ccf56c56495a31bfe8 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 14 Dec 2023 15:03:31 +0100 Subject: [PATCH 06/33] Add AuthorizeNetWebhooksClient and refactor webhooks with AppInitializer --- src/app-initializer.ts | 85 ++++++++++++++++++ .../authorize-net/authorize-net-error.ts | 4 + .../authorize-net-webhooks-client.ts | 89 +++++++++++++++++++ .../webhooks/webhook-manager-service.ts | 50 ++--------- .../transaction-cancelation-requested.ts | 11 ++- .../transaction-initialize-session.ts | 8 +- .../webhooks/transaction-process-session.ts | 8 +- .../webhooks/transaction-refund-requested.ts | 9 +- 8 files changed, 210 insertions(+), 54 deletions(-) create mode 100644 src/app-initializer.ts create mode 100644 src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts diff --git a/src/app-initializer.ts b/src/app-initializer.ts new file mode 100644 index 0000000..7053e40 --- /dev/null +++ b/src/app-initializer.ts @@ -0,0 +1,85 @@ +import { type AuthData } from "@saleor/app-sdk/APL"; +import { type AuthorizeProviderConfig } from "./modules/authorize-net/authorize-net-config"; +import { ActiveProviderResolver } from "./modules/configuration/active-provider-resolver"; +import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; +import { AppConfigResolver } from "./modules/configuration/app-config-resolver"; +import { AuthorizeNetWebhooksClient } from "./modules/authorize-net/webhooks-client/authorize-net-webhooks-client"; +import { WebhookManagerService } from "./modules/webhooks/webhook-manager-service"; +import { createServerClient } from "@/lib/create-graphq-client"; +import { type MetadataItem } from "generated/graphql"; +import { createLogger } from "@/lib/logger"; + +/** + * @description This class is used to get the Authorize.net configuration for the app and to register webhooks. + */ +export class AppInitializer { + private appMetadata: readonly MetadataItem[]; + private authData: AuthData; + private channelSlug: string; + private authorizeConfig: AuthorizeProviderConfig.FullShape | null = null; + private logger = createLogger({ + name: "AppInitializer", + }); + + constructor({ + appMetadata, + authData, + channelSlug, + }: { + appMetadata: readonly MetadataItem[]; + authData: AuthData; + channelSlug: string; + }) { + this.appMetadata = appMetadata; + this.authData = authData; + this.channelSlug = channelSlug; + } + + private async getAuthorizeConfig() { + if (this.authorizeConfig) { + return this.authorizeConfig; + } + + const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(this.authData); + const appConfigResolver = new AppConfigResolver(appConfigMetadataManager); + + const appConfig = await appConfigResolver.resolve({ metadata: this.appMetadata }); + const activeProviderResolver = new ActiveProviderResolver(appConfig); + const authorizeConfig = activeProviderResolver.resolve(this.channelSlug); + + this.authorizeConfig = authorizeConfig; + + return authorizeConfig; + } + + async createWebhookManagerService() { + const apiClient = createServerClient(this.authData.saleorApiUrl, this.authData.token); + + const webhookManagerService = new WebhookManagerService({ + authorizeConfig: await this.getAuthorizeConfig(), + apiClient, + }); + + return webhookManagerService; + } + + async registerAuthorizeWebhooks() { + const authorizeConfig = await this.getAuthorizeConfig(); + + if (authorizeConfig.webhooks.length > 0) { + this.logger.info("Webhooks already registered"); + return; + } + + const webhooksClient = new AuthorizeNetWebhooksClient(authorizeConfig); + + await webhooksClient.registerWebhook({ + name: `Saleor ${this.authData.domain} webhook`, + eventTypes: [], + status: "active", + url: `${this.authData.saleorApiUrl}/api/webhooks/authorize`, + }); + + this.logger.info("Webhook registered"); + } +} diff --git a/src/modules/authorize-net/authorize-net-error.ts b/src/modules/authorize-net/authorize-net-error.ts index 032d93b..a1b58d8 100644 --- a/src/modules/authorize-net/authorize-net-error.ts +++ b/src/modules/authorize-net/authorize-net-error.ts @@ -5,3 +5,7 @@ export const AuthorizeNetError = BaseError.subclass("AuthorizeNetError"); export const AuthorizeNetCreateTransactionError = AuthorizeNetError.subclass( "AuthorizeNetCreateTransactionError", ); + +export const AuthorizeNetInvalidWebhookSignatureError = AuthorizeNetError.subclass( + "AuthorizeNetInvalidWebhookSignatureError", +); diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts new file mode 100644 index 0000000..95d4e37 --- /dev/null +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts @@ -0,0 +1,89 @@ +import crypto from "crypto"; +import { z } from "zod"; +import { AuthorizeNetInvalidWebhookSignatureError } from "../authorize-net-error"; +import { createAuthorizeWebhooksFetch } from "./create-authorize-webhooks-fetch"; +import { + webhookSchema, + type AuthorizeProviderConfig, +} from "@/modules/authorize-net/authorize-net-config"; + +type AuthorizeNetNewWebhookParams = z.infer; + +const webhookResponseSchema = z + .object({ + _links: z.object({ + self: z.object({ + href: z.string(), + }), + }), + webhookId: z.string(), + }) + .and(webhookSchema); + +const listWebhooksResponseSchema = z.array(webhookResponseSchema); + +/** + * @description Authorize.net has a separate API for registering webhooks. + * @see AuthorizeNetClient for managing transactions etc. + */ +export class AuthorizeNetWebhooksClient { + private fetch: ReturnType; + private authorizeSignature = "X-ANET-Signature"; + + constructor(private config: AuthorizeProviderConfig.FullShape) { + this.fetch = createAuthorizeWebhooksFetch(config); + } + + /** + * @see https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification + */ + private async verifyAuthorizeWebhook(response: Response) { + const headers = response.headers; + const xAnetSignature = headers.get(this.authorizeSignature); + + if (!xAnetSignature) { + throw new AuthorizeNetInvalidWebhookSignatureError( + `Missing ${this.authorizeSignature} header`, + ); + } + + const body = await response.text(); + const hash = crypto + .createHmac("sha512", this.config.signatureKey) + .update(body) + .digest("base64"); + + const validSignature = `sha512=${hash}`; + + if (validSignature !== xAnetSignature) { + throw new AuthorizeNetInvalidWebhookSignatureError("Invalid signature"); + } + } + + async registerWebhook(params: AuthorizeNetNewWebhookParams) { + const response = await this.fetch({ + method: "POST", + body: params, + }); + + await this.verifyAuthorizeWebhook(response); + + const result = await response.json(); + const parsedResult = webhookResponseSchema.parse(result); + + return parsedResult; + } + + async listWebhooks() { + const response = await this.fetch({ + method: "GET", + }); + + await this.verifyAuthorizeWebhook(response); + + const result = await response.json(); + const parsedResult = listWebhooksResponseSchema.parse(result); + + return parsedResult; + } +} diff --git a/src/modules/webhooks/webhook-manager-service.ts b/src/modules/webhooks/webhook-manager-service.ts index 17fe7be..f8fa9ad 100644 --- a/src/modules/webhooks/webhook-manager-service.ts +++ b/src/modules/webhooks/webhook-manager-service.ts @@ -1,28 +1,21 @@ import { type Client } from "urql"; -import { type AuthData } from "@saleor/app-sdk/APL"; import { type AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; -import { ActiveProviderResolver } from "../configuration/active-provider-resolver"; -import { AppConfigMetadataManager } from "../configuration/app-config-metadata-manager"; -import { AppConfigResolver } from "../configuration/app-config-resolver"; +import { TransactionCancelationRequestedService } from "./transaction-cancelation-requested"; import { TransactionInitializeSessionService } from "./transaction-initialize-session"; import { TransactionProcessSessionService } from "./transaction-process-session"; -import { TransactionCancelationRequestedService } from "./transaction-cancelation-requested"; import { TransactionRefundRequestedService } from "./transaction-refund-requested"; import { type TransactionInitializeSessionResponse } from "@/schemas/TransactionInitializeSession/TransactionInitializeSessionResponse.mjs"; +import { type TransactionCancelationRequestedResponse } from "@/schemas/TransactionCancelationRequested/TransactionCancelationRequestedResponse.mjs"; +import { type TransactionProcessSessionResponse } from "@/schemas/TransactionProcessSession/TransactionProcessSessionResponse.mjs"; +import { type TransactionRefundRequestedResponse } from "@/schemas/TransactionRefundRequested/TransactionRefundRequestedResponse.mjs"; import { - type TransactionRefundRequestedEventFragment, - type MetadataItem, type TransactionCancelationRequestedEventFragment, type TransactionInitializeSessionEventFragment, type TransactionProcessSessionEventFragment, + type TransactionRefundRequestedEventFragment, } from "generated/graphql"; -import { type TransactionProcessSessionResponse } from "@/schemas/TransactionProcessSession/TransactionProcessSessionResponse.mjs"; -import { type TransactionCancelationRequestedResponse } from "@/schemas/TransactionCancelationRequested/TransactionCancelationRequestedResponse.mjs"; -import { logger } from "@/lib/logger"; -import { createServerClient } from "@/lib/create-graphq-client"; -import { type TransactionRefundRequestedResponse } from "@/schemas/TransactionRefundRequested/TransactionRefundRequestedResponse.mjs"; export interface PaymentsWebhooks { transactionInitializeSession: ( @@ -94,36 +87,3 @@ export class WebhookManagerService implements PaymentsWebhooks { return service.execute(payload); } } - -/** - * 1. Resolve appConfig from either webhook app metadata or environment variables. - * 2. Resolve active provider config from appConfig and channel slug. - * 3. Return webhook manager service created with the active provider config. - */ -export async function getWebhookManagerServiceFromCtx({ - appMetadata, - channelSlug, - authData, -}: { - appMetadata: readonly MetadataItem[]; - channelSlug: string; - authData: AuthData; -}) { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const appConfigResolver = new AppConfigResolver(appConfigMetadataManager); - - const appConfig = await appConfigResolver.resolve({ metadata: appMetadata }); - const activeProviderResolver = new ActiveProviderResolver(appConfig); - const authorizeConfig = activeProviderResolver.resolve(channelSlug); - - const apiClient = createServerClient(authData.saleorApiUrl, authData.token); - - logger.trace(`Found authorizeConfig for channel ${channelSlug}`); - - const webhookManagerService = new WebhookManagerService({ - authorizeConfig, - apiClient, - }); - - return webhookManagerService; -} diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 7404b09..23d2182 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -3,13 +3,14 @@ import * as Sentry from "@sentry/nextjs"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionCancelationRequestedError } from "@/modules/webhooks/transaction-cancelation-requested"; -import { getWebhookManagerServiceFromCtx } from "@/modules/webhooks/webhook-manager-service"; + import { saleorApp } from "@/saleor-app"; import { type TransactionCancelationRequestedResponse } from "@/schemas/TransactionCancelationRequested/TransactionCancelationRequestedResponse.mjs"; import { UntypedTransactionCancelationRequestedDocument, type TransactionCancelationRequestedEventFragment, } from "generated/graphql"; +import { AppInitializer } from "@/app-initializer"; export const config = { api: { @@ -40,12 +41,16 @@ export default transactionCancelationRequestedSyncWebhook.createHandler(async (r logger.debug({ payload: ctx.payload }, "handler called"); try { - const webhookManagerService = await getWebhookManagerServiceFromCtx({ + const appInitializer = new AppInitializer({ appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", authData: ctx.authData, + channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", }); + const webhookManagerService = await appInitializer.createWebhookManagerService(); + + await appInitializer.registerAuthorizeWebhooks(); + const response = await webhookManagerService.transactionCancelationRequested(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.info({ response }, "Responding with:"); diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index c52b4a5..4526b36 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -1,9 +1,9 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; +import { AppInitializer } from "@/app-initializer"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionInitializeError } from "@/modules/webhooks/transaction-initialize-session"; -import { getWebhookManagerServiceFromCtx } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionInitializeSessionResponse } from "@/schemas/TransactionInitializeSession/TransactionInitializeSessionResponse.mjs"; import { @@ -44,12 +44,16 @@ export default transactionInitializeSessionSyncWebhook.createHandler(async (req, logger.info({ action: ctx.payload.action }, "called with:"); try { - const webhookManagerService = await getWebhookManagerServiceFromCtx({ + const appInitializer = new AppInitializer({ appMetadata: ctx.payload.recipient?.privateMetadata ?? [], channelSlug: ctx.payload.sourceObject.channel.slug, authData: ctx.authData, }); + const webhookManagerService = await appInitializer.createWebhookManagerService(); + + await appInitializer.registerAuthorizeWebhooks(); + const response = await webhookManagerService.transactionInitializeSession(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index d2907eb..60bbe4e 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,9 +1,9 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; +import { AppInitializer } from "@/app-initializer"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionProcessError } from "@/modules/webhooks/transaction-process-session"; -import { getWebhookManagerServiceFromCtx } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionProcessSessionResponse } from "@/schemas/TransactionProcessSession/TransactionProcessSessionResponse.mjs"; import { @@ -52,12 +52,16 @@ export default transactionProcessSessionSyncWebhook.createHandler(async (req, re ); try { - const webhookManagerService = await getWebhookManagerServiceFromCtx({ + const appInitializer = new AppInitializer({ appMetadata: ctx.payload.recipient?.privateMetadata ?? [], channelSlug: ctx.payload.sourceObject.channel.slug, authData: ctx.authData, }); + const webhookManagerService = await appInitializer.createWebhookManagerService(); + + await appInitializer.registerAuthorizeWebhooks(); + const response = await webhookManagerService.transactionProcessSession(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index 59f117f..79273e5 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -3,7 +3,8 @@ import * as Sentry from "@sentry/nextjs"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; -import { getWebhookManagerServiceFromCtx } from "@/modules/webhooks/webhook-manager-service"; + +import { AppInitializer } from "@/app-initializer"; import { saleorApp } from "@/saleor-app"; import { type TransactionRefundRequestedResponse } from "@/schemas/TransactionRefundRequested/TransactionRefundRequestedResponse.mjs"; import { @@ -40,12 +41,16 @@ export default transactionRefundRequestedSyncWebhook.createHandler(async (req, r logger.debug({ payload: ctx.payload }, "handler called"); try { - const webhookManagerService = await getWebhookManagerServiceFromCtx({ + const appInitializer = new AppInitializer({ appMetadata: ctx.payload.recipient?.privateMetadata ?? [], channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", authData: ctx.authData, }); + const webhookManagerService = await appInitializer.createWebhookManagerService(); + + await appInitializer.registerAuthorizeWebhooks(); + const response = await webhookManagerService.transactionRefundRequested(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.info({ response }, "Responding with:"); From bb503ba2a2464ec7b66567d32e8b0266d166b22a Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 18 Dec 2023 12:25:20 +0100 Subject: [PATCH 07/33] refactor: :recycle: break up app-initializer --- src/app-initializer.ts | 85 ------------------ src/authorize-provider-resolver.ts | 24 ++++++ src/authorize-webhook-initializer.ts | 41 +++++++++ .../authorize-net/authorize-net-config.ts | 11 ++- .../authorize-net-webhooks-client.ts | 2 +- .../active-provider-resolver.test.ts | 4 +- .../configuration/app-config-resolver.test.ts | 2 +- .../configuration/app-configurator.test.ts | 4 +- .../provider/provider-configurator.test.ts | 30 +++---- .../webhooks/webhook-manager-service.ts | 18 ++++ .../transaction-cancelation-requested.ts | 62 +++++++------ .../transaction-initialize-session.ts | 70 ++++++++------- .../webhooks/transaction-process-session.ts | 86 +++++++++++-------- .../webhooks/transaction-refund-requested.ts | 62 +++++++------ 14 files changed, 273 insertions(+), 228 deletions(-) delete mode 100644 src/app-initializer.ts create mode 100644 src/authorize-provider-resolver.ts create mode 100644 src/authorize-webhook-initializer.ts diff --git a/src/app-initializer.ts b/src/app-initializer.ts deleted file mode 100644 index 7053e40..0000000 --- a/src/app-initializer.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { type AuthData } from "@saleor/app-sdk/APL"; -import { type AuthorizeProviderConfig } from "./modules/authorize-net/authorize-net-config"; -import { ActiveProviderResolver } from "./modules/configuration/active-provider-resolver"; -import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; -import { AppConfigResolver } from "./modules/configuration/app-config-resolver"; -import { AuthorizeNetWebhooksClient } from "./modules/authorize-net/webhooks-client/authorize-net-webhooks-client"; -import { WebhookManagerService } from "./modules/webhooks/webhook-manager-service"; -import { createServerClient } from "@/lib/create-graphq-client"; -import { type MetadataItem } from "generated/graphql"; -import { createLogger } from "@/lib/logger"; - -/** - * @description This class is used to get the Authorize.net configuration for the app and to register webhooks. - */ -export class AppInitializer { - private appMetadata: readonly MetadataItem[]; - private authData: AuthData; - private channelSlug: string; - private authorizeConfig: AuthorizeProviderConfig.FullShape | null = null; - private logger = createLogger({ - name: "AppInitializer", - }); - - constructor({ - appMetadata, - authData, - channelSlug, - }: { - appMetadata: readonly MetadataItem[]; - authData: AuthData; - channelSlug: string; - }) { - this.appMetadata = appMetadata; - this.authData = authData; - this.channelSlug = channelSlug; - } - - private async getAuthorizeConfig() { - if (this.authorizeConfig) { - return this.authorizeConfig; - } - - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(this.authData); - const appConfigResolver = new AppConfigResolver(appConfigMetadataManager); - - const appConfig = await appConfigResolver.resolve({ metadata: this.appMetadata }); - const activeProviderResolver = new ActiveProviderResolver(appConfig); - const authorizeConfig = activeProviderResolver.resolve(this.channelSlug); - - this.authorizeConfig = authorizeConfig; - - return authorizeConfig; - } - - async createWebhookManagerService() { - const apiClient = createServerClient(this.authData.saleorApiUrl, this.authData.token); - - const webhookManagerService = new WebhookManagerService({ - authorizeConfig: await this.getAuthorizeConfig(), - apiClient, - }); - - return webhookManagerService; - } - - async registerAuthorizeWebhooks() { - const authorizeConfig = await this.getAuthorizeConfig(); - - if (authorizeConfig.webhooks.length > 0) { - this.logger.info("Webhooks already registered"); - return; - } - - const webhooksClient = new AuthorizeNetWebhooksClient(authorizeConfig); - - await webhooksClient.registerWebhook({ - name: `Saleor ${this.authData.domain} webhook`, - eventTypes: [], - status: "active", - url: `${this.authData.saleorApiUrl}/api/webhooks/authorize`, - }); - - this.logger.info("Webhook registered"); - } -} diff --git a/src/authorize-provider-resolver.ts b/src/authorize-provider-resolver.ts new file mode 100644 index 0000000..e58f59b --- /dev/null +++ b/src/authorize-provider-resolver.ts @@ -0,0 +1,24 @@ +import { type AuthData } from "@saleor/app-sdk/APL"; +import { ActiveProviderResolver } from "./modules/configuration/active-provider-resolver"; +import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; +import { AppConfigResolver } from "./modules/configuration/app-config-resolver"; +import { type MetadataItem } from "generated/graphql"; + +export async function resolveAuthorizeConfig({ + authData, + appMetadata, + channelSlug, +}: { + authData: AuthData; + appMetadata: readonly MetadataItem[]; + channelSlug: string; +}) { + const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); + const appConfigResolver = new AppConfigResolver(appConfigMetadataManager); + + const appConfig = await appConfigResolver.resolve({ metadata: appMetadata }); + const activeProviderResolver = new ActiveProviderResolver(appConfig); + const authorizeConfig = activeProviderResolver.resolve(channelSlug); + + return authorizeConfig; +} diff --git a/src/authorize-webhook-initializer.ts b/src/authorize-webhook-initializer.ts new file mode 100644 index 0000000..65597f9 --- /dev/null +++ b/src/authorize-webhook-initializer.ts @@ -0,0 +1,41 @@ +import { type AuthData } from "@saleor/app-sdk/APL"; +import { type AuthorizeProviderConfig } from "./modules/authorize-net/authorize-net-config"; +import { + AuthorizeNetWebhooksClient, + type AuthorizeNetNewWebhookParams, +} from "./modules/authorize-net/webhooks-client/authorize-net-webhooks-client"; +import { createLogger } from "@/lib/logger"; + +export async function initializeAuthorizeWebhook({ + authData, + authorizeConfig, +}: { + authData: AuthData; + authorizeConfig: AuthorizeProviderConfig.FullShape; +}) { + const logger = createLogger({ + name: "AppWebhookInitializer", + }); + + /** + * @todo update app config metadata with webhook + */ + if (authorizeConfig.webhook) { + logger.info("Webhook already registered"); + return; + } + + logger.debug("Registering webhook..."); + + const webhooksClient = new AuthorizeNetWebhooksClient(authorizeConfig); + const webhookParams: AuthorizeNetNewWebhookParams = { + name: `Saleor ${authData.domain} webhook`, + eventTypes: ["net.authorize.payment.authcapture.created"], + status: "active", + url: `${authData.saleorApiUrl}/api/webhooks/authorize`, + }; + + await webhooksClient.registerWebhook(webhookParams); + + logger.info("Webhook registered"); +} diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index 9bffda8..2c0a0cc 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -2,7 +2,14 @@ import { z } from "zod"; export const authorizeEnvironmentSchema = z.enum(["sandbox", "production"]); -const authorizeNetEventSchema = z.enum(["net.authorize.payment.authcapture.created"]); // todo: add more +const authorizeNetEventSchema = z.enum([ + "net.authorize.payment.authorization.created", + "net.authorize.payment.authcapture.created", + "net.authorize.payment.capture.created", + "net.authorize.payment.refund.created", + "net.authorize.payment.priorAuthCapture.created", + "net.authorize.payment.void.create", +]); export const webhookSchema = z.object({ name: z.string(), @@ -17,7 +24,7 @@ const inputSchema = z.object({ transactionKey: z.string().min(1), signatureKey: z.string().min(1), environment: authorizeEnvironmentSchema, - webhooks: z.array(webhookSchema), + webhook: webhookSchema.nullable(), }); const fullSchema = inputSchema.extend({ diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts index 95d4e37..0978647 100644 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts @@ -7,7 +7,7 @@ import { type AuthorizeProviderConfig, } from "@/modules/authorize-net/authorize-net-config"; -type AuthorizeNetNewWebhookParams = z.infer; +export type AuthorizeNetNewWebhookParams = z.infer; const webhookResponseSchema = z .object({ diff --git a/src/modules/configuration/active-provider-resolver.test.ts b/src/modules/configuration/active-provider-resolver.test.ts index cb10abb..31b4aa8 100644 --- a/src/modules/configuration/active-provider-resolver.test.ts +++ b/src/modules/configuration/active-provider-resolver.test.ts @@ -14,7 +14,7 @@ describe("ActiveProviderResolver", () => { transactionKey: "transactionKey1", environment: "sandbox", signatureKey: "signatureKey1", - webhooks: [], + webhook: null, }, { id: "provider2", @@ -23,7 +23,7 @@ describe("ActiveProviderResolver", () => { transactionKey: "transactionKey2", environment: "sandbox", signatureKey: "signatureKey2", - webhooks: [], + webhook: null, }, ], connections: [ diff --git a/src/modules/configuration/app-config-resolver.test.ts b/src/modules/configuration/app-config-resolver.test.ts index 8063492..0cae572 100644 --- a/src/modules/configuration/app-config-resolver.test.ts +++ b/src/modules/configuration/app-config-resolver.test.ts @@ -20,7 +20,7 @@ const appConfig: AppConfig.Shape = { publicClientKey: "publicClientKey1", transactionKey: "transactionKey1", signatureKey: "signatureKey1", - webhooks: [], + webhook: null, }, ], }; diff --git a/src/modules/configuration/app-configurator.test.ts b/src/modules/configuration/app-configurator.test.ts index 66c5496..6532f26 100644 --- a/src/modules/configuration/app-configurator.test.ts +++ b/src/modules/configuration/app-configurator.test.ts @@ -12,7 +12,7 @@ describe("AppConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }); const parsedConfig = JSON.parse(configurator.serialize()); @@ -25,7 +25,7 @@ describe("AppConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", - webhooks: [], + webhook: null, }, ], connections: [], diff --git a/src/modules/provider/provider-configurator.test.ts b/src/modules/provider/provider-configurator.test.ts index a1bfcb7..3dc902f 100644 --- a/src/modules/provider/provider-configurator.test.ts +++ b/src/modules/provider/provider-configurator.test.ts @@ -19,7 +19,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, { id: "2", @@ -28,7 +28,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", - webhooks: [], + webhook: null, }, ]); @@ -40,7 +40,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, { id: "2", @@ -49,7 +49,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", - webhooks: [], + webhook: null, }, ]); }); @@ -65,7 +65,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, ]); @@ -76,7 +76,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }); }); @@ -89,7 +89,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, ]); @@ -107,7 +107,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }); expect(configurator.getProviders()).toEqual([ @@ -134,7 +134,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }); expect(configurator.getProviders()).toEqual(rootData); @@ -148,7 +148,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, ]); @@ -159,7 +159,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "new-public-client-key", signatureKey: "new-signature-key", - webhooks: [], + webhook: null, }); expect(configurator.getProviders()).toEqual([ @@ -185,7 +185,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, { id: "2", @@ -194,7 +194,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", - webhooks: [], + webhook: null, }, ]); @@ -221,7 +221,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key", - webhooks: [], + webhook: null, }, { id: "2", @@ -230,7 +230,7 @@ describe("ProvidersConfigurator", () => { environment: "sandbox", publicClientKey: "public-client-key", signatureKey: "signature-key-2", - webhooks: [], + webhook: null, }, ]); diff --git a/src/modules/webhooks/webhook-manager-service.ts b/src/modules/webhooks/webhook-manager-service.ts index f8fa9ad..0826a63 100644 --- a/src/modules/webhooks/webhook-manager-service.ts +++ b/src/modules/webhooks/webhook-manager-service.ts @@ -1,4 +1,5 @@ import { type Client } from "urql"; +import { type AuthData } from "@saleor/app-sdk/APL"; import { type AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; import { TransactionCancelationRequestedService } from "./transaction-cancelation-requested"; @@ -16,6 +17,7 @@ import { type TransactionProcessSessionEventFragment, type TransactionRefundRequestedEventFragment, } from "generated/graphql"; +import { createServerClient } from "@/lib/create-graphq-client"; export interface PaymentsWebhooks { transactionInitializeSession: ( @@ -87,3 +89,19 @@ export class WebhookManagerService implements PaymentsWebhooks { return service.execute(payload); } } + +export async function createWebhookManagerService({ + authData, + authorizeConfig, +}: { + authData: AuthData; + authorizeConfig: AuthorizeProviderConfig.FullShape; +}) { + const apiClient = createServerClient(authData.saleorApiUrl, authData.token); + const webhookManagerService = new WebhookManagerService({ + authorizeConfig, + apiClient, + }); + + return webhookManagerService; +} diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 23d2182..7c9e8be 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -4,13 +4,15 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionCancelationRequestedError } from "@/modules/webhooks/transaction-cancelation-requested"; +import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; +import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; +import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionCancelationRequestedResponse } from "@/schemas/TransactionCancelationRequested/TransactionCancelationRequestedResponse.mjs"; import { UntypedTransactionCancelationRequestedDocument, type TransactionCancelationRequestedEventFragment, } from "generated/graphql"; -import { AppInitializer } from "@/app-initializer"; export const config = { api: { @@ -36,33 +38,41 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder { - const responseBuilder = new WebhookResponseBuilder(res); - logger.debug({ payload: ctx.payload }, "handler called"); +export default transactionCancelationRequestedSyncWebhook.createHandler( + async (req, res, { authData, ...ctx }) => { + const responseBuilder = new WebhookResponseBuilder(res); + logger.debug({ payload: ctx.payload }, "handler called"); - try { - const appInitializer = new AppInitializer({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - authData: ctx.authData, - channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", - }); + try { + const authorizeConfig = await resolveAuthorizeConfig({ + authData, + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], + channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", + }); - const webhookManagerService = await appInitializer.createWebhookManagerService(); + await initializeAuthorizeWebhook({ + authData, + authorizeConfig, + }); - await appInitializer.registerAuthorizeWebhooks(); + const webhookManagerService = await createWebhookManagerService({ + authData, + authorizeConfig, + }); - const response = await webhookManagerService.transactionCancelationRequested(ctx.payload); - // eslint-disable-next-line @saleor/saleor-app/logger-leak - logger.info({ response }, "Responding with:"); - return responseBuilder.ok(response); - } catch (error) { - Sentry.captureException(error); + const response = await webhookManagerService.transactionCancelationRequested(ctx.payload); + // eslint-disable-next-line @saleor/saleor-app/logger-leak + logger.info({ response }, "Responding with:"); + return responseBuilder.ok(response); + } catch (error) { + Sentry.captureException(error); - const normalizedError = TransactionCancelationRequestedError.normalize(error); - return responseBuilder.ok({ - result: "CANCEL_FAILURE", - pspReference: "", // todo: add - message: normalizedError.message, - }); - } -}); + const normalizedError = TransactionCancelationRequestedError.normalize(error); + return responseBuilder.ok({ + result: "CANCEL_FAILURE", + pspReference: "", // todo: add + message: normalizedError.message, + }); + } + }, +); diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index 4526b36..337a0cf 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -1,9 +1,11 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AppInitializer } from "@/app-initializer"; +import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; +import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionInitializeError } from "@/modules/webhooks/transaction-initialize-session"; +import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionInitializeSessionResponse } from "@/schemas/TransactionInitializeSession/TransactionInitializeSessionResponse.mjs"; import { @@ -39,39 +41,47 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder { - const responseBuilder = new WebhookResponseBuilder(res); - logger.info({ action: ctx.payload.action }, "called with:"); +export default transactionInitializeSessionSyncWebhook.createHandler( + async (req, res, { authData, ...ctx }) => { + const responseBuilder = new WebhookResponseBuilder(res); + logger.info({ action: ctx.payload.action }, "called with:"); - try { - const appInitializer = new AppInitializer({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.sourceObject.channel.slug, - authData: ctx.authData, - }); + try { + const authorizeConfig = await resolveAuthorizeConfig({ + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], + channelSlug: ctx.payload.sourceObject.channel.slug, + authData, + }); - const webhookManagerService = await appInitializer.createWebhookManagerService(); + await initializeAuthorizeWebhook({ + authData, + authorizeConfig, + }); - await appInitializer.registerAuthorizeWebhooks(); + const webhookManagerService = await createWebhookManagerService({ + authData, + authorizeConfig, + }); - const response = await webhookManagerService.transactionInitializeSession(ctx.payload); + const response = await webhookManagerService.transactionInitializeSession(ctx.payload); - // eslint-disable-next-line @saleor/saleor-app/logger-leak - logger.info({ response }, "Responding with:"); - return responseBuilder.ok(response); - } catch (error) { - Sentry.captureException(error); + // eslint-disable-next-line @saleor/saleor-app/logger-leak + logger.info({ response }, "Responding with:"); + return responseBuilder.ok(response); + } catch (error) { + Sentry.captureException(error); - const normalizedError = TransactionInitializeError.normalize(error); - return responseBuilder.ok({ - amount: 0, // 0 or real amount? - result: "AUTHORIZATION_FAILURE", - message: "Failure", - data: { - error: { - message: normalizedError.message, + const normalizedError = TransactionInitializeError.normalize(error); + return responseBuilder.ok({ + amount: 0, // 0 or real amount? + result: "AUTHORIZATION_FAILURE", + message: "Failure", + data: { + error: { + message: normalizedError.message, + }, }, - }, - }); - } -}); + }); + } + }, +); diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index 60bbe4e..2f394db 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,9 +1,11 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AppInitializer } from "@/app-initializer"; +import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; +import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionProcessError } from "@/modules/webhooks/transaction-process-session"; +import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionProcessSessionResponse } from "@/schemas/TransactionProcessSession/TransactionProcessSessionResponse.mjs"; import { @@ -39,47 +41,55 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder { - const responseBuilder = new WebhookResponseBuilder(res); - // todo: add more extensive logs - logger.debug( - { - action: ctx.payload.action, - channelSlug: ctx.payload.sourceObject.channel.slug, - transaction: ctx.payload.transaction, - }, - "Handler called", - ); +export default transactionProcessSessionSyncWebhook.createHandler( + async (req, res, { authData, ...ctx }) => { + const responseBuilder = new WebhookResponseBuilder(res); + // todo: add more extensive logs + logger.debug( + { + action: ctx.payload.action, + channelSlug: ctx.payload.sourceObject.channel.slug, + transaction: ctx.payload.transaction, + }, + "Handler called", + ); - try { - const appInitializer = new AppInitializer({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.sourceObject.channel.slug, - authData: ctx.authData, - }); + try { + const authorizeConfig = await resolveAuthorizeConfig({ + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], + channelSlug: ctx.payload.sourceObject.channel.slug, + authData, + }); - const webhookManagerService = await appInitializer.createWebhookManagerService(); + await initializeAuthorizeWebhook({ + authData, + authorizeConfig, + }); - await appInitializer.registerAuthorizeWebhooks(); + const webhookManagerService = await createWebhookManagerService({ + authData, + authorizeConfig, + }); - const response = await webhookManagerService.transactionProcessSession(ctx.payload); + const response = await webhookManagerService.transactionProcessSession(ctx.payload); - // eslint-disable-next-line @saleor/saleor-app/logger-leak - logger.info({ response }, "Responding with:"); - return responseBuilder.ok(response); - } catch (error) { - Sentry.captureException(error); + // eslint-disable-next-line @saleor/saleor-app/logger-leak + logger.info({ response }, "Responding with:"); + return responseBuilder.ok(response); + } catch (error) { + Sentry.captureException(error); - const normalizedError = TransactionProcessError.normalize(error); - return responseBuilder.ok({ - amount: ctx.payload.action.amount, - result: "AUTHORIZATION_FAILURE", - message: "Failure", - data: { - error: { - message: normalizedError.message, + const normalizedError = TransactionProcessError.normalize(error); + return responseBuilder.ok({ + amount: ctx.payload.action.amount, + result: "AUTHORIZATION_FAILURE", + message: "Failure", + data: { + error: { + message: normalizedError.message, + }, }, - }, - }); - } -}); + }); + } + }, +); diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index 79273e5..a159526 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -4,7 +4,9 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; -import { AppInitializer } from "@/app-initializer"; +import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; +import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; +import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionRefundRequestedResponse } from "@/schemas/TransactionRefundRequested/TransactionRefundRequestedResponse.mjs"; import { @@ -36,33 +38,41 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder { - const responseBuilder = new WebhookResponseBuilder(res); - logger.debug({ payload: ctx.payload }, "handler called"); +export default transactionRefundRequestedSyncWebhook.createHandler( + async (req, res, { authData, ...ctx }) => { + const responseBuilder = new WebhookResponseBuilder(res); + logger.debug({ payload: ctx.payload }, "handler called"); - try { - const appInitializer = new AppInitializer({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", - authData: ctx.authData, - }); + try { + const authorizeConfig = await resolveAuthorizeConfig({ + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], + channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", + authData, + }); - const webhookManagerService = await appInitializer.createWebhookManagerService(); + await initializeAuthorizeWebhook({ + authData, + authorizeConfig, + }); - await appInitializer.registerAuthorizeWebhooks(); + const webhookManagerService = await createWebhookManagerService({ + authData, + authorizeConfig, + }); - const response = await webhookManagerService.transactionRefundRequested(ctx.payload); - // eslint-disable-next-line @saleor/saleor-app/logger-leak - logger.info({ response }, "Responding with:"); - return responseBuilder.ok(response); - } catch (error) { - Sentry.captureException(error); + const response = await webhookManagerService.transactionRefundRequested(ctx.payload); + // eslint-disable-next-line @saleor/saleor-app/logger-leak + logger.info({ response }, "Responding with:"); + return responseBuilder.ok(response); + } catch (error) { + Sentry.captureException(error); - const normalizedError = TransactionRefundRequestedError.normalize(error); - return responseBuilder.ok({ - result: "REFUND_FAILURE", - pspReference: "", // todo: add - message: normalizedError.message, - }); - } -}); + const normalizedError = TransactionRefundRequestedError.normalize(error); + return responseBuilder.ok({ + result: "REFUND_FAILURE", + pspReference: "", // todo: add + message: normalizedError.message, + }); + } + }, +); From 3f8a7392f0620bca3cb804bd2154dac085ad2894 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 18 Dec 2023 13:02:05 +0100 Subject: [PATCH 08/33] Refactor webhook registration and resolve app configuration --- src/authorize-provider-resolver.ts | 24 ------ src/authorize-webhook-initializer.ts | 84 +++++++++++++------ .../authorize-net-webhooks-client.ts | 4 +- .../active-provider-resolver.test.ts | 74 ---------------- .../configuration/active-provider-resolver.ts | 40 --------- .../configuration/app-config-resolver.test.ts | 35 -------- .../configuration/app-config-resolver.ts | 22 ++++- .../authorize-config-resolver.ts | 28 +++++++ .../webhooks/webhook-manager-service.ts | 8 +- .../transaction-cancelation-requested.ts | 27 ++++-- .../transaction-initialize-session.ts | 29 ++++--- .../webhooks/transaction-process-session.ts | 32 ++++--- .../webhooks/transaction-refund-requested.ts | 29 ++++--- 13 files changed, 189 insertions(+), 247 deletions(-) delete mode 100644 src/authorize-provider-resolver.ts delete mode 100644 src/modules/configuration/active-provider-resolver.test.ts delete mode 100644 src/modules/configuration/active-provider-resolver.ts delete mode 100644 src/modules/configuration/app-config-resolver.test.ts create mode 100644 src/modules/configuration/authorize-config-resolver.ts diff --git a/src/authorize-provider-resolver.ts b/src/authorize-provider-resolver.ts deleted file mode 100644 index e58f59b..0000000 --- a/src/authorize-provider-resolver.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { type AuthData } from "@saleor/app-sdk/APL"; -import { ActiveProviderResolver } from "./modules/configuration/active-provider-resolver"; -import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; -import { AppConfigResolver } from "./modules/configuration/app-config-resolver"; -import { type MetadataItem } from "generated/graphql"; - -export async function resolveAuthorizeConfig({ - authData, - appMetadata, - channelSlug, -}: { - authData: AuthData; - appMetadata: readonly MetadataItem[]; - channelSlug: string; -}) { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const appConfigResolver = new AppConfigResolver(appConfigMetadataManager); - - const appConfig = await appConfigResolver.resolve({ metadata: appMetadata }); - const activeProviderResolver = new ActiveProviderResolver(appConfig); - const authorizeConfig = activeProviderResolver.resolve(channelSlug); - - return authorizeConfig; -} diff --git a/src/authorize-webhook-initializer.ts b/src/authorize-webhook-initializer.ts index 65597f9..81df5ed 100644 --- a/src/authorize-webhook-initializer.ts +++ b/src/authorize-webhook-initializer.ts @@ -2,40 +2,74 @@ import { type AuthData } from "@saleor/app-sdk/APL"; import { type AuthorizeProviderConfig } from "./modules/authorize-net/authorize-net-config"; import { AuthorizeNetWebhooksClient, - type AuthorizeNetNewWebhookParams, + type AuthorizeNetWebhook, } from "./modules/authorize-net/webhooks-client/authorize-net-webhooks-client"; +import { AppConfigurator, type AppConfig } from "./modules/configuration/app-configurator"; +import { resolveAuthorizeConfigFromAppConfig } from "./modules/configuration/authorize-config-resolver"; +import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; import { createLogger } from "@/lib/logger"; -export async function initializeAuthorizeWebhook({ - authData, - authorizeConfig, -}: { - authData: AuthData; - authorizeConfig: AuthorizeProviderConfig.FullShape; -}) { - const logger = createLogger({ +export class AuthorizeWebhookManager { + private authData: AuthData; + private appConfig: AppConfig.Shape; + + private authorizeConfig: AuthorizeProviderConfig.FullShape; + + private logger = createLogger({ name: "AppWebhookInitializer", }); - /** - * @todo update app config metadata with webhook - */ - if (authorizeConfig.webhook) { - logger.info("Webhook already registered"); - return; + constructor({ + authData, + appConfig, + channelSlug, + }: { + authData: AuthData; + appConfig: AppConfig.Shape; + channelSlug: string; + }) { + this.authData = authData; + this.appConfig = appConfig; + + this.authorizeConfig = resolveAuthorizeConfigFromAppConfig({ + appConfig, + channelSlug, + }); + } + + private async updateMetadataWithWebhook(webhook: AuthorizeNetWebhook) { + const nextConfig: AuthorizeProviderConfig.FullShape = { + ...this.authorizeConfig, + webhook, + }; + + const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(this.authData); + const appConfigurator = new AppConfigurator(this.appConfig); + + appConfigurator.providers.updateProvider(nextConfig); + await appConfigMetadataManager.set(appConfigurator); } - logger.debug("Registering webhook..."); + public async register() { + if (this.authorizeConfig.webhook) { + this.logger.info("Webhook already registered"); + return; + } + + this.logger.debug("Registering webhook..."); - const webhooksClient = new AuthorizeNetWebhooksClient(authorizeConfig); - const webhookParams: AuthorizeNetNewWebhookParams = { - name: `Saleor ${authData.domain} webhook`, - eventTypes: ["net.authorize.payment.authcapture.created"], - status: "active", - url: `${authData.saleorApiUrl}/api/webhooks/authorize`, - }; + const webhooksClient = new AuthorizeNetWebhooksClient(this.authorizeConfig); + const webhookParams: AuthorizeNetWebhook = { + name: `Saleor ${this.authData.domain} webhook`, + eventTypes: ["net.authorize.payment.authcapture.created"], + status: "active", + url: `${this.authData.saleorApiUrl}/api/webhooks/authorize`, + }; - await webhooksClient.registerWebhook(webhookParams); + const webhook = await webhooksClient.registerWebhook(webhookParams); - logger.info("Webhook registered"); + await this.updateMetadataWithWebhook(webhook); + + this.logger.info("Webhook registered"); + } } diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts index 0978647..1e4cbf7 100644 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts @@ -7,7 +7,7 @@ import { type AuthorizeProviderConfig, } from "@/modules/authorize-net/authorize-net-config"; -export type AuthorizeNetNewWebhookParams = z.infer; +export type AuthorizeNetWebhook = z.infer; const webhookResponseSchema = z .object({ @@ -60,7 +60,7 @@ export class AuthorizeNetWebhooksClient { } } - async registerWebhook(params: AuthorizeNetNewWebhookParams) { + async registerWebhook(params: AuthorizeNetWebhook) { const response = await this.fetch({ method: "POST", body: params, diff --git a/src/modules/configuration/active-provider-resolver.test.ts b/src/modules/configuration/active-provider-resolver.test.ts deleted file mode 100644 index 31b4aa8..0000000 --- a/src/modules/configuration/active-provider-resolver.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { ActiveProviderResolver } from "./active-provider-resolver"; -import { type AppConfig } from "./app-configurator"; - -describe("ActiveProviderResolver", () => { - describe("resolve", () => { - it("should return the provider for the given channel", () => { - const appConfig: AppConfig.Shape = { - providers: [ - { - id: "provider1", - apiLoginId: "apiLoginId1", - publicClientKey: "publicClientKey1", - transactionKey: "transactionKey1", - environment: "sandbox", - signatureKey: "signatureKey1", - webhook: null, - }, - { - id: "provider2", - apiLoginId: "apiLoginId2", - publicClientKey: "publicClientKey2", - transactionKey: "transactionKey2", - environment: "sandbox", - signatureKey: "signatureKey2", - webhook: null, - }, - ], - connections: [ - { - channelSlug: "channel1", - providerId: "provider1", - id: "connection1", - }, - { - channelSlug: "channel2", - providerId: "provider2", - id: "connection2", - }, - ], - }; - - const activeProviderResolver = new ActiveProviderResolver(appConfig); - - const provider = activeProviderResolver.resolve("channel1"); - - expect(provider).toEqual(appConfig.providers[0]); - }); - it("should throw an error if the connection is not found", () => { - const appConfig: AppConfig.Shape = { - connections: [], - providers: [], - }; - - const activeProviderResolver = new ActiveProviderResolver(appConfig); - expect(() => activeProviderResolver.resolve("channel1")).toThrow(); - }); - it("should throw an error if the provider is not found", () => { - const appConfig: AppConfig.Shape = { - connections: [ - { - channelSlug: "channel1", - providerId: "provider1", - id: "connection1", - }, - ], - providers: [], - }; - - const activeProviderResolver = new ActiveProviderResolver(appConfig); - expect(() => activeProviderResolver.resolve("channel1")).toThrow(); - }); - }); -}); diff --git a/src/modules/configuration/active-provider-resolver.ts b/src/modules/configuration/active-provider-resolver.ts deleted file mode 100644 index c39ad6d..0000000 --- a/src/modules/configuration/active-provider-resolver.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { NoChannelSlugFoundError, NoConnectionFoundError, NoProviderFoundError } from "@/errors"; -import { type ChannelConnection } from "@/modules/channel-connection/channel-connection.schema"; -import { type AppConfig } from "@/modules/configuration/app-configurator"; - -export class ActiveProviderResolver { - constructor(private appConfig: AppConfig.Shape) {} - - private resolveActiveConnection(channelSlug: string) { - const channel = this.appConfig.connections.find((c) => c.channelSlug === channelSlug); - - if (!channel) { - throw new NoConnectionFoundError(`Channel ${channelSlug} not found in the connections`); - } - - return channel; - } - - private resolveProviderForConnection(connection: ChannelConnection.FullShape) { - const provider = this.appConfig.providers.find((p) => p.id === connection.providerId); - - if (!provider) { - throw new NoProviderFoundError( - `Provider ${connection.providerId} not found in the providers`, - ); - } - - return provider; - } - - public resolve(channelSlug: string | null | undefined) { - if (!channelSlug) { - throw new NoChannelSlugFoundError(`Channel ${channelSlug} not found in the connections`); - } - - const connection = this.resolveActiveConnection(channelSlug); - const provider = this.resolveProviderForConnection(connection); - - return provider; - } -} diff --git a/src/modules/configuration/app-config-resolver.test.ts b/src/modules/configuration/app-config-resolver.test.ts deleted file mode 100644 index 0cae572..0000000 --- a/src/modules/configuration/app-config-resolver.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { encrypt } from "@saleor/app-sdk/settings-manager"; -import { describe } from "vitest"; -import { type AppConfig } from "./app-configurator"; -import { env } from "@/lib/env.mjs"; -import { type WebhookRecipientFragment } from "generated/graphql"; - -const appConfig: AppConfig.Shape = { - connections: [ - { - channelSlug: "channel1", - id: "connection1", - providerId: "provider1", - }, - ], - providers: [ - { - apiLoginId: "apiLoginId1", - environment: "sandbox", - id: "provider1", - publicClientKey: "publicClientKey1", - transactionKey: "transactionKey1", - signatureKey: "signatureKey1", - webhook: null, - }, - ], -}; - -const _metadata: WebhookRecipientFragment["privateMetadata"] = [ - { - key: "appConfig", - value: encrypt(JSON.stringify(appConfig), env.SECRET_KEY), - }, -]; - -describe.todo("AppConfigResolver"); diff --git a/src/modules/configuration/app-config-resolver.ts b/src/modules/configuration/app-config-resolver.ts index 747109a..65f077f 100644 --- a/src/modules/configuration/app-config-resolver.ts +++ b/src/modules/configuration/app-config-resolver.ts @@ -1,10 +1,11 @@ import { decrypt } from "@saleor/app-sdk/settings-manager"; -import { type AppConfigMetadataManager } from "./app-config-metadata-manager"; +import { type AuthData } from "@saleor/app-sdk/APL"; +import { AppConfigMetadataManager } from "./app-config-metadata-manager"; import { env } from "@/lib/env.mjs"; import { generateId } from "@/lib/generate-id"; import { createLogger, logger } from "@/lib/logger"; import { AppConfig, AppConfigurator } from "@/modules/configuration/app-configurator"; -import { type WebhookRecipientFragment } from "generated/graphql"; +import { type MetadataItem, type WebhookRecipientFragment } from "generated/graphql"; /** * This function looks for AppConfig in the webhook recipient metadata. If it's not found, it looks for it in the env. @@ -18,7 +19,7 @@ const defaultAppConfig: AppConfig.Shape = { providers: [], }; -export class AppConfigResolver { +class AppConfigResolver { private logger = createLogger({ name: "AppConfigResolver", }); @@ -92,3 +93,18 @@ export class AppConfigResolver { return envConfig; } } + +export async function resolveAppConfigFromCtx({ + authData, + appMetadata, +}: { + authData: AuthData; + appMetadata: readonly MetadataItem[]; +}) { + const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); + const appConfigResolver = new AppConfigResolver(appConfigMetadataManager); + + const appConfig = await appConfigResolver.resolve({ metadata: appMetadata }); + + return appConfig; +} diff --git a/src/modules/configuration/authorize-config-resolver.ts b/src/modules/configuration/authorize-config-resolver.ts new file mode 100644 index 0000000..0e0d1b3 --- /dev/null +++ b/src/modules/configuration/authorize-config-resolver.ts @@ -0,0 +1,28 @@ +import { NoChannelSlugFoundError, NoConnectionFoundError, NoProviderFoundError } from "@/errors"; +import { type AppConfig } from "@/modules/configuration/app-configurator"; + +export function resolveAuthorizeConfigFromAppConfig({ + appConfig, + channelSlug, +}: { + appConfig: AppConfig.Shape; + channelSlug: string | null | undefined; +}) { + if (!channelSlug) { + throw new NoChannelSlugFoundError(`Channel ${channelSlug} not found in the connections`); + } + + const channel = appConfig.connections.find((c) => c.channelSlug === channelSlug); + + if (!channel) { + throw new NoConnectionFoundError(`Channel ${channelSlug} not found in the connections`); + } + + const provider = appConfig.providers.find((p) => p.id === channel.providerId); + + if (!provider) { + throw new NoProviderFoundError(`Provider ${channel.providerId} not found in the providers`); + } + + return provider; +} diff --git a/src/modules/webhooks/webhook-manager-service.ts b/src/modules/webhooks/webhook-manager-service.ts index 0826a63..29236dc 100644 --- a/src/modules/webhooks/webhook-manager-service.ts +++ b/src/modules/webhooks/webhook-manager-service.ts @@ -31,7 +31,7 @@ export interface PaymentsWebhooks { ) => Promise; } -export class WebhookManagerService implements PaymentsWebhooks { +export class AppWebhookManager implements PaymentsWebhooks { private authorizeConfig: AuthorizeProviderConfig.FullShape; private apiClient: Client; @@ -90,7 +90,7 @@ export class WebhookManagerService implements PaymentsWebhooks { } } -export async function createWebhookManagerService({ +export async function createAppWebhookManager({ authData, authorizeConfig, }: { @@ -98,10 +98,10 @@ export async function createWebhookManagerService({ authorizeConfig: AuthorizeProviderConfig.FullShape; }) { const apiClient = createServerClient(authData.saleorApiUrl, authData.token); - const webhookManagerService = new WebhookManagerService({ + const appWebhookManager = new AppWebhookManager({ authorizeConfig, apiClient, }); - return webhookManagerService; + return appWebhookManager; } diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 7c9e8be..0b28243 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -4,15 +4,16 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionCancelationRequestedError } from "@/modules/webhooks/transaction-cancelation-requested"; -import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; -import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; -import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; +import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; +import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; +import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionCancelationRequestedResponse } from "@/schemas/TransactionCancelationRequested/TransactionCancelationRequestedResponse.mjs"; import { UntypedTransactionCancelationRequestedDocument, type TransactionCancelationRequestedEventFragment, } from "generated/graphql"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; export const config = { api: { @@ -44,23 +45,31 @@ export default transactionCancelationRequestedSyncWebhook.createHandler( logger.debug({ payload: ctx.payload }, "handler called"); try { - const authorizeConfig = await resolveAuthorizeConfig({ + const channelSlug = ctx.payload.transaction?.sourceObject?.channel.slug ?? ""; + const appConfig = await resolveAppConfigFromCtx({ authData, appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", }); - await initializeAuthorizeWebhook({ + const authorizeConfig = resolveAuthorizeConfigFromAppConfig({ + appConfig, + channelSlug, + }); + + const authorizeWebhookManager = new AuthorizeWebhookManager({ authData, - authorizeConfig, + appConfig, + channelSlug, }); - const webhookManagerService = await createWebhookManagerService({ + await authorizeWebhookManager.register(); + + const appWebhookManager = await createAppWebhookManager({ authData, authorizeConfig, }); - const response = await webhookManagerService.transactionCancelationRequested(ctx.payload); + const response = await appWebhookManager.transactionCancelationRequested(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.info({ response }, "Responding with:"); return responseBuilder.ok(response); diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index 337a0cf..6ef3781 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -1,11 +1,12 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; -import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; +import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; +import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; import { TransactionInitializeError } from "@/modules/webhooks/transaction-initialize-session"; -import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; +import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionInitializeSessionResponse } from "@/schemas/TransactionInitializeSession/TransactionInitializeSessionResponse.mjs"; import { @@ -47,23 +48,31 @@ export default transactionInitializeSessionSyncWebhook.createHandler( logger.info({ action: ctx.payload.action }, "called with:"); try { - const authorizeConfig = await resolveAuthorizeConfig({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.sourceObject.channel.slug, + const channelSlug = ctx.payload.sourceObject.channel.slug; + const appConfig = await resolveAppConfigFromCtx({ authData, + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], + }); + + const authorizeConfig = resolveAuthorizeConfigFromAppConfig({ + appConfig, + channelSlug, }); - await initializeAuthorizeWebhook({ + const authorizeWebhookManager = new AuthorizeWebhookManager({ authData, - authorizeConfig, + appConfig, + channelSlug, }); - const webhookManagerService = await createWebhookManagerService({ + await authorizeWebhookManager.register(); + + const appWebhookManager = await createAppWebhookManager({ authData, authorizeConfig, }); - const response = await webhookManagerService.transactionInitializeSession(ctx.payload); + const response = await appWebhookManager.transactionInitializeSession(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.info({ response }, "Responding with:"); diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index 2f394db..cfb4549 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,11 +1,12 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; -import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; +import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; +import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; import { TransactionProcessError } from "@/modules/webhooks/transaction-process-session"; -import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; +import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionProcessSessionResponse } from "@/schemas/TransactionProcessSession/TransactionProcessSessionResponse.mjs"; import { @@ -44,34 +45,43 @@ class WebhookResponseBuilder extends SynchronousWebhookResponseBuilder { const responseBuilder = new WebhookResponseBuilder(res); + const channelSlug = ctx.payload.sourceObject.channel.slug; + // todo: add more extensive logs logger.debug( { action: ctx.payload.action, - channelSlug: ctx.payload.sourceObject.channel.slug, + channelSlug, transaction: ctx.payload.transaction, }, "Handler called", ); try { - const authorizeConfig = await resolveAuthorizeConfig({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.sourceObject.channel.slug, + const appConfig = await resolveAppConfigFromCtx({ authData, + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], }); - await initializeAuthorizeWebhook({ + const authorizeConfig = resolveAuthorizeConfigFromAppConfig({ + appConfig, + channelSlug, + }); + + const authorizeWebhookManager = new AuthorizeWebhookManager({ authData, - authorizeConfig, + appConfig, + channelSlug, }); - const webhookManagerService = await createWebhookManagerService({ + await authorizeWebhookManager.register(); + + const appWebhookManager = await createAppWebhookManager({ authData, authorizeConfig, }); - const response = await webhookManagerService.transactionProcessSession(ctx.payload); + const response = await appWebhookManager.transactionProcessSession(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.info({ response }, "Responding with:"); diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index a159526..59d0d02 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -4,9 +4,10 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; -import { resolveAuthorizeConfig } from "@/authorize-provider-resolver"; -import { initializeAuthorizeWebhook } from "@/authorize-webhook-initializer"; -import { createWebhookManagerService } from "@/modules/webhooks/webhook-manager-service"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; +import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; +import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; +import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionRefundRequestedResponse } from "@/schemas/TransactionRefundRequested/TransactionRefundRequestedResponse.mjs"; import { @@ -44,23 +45,31 @@ export default transactionRefundRequestedSyncWebhook.createHandler( logger.debug({ payload: ctx.payload }, "handler called"); try { - const authorizeConfig = await resolveAuthorizeConfig({ - appMetadata: ctx.payload.recipient?.privateMetadata ?? [], - channelSlug: ctx.payload.transaction?.sourceObject?.channel.slug ?? "", + const channelSlug = ctx.payload.transaction?.sourceObject?.channel.slug ?? ""; + const appConfig = await resolveAppConfigFromCtx({ authData, + appMetadata: ctx.payload.recipient?.privateMetadata ?? [], + }); + + const authorizeConfig = resolveAuthorizeConfigFromAppConfig({ + channelSlug, + appConfig, }); - await initializeAuthorizeWebhook({ + const authorizeWebhookManager = new AuthorizeWebhookManager({ authData, - authorizeConfig, + appConfig, + channelSlug, }); - const webhookManagerService = await createWebhookManagerService({ + await authorizeWebhookManager.register(); + + const appWebhookManager = await createAppWebhookManager({ authData, authorizeConfig, }); - const response = await webhookManagerService.transactionRefundRequested(ctx.payload); + const response = await appWebhookManager.transactionRefundRequested(ctx.payload); // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.info({ response }, "Responding with:"); return responseBuilder.ok(response); From e70f5ccfebd5f5ba1e5e69a65a95b06335a16612 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 18 Dec 2023 13:27:00 +0100 Subject: [PATCH 09/33] Rename AuthorizeWebhookInitializer -> AuthorizeWebhookManager --- ...rize-webhook-initializer.ts => authorize-webhook-manager.ts} | 0 src/pages/api/webhooks/transaction-cancelation-requested.ts | 2 +- src/pages/api/webhooks/transaction-initialize-session.ts | 2 +- src/pages/api/webhooks/transaction-process-session.ts | 2 +- src/pages/api/webhooks/transaction-refund-requested.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/{authorize-webhook-initializer.ts => authorize-webhook-manager.ts} (100%) diff --git a/src/authorize-webhook-initializer.ts b/src/authorize-webhook-manager.ts similarity index 100% rename from src/authorize-webhook-initializer.ts rename to src/authorize-webhook-manager.ts diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 0b28243..7c779ff 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -13,7 +13,7 @@ import { UntypedTransactionCancelationRequestedDocument, type TransactionCancelationRequestedEventFragment, } from "generated/graphql"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; export const config = { api: { diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index 6ef3781..fc88e65 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -1,6 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index cfb4549..7e39413 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,6 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index 59d0d02..0cb4c53 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -4,7 +4,7 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-initializer"; +import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; From c54a4c7f284ba1502b0c41d54958c7c4a6eb7281 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 18 Dec 2023 18:04:50 +0100 Subject: [PATCH 10/33] extract AuthorizeWebhookVerifier; merge app-configurator --- src/authorize-webhook-manager.ts | 13 +- src/lib/env.mjs | 2 + .../authorize-net/authorize-net-config.ts | 1 - .../authorize-net-webhook-verifier.ts | 42 +++ .../authorize-net-webhooks-client.ts | 41 +-- .../create-authorize-webhooks-fetch.ts | 17 +- .../channel-connection-configurator.test.ts | 167 ----------- .../channel-connection-configurator.ts | 44 --- .../channel-connection.router.ts | 54 ---- .../configuration/app-config-resolver.ts | 1 + src/modules/configuration/app-configurator.ts | 72 ++++- .../provider/provider-configurator.test.ts | 259 ------------------ src/modules/provider/provider-configurator.ts | 44 --- src/modules/provider/provider.router.ts | 61 ----- src/modules/trpc/trpc-app-router.ts | 5 +- 15 files changed, 140 insertions(+), 683 deletions(-) create mode 100644 src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts delete mode 100644 src/modules/channel-connection/channel-connection-configurator.test.ts delete mode 100644 src/modules/channel-connection/channel-connection-configurator.ts delete mode 100644 src/modules/channel-connection/channel-connection.router.ts delete mode 100644 src/modules/provider/provider-configurator.test.ts delete mode 100644 src/modules/provider/provider-configurator.ts delete mode 100644 src/modules/provider/provider.router.ts diff --git a/src/authorize-webhook-manager.ts b/src/authorize-webhook-manager.ts index 81df5ed..a566a08 100644 --- a/src/authorize-webhook-manager.ts +++ b/src/authorize-webhook-manager.ts @@ -7,6 +7,8 @@ import { import { AppConfigurator, type AppConfig } from "./modules/configuration/app-configurator"; import { resolveAuthorizeConfigFromAppConfig } from "./modules/configuration/authorize-config-resolver"; import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; +import { env } from "./lib/env.mjs"; +import { isDevelopment } from "./lib/isEnv"; import { createLogger } from "@/lib/logger"; export class AuthorizeWebhookManager { @@ -16,7 +18,7 @@ export class AuthorizeWebhookManager { private authorizeConfig: AuthorizeProviderConfig.FullShape; private logger = createLogger({ - name: "AppWebhookInitializer", + name: "AuthorizeWebhookManager", }); constructor({ @@ -51,6 +53,9 @@ export class AuthorizeWebhookManager { } public async register() { + this.logger.debug({ + authorizeConfig: this.authorizeConfig, + }); if (this.authorizeConfig.webhook) { this.logger.info("Webhook already registered"); return; @@ -59,17 +64,17 @@ export class AuthorizeWebhookManager { this.logger.debug("Registering webhook..."); const webhooksClient = new AuthorizeNetWebhooksClient(this.authorizeConfig); + const appUrl = isDevelopment() ? env.LOCAL_APP_URL : `https://${env.VERCEL_URL}`; const webhookParams: AuthorizeNetWebhook = { - name: `Saleor ${this.authData.domain} webhook`, eventTypes: ["net.authorize.payment.authcapture.created"], status: "active", - url: `${this.authData.saleorApiUrl}/api/webhooks/authorize`, + url: `${appUrl}/api/webhooks/authorize`, }; const webhook = await webhooksClient.registerWebhook(webhookParams); await this.updateMetadataWithWebhook(webhook); - this.logger.info("Webhook registered"); + this.logger.info("Webhook registered successfully"); } } diff --git a/src/lib/env.mjs b/src/lib/env.mjs index 4faa5bf..8ca26ea 100644 --- a/src/lib/env.mjs +++ b/src/lib/env.mjs @@ -19,6 +19,7 @@ export const env = createEnv({ .optional() .default("error"), VERCEL_URL: z.string().optional(), + LOCAL_APP_URL: z.string().optional(), PORT: z.coerce.number().optional(), UPSTASH_URL: z.string().optional(), UPSTASH_TOKEN: z.string().optional(), @@ -91,6 +92,7 @@ export const env = createEnv({ CI: process.env.CI, APP_DEBUG: process.env.APP_DEBUG, VERCEL_URL: process.env.VERCEL_URL, + LOCAL_APP_URL: process.env.LOCAL_APP_URL, PORT: process.env.PORT, UPSTASH_URL: process.env.UPSTASH_URL, UPSTASH_TOKEN: process.env.UPSTASH_TOKEN, diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index 2c0a0cc..c772312 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -12,7 +12,6 @@ const authorizeNetEventSchema = z.enum([ ]); export const webhookSchema = z.object({ - name: z.string(), url: z.string(), eventTypes: z.array(authorizeNetEventSchema), status: z.enum(["active", "inactive"]), diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts new file mode 100644 index 0000000..aaa6ed3 --- /dev/null +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts @@ -0,0 +1,42 @@ +import crypto from "crypto"; +import { AuthorizeNetInvalidWebhookSignatureError } from "../authorize-net-error"; +import { type AuthorizeProviderConfig } from "@/modules/authorize-net/authorize-net-config"; +import { createLogger } from "@/lib/logger"; + +export class AuthorizeWebhookVerifier { + private logger = createLogger({ + name: "AuthorizeWebhookVerifier", + }); + private authorizeSignature = "X-ANET-Signature"; + + constructor(private config: AuthorizeProviderConfig.FullShape) {} + + /** + * @see https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification + * @todo use in webhook handler + */ + async verifyAuthorizeWebhook(response: Response) { + const headers = response.headers; + + this.logger.debug({ headers }, "Webhook headers"); + const xAnetSignature = headers.get(this.authorizeSignature); + + if (!xAnetSignature) { + throw new AuthorizeNetInvalidWebhookSignatureError( + `Missing ${this.authorizeSignature} header`, + ); + } + + const body = await response.text(); + const hash = crypto + .createHmac("sha512", this.config.signatureKey) + .update(body) + .digest("base64"); + + const validSignature = `sha512=${hash}`; + + if (validSignature !== xAnetSignature) { + throw new AuthorizeNetInvalidWebhookSignatureError("Invalid signature"); + } + } +} diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts index 1e4cbf7..149fe0b 100644 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts @@ -1,11 +1,10 @@ -import crypto from "crypto"; import { z } from "zod"; -import { AuthorizeNetInvalidWebhookSignatureError } from "../authorize-net-error"; import { createAuthorizeWebhooksFetch } from "./create-authorize-webhooks-fetch"; import { webhookSchema, type AuthorizeProviderConfig, } from "@/modules/authorize-net/authorize-net-config"; +import { createLogger } from "@/lib/logger"; export type AuthorizeNetWebhook = z.infer; @@ -28,36 +27,13 @@ const listWebhooksResponseSchema = z.array(webhookResponseSchema); */ export class AuthorizeNetWebhooksClient { private fetch: ReturnType; - private authorizeSignature = "X-ANET-Signature"; - constructor(private config: AuthorizeProviderConfig.FullShape) { - this.fetch = createAuthorizeWebhooksFetch(config); - } - - /** - * @see https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification - */ - private async verifyAuthorizeWebhook(response: Response) { - const headers = response.headers; - const xAnetSignature = headers.get(this.authorizeSignature); - - if (!xAnetSignature) { - throw new AuthorizeNetInvalidWebhookSignatureError( - `Missing ${this.authorizeSignature} header`, - ); - } - - const body = await response.text(); - const hash = crypto - .createHmac("sha512", this.config.signatureKey) - .update(body) - .digest("base64"); + private logger = createLogger({ + name: "AuthorizeNetWebhooksClient", + }); - const validSignature = `sha512=${hash}`; - - if (validSignature !== xAnetSignature) { - throw new AuthorizeNetInvalidWebhookSignatureError("Invalid signature"); - } + constructor(config: AuthorizeProviderConfig.FullShape) { + this.fetch = createAuthorizeWebhooksFetch(config); } async registerWebhook(params: AuthorizeNetWebhook) { @@ -66,9 +42,8 @@ export class AuthorizeNetWebhooksClient { body: params, }); - await this.verifyAuthorizeWebhook(response); - const result = await response.json(); + const parsedResult = webhookResponseSchema.parse(result); return parsedResult; @@ -79,8 +54,6 @@ export class AuthorizeNetWebhooksClient { method: "GET", }); - await this.verifyAuthorizeWebhook(response); - const result = await response.json(); const parsedResult = listWebhooksResponseSchema.parse(result); diff --git a/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts b/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts index 8e8ae23..73ab6f5 100644 --- a/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts +++ b/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts @@ -1,3 +1,4 @@ +import { createLogger } from "@/lib/logger"; import { type AuthorizeProviderConfig } from "@/modules/authorize-net/authorize-net-config"; /** @@ -19,18 +20,28 @@ type AuthorizeWebhooksFetchParams = { export function createAuthorizeWebhooksFetch(config: AuthorizeProviderConfig.FullShape) { const authenticationKey = createAuthorizeAuthenticationKey(config); + const url = config.environment === "sandbox" ? "https://apitest.authorize.net/rest/v1/webhooks" : "https://api.authorize.net/rest/v1/webhooks"; - return ({ path, body }: AuthorizeWebhooksFetchParams) => { + const logger = createLogger({ + name: "AuthorizeWebhooksFetch", + }); + + return ({ path, body, method }: AuthorizeWebhooksFetchParams) => { const apiUrl = path ? `${url}/${path}` : url; - return fetch(apiUrl, { + const options = { + method, headers: { + "Content-Type": "application/json", Authorization: `Basic ${authenticationKey}`, }, body: JSON.stringify(body), - }); + }; + + logger.trace({ apiUrl, options: { method, body } }, "Calling Authorize.net webhooks API"); + return fetch(apiUrl, options); }; } diff --git a/src/modules/channel-connection/channel-connection-configurator.test.ts b/src/modules/channel-connection/channel-connection-configurator.test.ts deleted file mode 100644 index f1f44b3..0000000 --- a/src/modules/channel-connection/channel-connection-configurator.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { type AppConfig } from "../configuration/app-configurator"; -import { ChannelConnectionConfigurator } from "./channel-connection-configurator"; - -let rootData: AppConfig.Shape["connections"] = []; - -beforeEach(() => { - rootData = []; -}); - -describe("ChannelConnectionConfigurator", () => { - describe("getConnections", () => { - it("returns all connections", () => { - const configurator = new ChannelConnectionConfigurator([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - { - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - - expect(configurator.getConnections()).toEqual([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - { - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - }); - }); - - describe("addConnection", () => { - it("adds a connection", () => { - const configurator = new ChannelConnectionConfigurator(rootData); - - configurator.addConnection({ - channelSlug: "channel-slug", - providerId: "provider-id", - }); - - expect(configurator.getConnections()).toEqual([ - { - id: expect.any(String), - channelSlug: "channel-slug", - providerId: "provider-id", - }, - ]); - }); - }); - - describe("updateConnection", () => { - it("updates a connection", () => { - const configurator = new ChannelConnectionConfigurator([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - ]); - - configurator.updateConnection({ - id: "1", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }); - - expect(configurator.getConnections()).toEqual([ - { - id: "1", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - }); - - it("doesn't update a connection if the id doesn't match", () => { - const configurator = new ChannelConnectionConfigurator([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - ]); - - configurator.updateConnection({ - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }); - - expect(configurator.getConnections()).toEqual([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - ]); - }); - }); - - describe("deleteConnection", () => { - it("deletes a connection", () => { - const configurator = new ChannelConnectionConfigurator([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - { - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - - configurator.deleteConnection("1"); - - expect(configurator.getConnections()).toEqual([ - { - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - }); - - it("doesn't delete a connection if the id doesn't match", () => { - const configurator = new ChannelConnectionConfigurator([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - { - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - - configurator.deleteConnection("3"); - - expect(configurator.getConnections()).toEqual([ - { - id: "1", - channelSlug: "channel-slug-1", - providerId: "provider-id-1", - }, - { - id: "2", - channelSlug: "channel-slug-2", - providerId: "provider-id-2", - }, - ]); - }); - }); -}); diff --git a/src/modules/channel-connection/channel-connection-configurator.ts b/src/modules/channel-connection/channel-connection-configurator.ts deleted file mode 100644 index 2647682..0000000 --- a/src/modules/channel-connection/channel-connection-configurator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { type AppConfig } from "../configuration/app-configurator"; -import { ChannelConnection } from "./channel-connection.schema"; -import { generateId } from "@/lib/generate-id"; - -type Connections = AppConfig.Shape["connections"]; - -export class ChannelConnectionConfigurator { - private connections: Connections = []; - - constructor(connections: Connections) { - this.connections = connections; - } - - getConnections() { - return this.connections; - } - - addConnection(input: ChannelConnection.InputShape) { - const connectionConfig = ChannelConnection.Schema.Input.parse(input); - - this.connections.push({ - ...connectionConfig, - id: generateId(), - }); - - return this; - } - - updateConnection(connection: ChannelConnection.FullShape) { - const parsedConfig = ChannelConnection.Schema.Full.parse(connection); - - this.connections = this.connections.map((p) => { - if (p.id === parsedConfig.id) { - return parsedConfig; - } - - return p; - }); - } - - deleteConnection(connectionId: string) { - this.connections = this.connections.filter((p) => p.id !== connectionId); - } -} diff --git a/src/modules/channel-connection/channel-connection.router.ts b/src/modules/channel-connection/channel-connection.router.ts deleted file mode 100644 index 28835c1..0000000 --- a/src/modules/channel-connection/channel-connection.router.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { z } from "zod"; - -import { AppConfigMetadataManager } from "../configuration/app-config-metadata-manager"; -import { createSettingsManager } from "../configuration/settings-manager"; -import { protectedClientProcedure } from "../trpc/protected-client-procedure"; -import { router } from "../trpc/trpc-server"; -import { ChannelConnection } from "./channel-connection.schema"; - -const procedure = protectedClientProcedure.use(({ ctx, next }) => { - const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId!); - - return next({ - ctx: { - settingsManager, - appConfigService: new AppConfigMetadataManager(settingsManager), - }, - }); -}); - -export const channelConnectionRouter = router({ - getAll: procedure.query(async ({ ctx: { appConfigService } }) => { - const config = await appConfigService.get(); - const providers = config.connections.getConnections(); - - return providers; - }), - addOne: procedure - .input(ChannelConnection.Schema.Input) - .mutation(async ({ ctx: { appConfigService }, input }) => { - const config = await appConfigService.get(); - - config.connections.addConnection(input); - - await appConfigService.set(config); - }), - updateOne: procedure - .input(ChannelConnection.Schema.Full) - .mutation(async ({ input, ctx: { appConfigService } }) => { - const config = await appConfigService.get(); - - config.connections.updateConnection(input); - - return appConfigService.set(config); - }), - deleteOne: procedure - .input(z.object({ id: z.string() })) - .mutation(async ({ input, ctx: { appConfigService } }) => { - const config = await appConfigService.get(); - - config.connections.deleteConnection(input.id); - - return appConfigService.set(config); - }), -}); diff --git a/src/modules/configuration/app-config-resolver.ts b/src/modules/configuration/app-config-resolver.ts index 65f077f..b4c4dbc 100644 --- a/src/modules/configuration/app-config-resolver.ts +++ b/src/modules/configuration/app-config-resolver.ts @@ -34,6 +34,7 @@ class AppConfigResolver { transactionKey: env.AUTHORIZE_TRANSACTION_KEY, environment: env.AUTHORIZE_ENVIRONMENT, signatureKey: env.AUTHORIZE_SIGNATURE_KEY, + webhook: null, }; const connectionInput = { diff --git a/src/modules/configuration/app-configurator.ts b/src/modules/configuration/app-configurator.ts index 4aec579..18b4a82 100644 --- a/src/modules/configuration/app-configurator.ts +++ b/src/modules/configuration/app-configurator.ts @@ -1,8 +1,7 @@ import { z } from "zod"; import { AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; -import { ChannelConnectionConfigurator } from "../channel-connection/channel-connection-configurator"; import { ChannelConnection } from "../channel-connection/channel-connection.schema"; -import { ProvidersConfigurator } from "../provider/provider-configurator"; +import { generateId } from "@/lib/generate-id"; export namespace AppConfig { export const Schema = z.object({ @@ -19,18 +18,75 @@ export class AppConfigurator { connections: [], }; - providers: ProvidersConfigurator; - connections: ChannelConnectionConfigurator; - constructor(initialData?: AppConfig.Shape) { if (initialData) { this.rootData = initialData; } - - this.providers = new ProvidersConfigurator(this.rootData.providers); - this.connections = new ChannelConnectionConfigurator(this.rootData.connections); } + providers = { + getProviders: () => { + return this.rootData.providers; + }, + getProviderById: (id: string) => { + return this.rootData.providers.find((p) => p.id === id); + }, + addProvider: (input: AuthorizeProviderConfig.InputShape) => { + const nextProviders = [ + ...this.rootData.providers, + { + ...input, + id: generateId(), + }, + ]; + + this.rootData.providers = nextProviders; + }, + updateProvider: (provider: AuthorizeProviderConfig.FullShape) => { + const nextProviders = this.rootData.providers.map((p) => { + if (p.id === provider.id) { + return provider; + } + + return p; + }); + + this.rootData.providers = nextProviders; + }, + deleteProvider: (providerId: string) => { + const nextProviders = this.rootData.providers.filter((p) => p.id !== providerId); + + this.rootData.providers = nextProviders; + }, + }; + + connections = { + getConnections: () => { + return this.rootData.connections; + }, + addConnection: (input: ChannelConnection.InputShape) => { + const nextConnections = [...this.rootData.connections, { ...input, id: generateId() }]; + + this.rootData.connections = nextConnections; + }, + updateConnection: (connection: ChannelConnection.FullShape) => { + const nextConnections = this.rootData.connections.map((p) => { + if (p.id === connection.id) { + return connection; + } + + return p; + }); + + this.rootData.connections = nextConnections; + }, + deleteConnection: (connectionId: string) => { + const nextConnections = this.rootData.connections.filter((p) => p.id !== connectionId); + + this.rootData.connections = nextConnections; + }, + }; + static parse(serializedSchema: string) { const parsedSchema = JSON.parse(serializedSchema); const configSchema = AppConfig.Schema.parse(parsedSchema); diff --git a/src/modules/provider/provider-configurator.test.ts b/src/modules/provider/provider-configurator.test.ts deleted file mode 100644 index 3dc902f..0000000 --- a/src/modules/provider/provider-configurator.test.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { type AppConfig } from "../configuration/app-configurator"; -import { ProvidersConfigurator } from "./provider-configurator"; - -let rootData: AppConfig.Shape["providers"] = []; - -beforeEach(() => { - rootData = []; -}); - -describe("ProvidersConfigurator", () => { - describe("getProviders", () => { - it("returns all providers", () => { - const configurator = new ProvidersConfigurator([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - { - id: "2", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key-2", - webhook: null, - }, - ]); - - expect(configurator.getProviders()).toEqual([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - { - id: "2", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key-2", - webhook: null, - }, - ]); - }); - }); - - describe("getProviderById", () => { - it("returns the provider with the given id", () => { - const configurator = new ProvidersConfigurator([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - ]); - - expect(configurator.getProviderById("1")).toEqual({ - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }); - }); - - it("returns undefined if the id doesn't match", () => { - const configurator = new ProvidersConfigurator([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - ]); - - expect(configurator.getProviderById("not-a-real-id")).toBeUndefined(); - }); - }); - - describe("addProvider", () => { - it("adds a new provider", () => { - const configurator = new ProvidersConfigurator(rootData); - - configurator.addProvider({ - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }); - - expect(configurator.getProviders()).toEqual([ - { - id: expect.any(String), - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - }, - ]); - }); - }); - - describe("updateProvider", () => { - it("doesn't update the provider if the id doesn't match", () => { - const configurator = new ProvidersConfigurator(rootData); - - configurator.updateProvider({ - id: "not-a-real-id", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }); - - expect(configurator.getProviders()).toEqual(rootData); - }); - it("updates the provider with the given id", () => { - const configurator = new ProvidersConfigurator([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - ]); - - configurator.updateProvider({ - id: "1", - apiLoginId: "new-api-login-id", - transactionKey: "new-transaction-key", - environment: "sandbox", - publicClientKey: "new-public-client-key", - signatureKey: "new-signature-key", - webhook: null, - }); - - expect(configurator.getProviders()).toEqual([ - { - id: "1", - apiLoginId: "new-api-login-id", - transactionKey: "new-transaction-key", - environment: "sandbox", - publicClientKey: "new-public-client-key", - signatureKey: "new-signature-key", - }, - ]); - }); - }); - - describe("deleteProvider", () => { - it("deletes the selected provider", () => { - const configurator = new ProvidersConfigurator([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - { - id: "2", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key-2", - webhook: null, - }, - ]); - - configurator.deleteProvider("1"); - - expect(configurator.getProviders()).toEqual([ - { - id: "2", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key-2", - }, - ]); - }); - - it("doesn't delete any providers if the id doesn't match", () => { - const configurator = new ProvidersConfigurator([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - webhook: null, - }, - { - id: "2", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key-2", - webhook: null, - }, - ]); - - configurator.deleteProvider("not-a-real-id"); - - expect(configurator.getProviders()).toEqual([ - { - id: "1", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key", - }, - { - id: "2", - apiLoginId: "api-login-id", - transactionKey: "transaction-key", - environment: "sandbox", - publicClientKey: "public-client-key", - signatureKey: "signature-key-2", - }, - ]); - }); - }); -}); diff --git a/src/modules/provider/provider-configurator.ts b/src/modules/provider/provider-configurator.ts deleted file mode 100644 index 40bd481..0000000 --- a/src/modules/provider/provider-configurator.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; -import { type AppConfig } from "../configuration/app-configurator"; -import { generateId } from "@/lib/generate-id"; - -type Providers = AppConfig.Shape["providers"]; - -export class ProvidersConfigurator { - constructor(private providers: Providers) {} - - getProviders() { - return this.providers; - } - - getProviderById(id: string) { - return this.providers.find((p) => p.id === id); - } - - addProvider(input: AuthorizeProviderConfig.InputShape) { - const providerConfig = AuthorizeProviderConfig.Schema.Input.parse(input); - - this.providers.push({ - ...providerConfig, - id: generateId(), - }); - - return this; - } - - updateProvider(provider: AuthorizeProviderConfig.FullShape) { - const parsedConfig = AuthorizeProviderConfig.Schema.Full.parse(provider); - - this.providers = this.providers.map((p) => { - if (p.id === parsedConfig.id) { - return parsedConfig; - } else { - return p; - } - }); - } - - deleteProvider(providerId: string) { - this.providers = this.providers.filter((p) => p.id !== providerId); - } -} diff --git a/src/modules/provider/provider.router.ts b/src/modules/provider/provider.router.ts deleted file mode 100644 index 4a5fd04..0000000 --- a/src/modules/provider/provider.router.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { z } from "zod"; - -import { AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; -import { AppConfigMetadataManager } from "../configuration/app-config-metadata-manager"; -import { createSettingsManager } from "../configuration/settings-manager"; -import { protectedClientProcedure } from "../trpc/protected-client-procedure"; -import { router } from "../trpc/trpc-server"; - -const procedure = protectedClientProcedure.use(({ ctx, next }) => { - const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId!); - - return next({ - ctx: { - settingsManager, - appConfigService: new AppConfigMetadataManager(settingsManager), - }, - }); -}); - -export const providerRouter = router({ - getAll: procedure.query(async ({ ctx: { appConfigService } }) => { - const config = await appConfigService.get(); - const providers = config.providers.getProviders(); - - return providers; - }), - getOne: procedure - .input(z.object({ id: z.string() })) - .query(async ({ ctx: { appConfigService }, input }) => { - const config = await appConfigService.get(); - - return config.providers.getProviderById(input.id) ?? null; - }), - addOne: procedure - .input(AuthorizeProviderConfig.Schema.Input) - .mutation(async ({ ctx: { appConfigService }, input }) => { - const config = await appConfigService.get(); - - config.providers.addProvider(input); - - await appConfigService.set(config); - }), - updateOne: procedure - .input(AuthorizeProviderConfig.Schema.Full) - .mutation(async ({ input, ctx: { appConfigService } }) => { - const config = await appConfigService.get(); - - config?.providers.updateProvider(input); - - return appConfigService.set(config); - }), - deleteOne: procedure - .input(z.object({ id: z.string() })) - .mutation(async ({ input, ctx: { appConfigService } }) => { - const config = await appConfigService.get(); - - config.providers.deleteProvider(input.id); - - return appConfigService.set(config); - }), -}); diff --git a/src/modules/trpc/trpc-app-router.ts b/src/modules/trpc/trpc-app-router.ts index 92e58eb..8e82f9a 100644 --- a/src/modules/trpc/trpc-app-router.ts +++ b/src/modules/trpc/trpc-app-router.ts @@ -1,10 +1,7 @@ -import { channelConnectionRouter } from "../channel-connection/channel-connection.router"; -import { providerRouter } from "../provider/provider.router"; import { router } from "./trpc-server"; export const appRouter = router({ - providers: providerRouter, - connections: channelConnectionRouter, + // no routers because we don't have client-side calls }); export type AppRouter = typeof appRouter; From c296c71ed5adbea46cd1f4dee639e24fcf51f6ca Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 19 Dec 2023 07:31:04 +0100 Subject: [PATCH 11/33] Move around --- ...ient.ts => authorize-net-webhook-client.ts} | 18 ++++++++++-------- .../transaction-cancelation-requested.ts | 2 +- .../webhooks/transaction-initialize-session.ts | 6 +++--- .../webhooks/transaction-process-session.ts | 2 +- .../webhooks/transaction-refund-requested.ts | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) rename src/modules/authorize-net/webhooks-client/{authorize-net-webhooks-client.ts => authorize-net-webhook-client.ts} (79%) diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-client.ts similarity index 79% rename from src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts rename to src/modules/authorize-net/webhooks-client/authorize-net-webhook-client.ts index 149fe0b..08f213f 100644 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhooks-client.ts +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-client.ts @@ -1,12 +1,11 @@ import { z } from "zod"; import { createAuthorizeWebhooksFetch } from "./create-authorize-webhooks-fetch"; +import { createLogger } from "@/lib/logger"; import { webhookSchema, type AuthorizeProviderConfig, + type AuthorizeNetWebhookInput, } from "@/modules/authorize-net/authorize-net-config"; -import { createLogger } from "@/lib/logger"; - -export type AuthorizeNetWebhook = z.infer; const webhookResponseSchema = z .object({ @@ -15,35 +14,38 @@ const webhookResponseSchema = z href: z.string(), }), }), - webhookId: z.string(), }) .and(webhookSchema); +export type AuthorizeNetWebhookResponse = z.infer; + const listWebhooksResponseSchema = z.array(webhookResponseSchema); /** * @description Authorize.net has a separate API for registering webhooks. * @see AuthorizeNetClient for managing transactions etc. */ -export class AuthorizeNetWebhooksClient { +export class AuthorizeNetWebhookClient { private fetch: ReturnType; private logger = createLogger({ - name: "AuthorizeNetWebhooksClient", + name: "AuthorizeNetWebhookClient", }); constructor(config: AuthorizeProviderConfig.FullShape) { this.fetch = createAuthorizeWebhooksFetch(config); } - async registerWebhook(params: AuthorizeNetWebhook) { + async registerWebhook(input: AuthorizeNetWebhookInput) { const response = await this.fetch({ method: "POST", - body: params, + body: input, }); const result = await response.json(); + this.logger.trace({ result }, "registerWebhook response:"); + const parsedResult = webhookResponseSchema.parse(result); return parsedResult; diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 7c779ff..39830e9 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -13,7 +13,7 @@ import { UntypedTransactionCancelationRequestedDocument, type TransactionCancelationRequestedEventFragment, } from "generated/graphql"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; export const config = { api: { diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index fc88e65..b9c2d9b 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -1,11 +1,11 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; +import { normalizeError } from "@/errors"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; -import { TransactionInitializeError } from "@/modules/webhooks/transaction-initialize-session"; import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; import { saleorApp } from "@/saleor-app"; import { type TransactionInitializeSessionResponse } from "@/schemas/TransactionInitializeSession/TransactionInitializeSessionResponse.mjs"; @@ -80,7 +80,7 @@ export default transactionInitializeSessionSyncWebhook.createHandler( } catch (error) { Sentry.captureException(error); - const normalizedError = TransactionInitializeError.normalize(error); + const normalizedError = normalizeError(error); return responseBuilder.ok({ amount: 0, // 0 or real amount? result: "AUTHORIZATION_FAILURE", diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index 7e39413..6672537 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,6 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index 0cb4c53..e469c3c 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -4,7 +4,7 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; -import { AuthorizeWebhookManager } from "@/authorize-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; From f23065643de4394952a302f944649453c684d8cf Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 19 Dec 2023 07:31:41 +0100 Subject: [PATCH 12/33] Update transaction webhook log messages --- src/modules/webhooks/transaction-cancelation-requested.ts | 2 +- src/modules/webhooks/transaction-initialize-session.ts | 5 ++++- src/modules/webhooks/transaction-process-session.ts | 2 +- src/modules/webhooks/transaction-refund-requested.ts | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/webhooks/transaction-cancelation-requested.ts b/src/modules/webhooks/transaction-cancelation-requested.ts index cb19c98..6053658 100644 --- a/src/modules/webhooks/transaction-cancelation-requested.ts +++ b/src/modules/webhooks/transaction-cancelation-requested.ts @@ -59,7 +59,7 @@ export class TransactionCancelationRequestedService { async execute( payload: TransactionCancelationRequestedEventFragment, ): Promise { - this.logger.debug({ id: payload.transaction?.id }, "Called execute with"); + this.logger.debug({ id: payload.transaction?.id }, "Canceling transaction"); const saleorTransactionId = payload.transaction?.id; diff --git a/src/modules/webhooks/transaction-initialize-session.ts b/src/modules/webhooks/transaction-initialize-session.ts index 16d174b..dba9126 100644 --- a/src/modules/webhooks/transaction-initialize-session.ts +++ b/src/modules/webhooks/transaction-initialize-session.ts @@ -133,7 +133,10 @@ export class TransactionInitializeSessionService { async execute( payload: TransactionInitializeSessionEventFragment, ): Promise { - this.logger.debug({ id: payload.transaction?.id }, "Called execute with"); + this.logger.debug( + { id: payload.transaction?.id }, + "Getting hosted payment page settings for transaction", + ); const transactionInput = await this.buildTransactionFromPayload(payload); diff --git a/src/modules/webhooks/transaction-process-session.ts b/src/modules/webhooks/transaction-process-session.ts index e7320a3..3603f7e 100644 --- a/src/modules/webhooks/transaction-process-session.ts +++ b/src/modules/webhooks/transaction-process-session.ts @@ -80,7 +80,7 @@ export class TransactionProcessSessionService { async execute( payload: TransactionProcessSessionEventFragment, ): Promise { - this.logger.debug({ id: payload.transaction?.id }, "Called execute with"); + this.logger.debug({ id: payload.transaction?.id }, "Mapping the state of transaction"); const dataParseResult = transactionProcessPayloadDataSchema.safeParse(payload.data); if (!dataParseResult.success) { diff --git a/src/modules/webhooks/transaction-refund-requested.ts b/src/modules/webhooks/transaction-refund-requested.ts index 995d6d0..c2ebbe5 100644 --- a/src/modules/webhooks/transaction-refund-requested.ts +++ b/src/modules/webhooks/transaction-refund-requested.ts @@ -56,7 +56,7 @@ export class TransactionRefundRequestedService { async execute( payload: TransactionRefundRequestedEventFragment, ): Promise { - this.logger.debug({ id: payload.transaction?.id }, "Called execute with"); + this.logger.debug({ id: payload.transaction?.id }, "Refunding the transaction"); const saleorTransactionId = payload.transaction?.id; From f33f36a35bdda28887f50c69128fbd8c67e9e5d9 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 19 Dec 2023 07:32:03 +0100 Subject: [PATCH 13/33] Move AuthorizeNetWebhookManager class --- .../authorize-net-webhook-manager.ts} | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) rename src/{authorize-webhook-manager.ts => modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts} (51%) diff --git a/src/authorize-webhook-manager.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts similarity index 51% rename from src/authorize-webhook-manager.ts rename to src/modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts index a566a08..2dd1495 100644 --- a/src/authorize-webhook-manager.ts +++ b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts @@ -1,14 +1,17 @@ import { type AuthData } from "@saleor/app-sdk/APL"; -import { type AuthorizeProviderConfig } from "./modules/authorize-net/authorize-net-config"; +import { env } from "../../../lib/env.mjs"; +import { isDevelopment } from "../../../lib/isEnv"; +import { AppConfigMetadataManager } from "../../configuration/app-config-metadata-manager"; +import { AppConfigurator, type AppConfig } from "../../configuration/app-configurator"; +import { resolveAuthorizeConfigFromAppConfig } from "../../configuration/authorize-config-resolver"; import { - AuthorizeNetWebhooksClient, type AuthorizeNetWebhook, -} from "./modules/authorize-net/webhooks-client/authorize-net-webhooks-client"; -import { AppConfigurator, type AppConfig } from "./modules/configuration/app-configurator"; -import { resolveAuthorizeConfigFromAppConfig } from "./modules/configuration/authorize-config-resolver"; -import { AppConfigMetadataManager } from "./modules/configuration/app-config-metadata-manager"; -import { env } from "./lib/env.mjs"; -import { isDevelopment } from "./lib/isEnv"; + type AuthorizeNetWebhookInput, + type AuthorizeProviderConfig, +} from "../authorize-net-config"; +import { AuthorizeNetWebhookClient } from "./authorize-net-webhook-client"; +import { MissingAppUrlError } from "./authorize-net-webhook-errors"; +import { AuthorizeNetWebhookUrlBuilder } from "./authorize-net-webhook-url-builder"; import { createLogger } from "@/lib/logger"; export class AuthorizeWebhookManager { @@ -52,25 +55,40 @@ export class AuthorizeWebhookManager { await appConfigMetadataManager.set(appConfigurator); } - public async register() { - this.logger.debug({ - authorizeConfig: this.authorizeConfig, - }); - if (this.authorizeConfig.webhook) { - this.logger.info("Webhook already registered"); - return; - } + private getWebhookParams() { + const appUrl = isDevelopment() ? env.LOCAL_APP_URL : `https://${env.VERCEL_URL}`; - this.logger.debug("Registering webhook..."); + if (!appUrl) { + throw new MissingAppUrlError("Missing appUrl needed for registering the webhook"); + } - const webhooksClient = new AuthorizeNetWebhooksClient(this.authorizeConfig); - const appUrl = isDevelopment() ? env.LOCAL_APP_URL : `https://${env.VERCEL_URL}`; - const webhookParams: AuthorizeNetWebhook = { - eventTypes: ["net.authorize.payment.authcapture.created"], + const urlBuilder = new AuthorizeNetWebhookUrlBuilder(); + const webhookParams: AuthorizeNetWebhookInput = { + eventTypes: [ + "net.authorize.payment.authcapture.created", + "net.authorize.payment.authorization.created", + ], status: "active", - url: `${appUrl}/api/webhooks/authorize`, + url: urlBuilder.buildFromParams({ + appUrl, + }), }; + return webhookParams; + } + + public async register() { + // todo: bring back + // if (this.authorizeConfig.webhook) { + // this.logger.info("Webhook already registered"); + // return; + // } + + this.logger.debug("Registering webhook..."); + + const webhooksClient = new AuthorizeNetWebhookClient(this.authorizeConfig); + + const webhookParams = this.getWebhookParams(); const webhook = await webhooksClient.registerWebhook(webhookParams); await this.updateMetadataWithWebhook(webhook); From ad7bccfb11ef2f66aa0d3c1374eeced194811e17 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 19 Dec 2023 14:36:57 +0100 Subject: [PATCH 14/33] Fix various bugs and refactor code --- .../mutations/TransactionEventReport.graphql | 4 +- src/errors.ts | 9 ++ .../authorize-net/authorize-net-config.ts | 16 +- .../client/create-transaction.ts | 15 +- .../client/transaction-details-client.ts | 2 + .../authorize-net-webhook-client.ts | 2 +- .../webhook/authorize-net-webhook-errors.ts | 9 ++ .../webhook/authorize-net-webhook-handler.ts | 141 ++++++++++++++++++ .../authorize-net-webhook-manager.ts | 19 ++- ...ze-net-webhook-transaction-synchronizer.ts | 77 ++++++++++ .../create-authorize-webhooks-fetch.ts | 0 .../authorize-net-webhook-verifier.ts | 42 ------ src/modules/configuration/app-configurator.ts | 2 +- .../transaction-cancelation-requested.ts | 5 +- .../webhooks/transaction-process-session.ts | 15 +- .../webhooks/transaction-refund-requested.ts | 2 +- src/pages/api/webhooks/authorize.ts | 22 +++ .../transaction-cancelation-requested.ts | 2 +- .../transaction-initialize-session.ts | 2 +- .../webhooks/transaction-process-session.ts | 2 +- .../webhooks/transaction-refund-requested.ts | 2 +- 21 files changed, 320 insertions(+), 70 deletions(-) rename src/modules/authorize-net/{webhooks-client => webhook}/authorize-net-webhook-client.ts (97%) create mode 100644 src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts create mode 100644 src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts rename src/modules/authorize-net/{webhooks-client => webhook}/authorize-net-webhook-manager.ts (88%) create mode 100644 src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts rename src/modules/authorize-net/{webhooks-client => webhook}/create-authorize-webhooks-fetch.ts (100%) delete mode 100644 src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts create mode 100644 src/pages/api/webhooks/authorize.ts diff --git a/graphql/mutations/TransactionEventReport.graphql b/graphql/mutations/TransactionEventReport.graphql index 99c9ad9..f9a319f 100644 --- a/graphql/mutations/TransactionEventReport.graphql +++ b/graphql/mutations/TransactionEventReport.graphql @@ -2,10 +2,10 @@ mutation TransactionEventReport( $transactionId: ID! $amount: PositiveDecimal! $availableActions: [TransactionActionEnum!]! - $externalUrl: String! + $externalUrl: String $message: String $pspReference: String! - $time: DateTime! + $time: DateTime $type: TransactionEventTypeEnum! ) { transactionEventReport( diff --git a/src/errors.ts b/src/errors.ts index 45259ce..cc03834 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,6 +1,7 @@ import { type TRPC_ERROR_CODE_KEY } from "@trpc/server/rpc"; import ModernError from "modern-errors"; import ModernErrorsSerialize from "modern-errors-serialize"; +import { ZodError } from "zod"; // Http errors type CommonProps = { @@ -49,3 +50,11 @@ export const ReqMissingTokenError = BaseTrpcError.subclass("ReqMissingTokenError export const ReqMissingAppIdError = BaseTrpcError.subclass("ReqMissingAppIdError", { props: { trpcCode: "BAD_REQUEST" } as TrpcErrorOptions, }); + +export function normalizeError(error: unknown) { + if (error instanceof ZodError) { + return BaseError.normalize(error.format()); + } + + return BaseError.normalize(error); +} diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index c772312..8780b57 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const authorizeEnvironmentSchema = z.enum(["sandbox", "production"]); -const authorizeNetEventSchema = z.enum([ +export const authorizeNetEventSchema = z.enum([ "net.authorize.payment.authorization.created", "net.authorize.payment.authcapture.created", "net.authorize.payment.capture.created", @@ -11,12 +11,24 @@ const authorizeNetEventSchema = z.enum([ "net.authorize.payment.void.create", ]); -export const webhookSchema = z.object({ +export type AuthorizeNetEvent = z.infer; + +export const webhookInputSchema = z.object({ url: z.string(), eventTypes: z.array(authorizeNetEventSchema), status: z.enum(["active", "inactive"]), }); +export type AuthorizeNetWebhookInput = z.infer; + +export const webhookSchema = webhookInputSchema.and( + z.object({ + webhookId: z.string(), + }), +); + +export type AuthorizeNetWebhook = z.infer; + const inputSchema = z.object({ apiLoginId: z.string().min(1), publicClientKey: z.string().min(1), diff --git a/src/modules/authorize-net/client/create-transaction.ts b/src/modules/authorize-net/client/create-transaction.ts index 0b1f5c8..e6c84ad 100644 --- a/src/modules/authorize-net/client/create-transaction.ts +++ b/src/modules/authorize-net/client/create-transaction.ts @@ -11,12 +11,21 @@ const createTransactionSchema = baseAuthorizeObjectSchema.and(z.unknown()); type CreateTransactionResponse = z.infer; export class CreateTransactionClient extends AuthorizeNetClient { - async createTransaction( - transactionInput: AuthorizeNet.APIContracts.TransactionRequestType, - ): Promise { + async createTransaction({ + transactionInput, + saleorTransactionId, + }: { + transactionInput: AuthorizeNet.APIContracts.TransactionRequestType; + saleorTransactionId: string; + }): Promise { const createRequest = new ApiContracts.CreateTransactionRequest(); createRequest.setMerchantAuthentication(this.merchantAuthenticationType); createRequest.setTransactionRequest(transactionInput); + /** + * @description refId is needed to update the state of the transaction on Authorize.net webhook. + * @see AuthorizeNetWebhookHandler + */ + createRequest.setRefId(saleorTransactionId); const transactionController = new ApiControllers.CreateTransactionController( createRequest.getJSON(), diff --git a/src/modules/authorize-net/client/transaction-details-client.ts b/src/modules/authorize-net/client/transaction-details-client.ts index e8b47e8..87cf912 100644 --- a/src/modules/authorize-net/client/transaction-details-client.ts +++ b/src/modules/authorize-net/client/transaction-details-client.ts @@ -12,6 +12,8 @@ const getTransactionDetailsSchema = baseAuthorizeObjectSchema.and( transactionStatus: z.string().min(1), authAmount: z.number(), responseReasonDescription: z.string().min(1), + refTransId: z.string().min(1), + submitTimeLocal: z.string().min(1), }), }), ); diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-client.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-client.ts similarity index 97% rename from src/modules/authorize-net/webhooks-client/authorize-net-webhook-client.ts rename to src/modules/authorize-net/webhook/authorize-net-webhook-client.ts index 08f213f..a7d66e5 100644 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-client.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-client.ts @@ -22,7 +22,7 @@ export type AuthorizeNetWebhookResponse = z.infer; const listWebhooksResponseSchema = z.array(webhookResponseSchema); /** - * @description Authorize.net has a separate API for registering webhooks. + * @description Authorize.net has a separate API for registering webhooks. This class communicates with that API. * @see AuthorizeNetClient for managing transactions etc. */ export class AuthorizeNetWebhookClient { diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts new file mode 100644 index 0000000..63c97c2 --- /dev/null +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts @@ -0,0 +1,9 @@ +import { BaseError } from "@/errors"; + +const AuthorizeNetWebhookError = BaseError.subclass("AuthorizeNetWebhookError", {}); + +export const IncorrectQueryError = AuthorizeNetWebhookError.subclass("IncorrectQueryError", {}); + +export const MissingAuthDataError = AuthorizeNetWebhookError.subclass("MissingAuthDataError", {}); + +export const MissingAppUrlError = AuthorizeNetWebhookError.subclass("MissingAppUrlError", {}); diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts new file mode 100644 index 0000000..cb40366 --- /dev/null +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts @@ -0,0 +1,141 @@ +import crypto from "crypto"; +import { type AuthData } from "@saleor/app-sdk/APL"; +import { type NextApiRequest } from "next"; +import { z } from "zod"; +import { authorizeNetEventSchema, type AuthorizeProviderConfig } from "../authorize-net-config"; +import { AuthorizeNetInvalidWebhookSignatureError } from "../authorize-net-error"; +import { MissingAuthDataError } from "./authorize-net-webhook-errors"; +import { AuthorizeNetWebhookTransactionSynchronizer } from "./authorize-net-webhook-transaction-synchronizer"; +import { saleorApp } from "@/saleor-app"; +import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; +import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager"; +import { createLogger } from "@/lib/logger"; +import { createServerClient } from "@/lib/create-graphq-client"; + +const eventPayloadSchema = z.object({ + notificationId: z.string(), + eventType: authorizeNetEventSchema, + eventDate: z.string(), + webhookId: z.string(), + payload: z.object({ + entityName: z.enum(["transaction"]), + id: z.string(), + }), +}); + +export type EventPayload = z.infer; + +/** + * @description This class is used to handle webhook calls from Authorize.net + */ +export class AuthorizeNetWebhookHandler { + private authorizeSignature = "X-ANET-Signature"; + private authData: AuthData | null = null; + private authorizeConfig: AuthorizeProviderConfig.FullShape | null = null; + + private logger = createLogger({ + name: "AuthorizeWebhookHandler", + }); + + constructor(private request: NextApiRequest) {} + + private async getAuthData() { + if (this.authData) { + return this.authData; + } + + const results = await saleorApp.apl.getAll(); + const authData = results?.[0]; + + if (!authData) { + throw new MissingAuthDataError("APL not found"); + } + + this.authData = authData; + + return authData; + } + + private async decryptAuthorizeWebhookBody({ + authorizeConfig, + }: { + authorizeConfig: AuthorizeProviderConfig.FullShape; + }) { + const logger = createLogger({ + name: "AuthorizeNetWebhookRequestProcessor", + }); + + const headers = this.request.headers; + const xAnetSignature = headers[this.authorizeSignature]; + if (!xAnetSignature) { + throw new AuthorizeNetInvalidWebhookSignatureError( + `Missing ${this.authorizeSignature} header`, + ); + } + + logger.debug("Got xAnetSignature from webhook"); + + const body = this.request.body; + const hash = crypto + .createHmac("sha512", authorizeConfig.signatureKey) + .update(body) + .digest("base64"); + + const validSignature = `sha512=${hash}`; + + if (validSignature !== xAnetSignature) { + throw new AuthorizeNetInvalidWebhookSignatureError("Invalid signature"); + } + + logger.debug("Signature is valid"); + + const eventPayload = eventPayloadSchema.parse(body); + + return eventPayload; + } + + private async getAuthorizeConfig() { + if (this.authorizeConfig) { + return this.authorizeConfig; + } + + const authData = await this.getAuthData(); + const channelSlug = "default-channel"; // todo: get rid of channelSlug + + const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); + const appConfigurator = await appConfigMetadataManager.get(); + const appConfig = appConfigurator.rootData; + + const authorizeConfig = resolveAuthorizeConfigFromAppConfig({ appConfig, channelSlug }); + this.authorizeConfig = authorizeConfig; + + return authorizeConfig; + } + + private async processAuthorizeWebhook(eventPayload: EventPayload) { + const authData = await this.getAuthData(); + const authorizeConfig = await this.getAuthorizeConfig(); + + const client = createServerClient(authData.saleorApiUrl, authData.token); + + const synchronizer = new AuthorizeNetWebhookTransactionSynchronizer({ + client, + authorizeConfig, + }); + return synchronizer.synchronizeTransaction(eventPayload); + } + + async handle() { + const authorizeConfig = await this.getAuthorizeConfig(); + this.logger.debug("Decrypting webhook body..."); + const eventPayload = await this.decryptAuthorizeWebhookBody({ + authorizeConfig, + }); + + this.logger.debug("Webhook body decrypted"); + this.logger.trace({ eventPayload }, "Webhook body decrypted"); + + await this.processAuthorizeWebhook(eventPayload); + this.logger.info("Webhook processed successfully"); + } +} diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts similarity index 88% rename from src/modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts rename to src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts index 2dd1495..1fb12a1 100644 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-manager.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts @@ -11,9 +11,11 @@ import { } from "../authorize-net-config"; import { AuthorizeNetWebhookClient } from "./authorize-net-webhook-client"; import { MissingAppUrlError } from "./authorize-net-webhook-errors"; -import { AuthorizeNetWebhookUrlBuilder } from "./authorize-net-webhook-url-builder"; import { createLogger } from "@/lib/logger"; +/** + * @description This class is used to register and manage the webhook with Authorize.net + */ export class AuthorizeWebhookManager { private authData: AuthData; private appConfig: AppConfig.Shape; @@ -62,27 +64,24 @@ export class AuthorizeWebhookManager { throw new MissingAppUrlError("Missing appUrl needed for registering the webhook"); } - const urlBuilder = new AuthorizeNetWebhookUrlBuilder(); const webhookParams: AuthorizeNetWebhookInput = { eventTypes: [ "net.authorize.payment.authcapture.created", "net.authorize.payment.authorization.created", + "net.authorize.payment.capture.created", ], status: "active", - url: urlBuilder.buildFromParams({ - appUrl, - }), + url: `${appUrl}/api/webhooks/authorize`, }; return webhookParams; } public async register() { - // todo: bring back - // if (this.authorizeConfig.webhook) { - // this.logger.info("Webhook already registered"); - // return; - // } + if (this.authorizeConfig.webhook) { + this.logger.info("Webhook already registered"); + return; + } this.logger.debug("Registering webhook..."); diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts new file mode 100644 index 0000000..028fcd9 --- /dev/null +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts @@ -0,0 +1,77 @@ +import { type Client } from "urql"; +import { type AuthorizeProviderConfig, type AuthorizeNetEvent } from "../authorize-net-config"; +import { TransactionDetailsClient } from "../client/transaction-details-client"; +import { AuthorizeNetError } from "../authorize-net-error"; +import { type EventPayload } from "./authorize-net-webhook-handler"; +import { + type TransactionEventReportMutation, + type TransactionEventReportMutationVariables, + TransactionEventReportDocument, + TransactionEventTypeEnum, +} from "generated/graphql"; + +const TransactionEventReportMutationError = AuthorizeNetError.subclass( + "TransactionEventReportMutationError", +); + +/** + * @description This class is used to synchronize Authorize.net transactions with Saleor transactions + */ +export class AuthorizeNetWebhookTransactionSynchronizer { + private authorizeConfig: AuthorizeProviderConfig.FullShape; + private client: Client; + + constructor({ + authorizeConfig, + client, + }: { + authorizeConfig: AuthorizeProviderConfig.FullShape; + client: Client; + }) { + this.authorizeConfig = authorizeConfig; + this.client = client; + } + + private mapEventType(authorizeTransactionType: AuthorizeNetEvent): TransactionEventTypeEnum { + switch (authorizeTransactionType) { + // todo: + default: + return TransactionEventTypeEnum.AuthorizationActionRequired; + } + } + + private async transactionEventReport(variables: TransactionEventReportMutationVariables) { + const { error: mutationError } = await this.client + .mutation(TransactionEventReportDocument, variables) + .toPromise(); + + if (mutationError) { + throw new TransactionEventReportMutationError( + "Error while mapping the transaction in the authorize webhook handler.", + { cause: mutationError.message }, + ); + } + } + + private getAuthorizeTransaction({ id }: { id: string }) { + const transactionDetailsClient = new TransactionDetailsClient(this.authorizeConfig); + return transactionDetailsClient.getTransactionDetailsRequest({ transactionId: id }); + } + + async synchronizeTransaction(eventPayload: EventPayload) { + const transactionId = eventPayload.payload.id; + const authorizeTransaction = await this.getAuthorizeTransaction({ id: transactionId }); + + const saleorTransactionId = authorizeTransaction.transaction.refTransId; + const type = this.mapEventType(eventPayload.eventType); + + await this.transactionEventReport({ + amount: authorizeTransaction.transaction.authAmount, + availableActions: [], + pspReference: authorizeTransaction.transaction.refTransId, + time: authorizeTransaction.transaction.submitTimeLocal, + transactionId: saleorTransactionId, + type, + }); + } +} diff --git a/src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts b/src/modules/authorize-net/webhook/create-authorize-webhooks-fetch.ts similarity index 100% rename from src/modules/authorize-net/webhooks-client/create-authorize-webhooks-fetch.ts rename to src/modules/authorize-net/webhook/create-authorize-webhooks-fetch.ts diff --git a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts b/src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts deleted file mode 100644 index aaa6ed3..0000000 --- a/src/modules/authorize-net/webhooks-client/authorize-net-webhook-verifier.ts +++ /dev/null @@ -1,42 +0,0 @@ -import crypto from "crypto"; -import { AuthorizeNetInvalidWebhookSignatureError } from "../authorize-net-error"; -import { type AuthorizeProviderConfig } from "@/modules/authorize-net/authorize-net-config"; -import { createLogger } from "@/lib/logger"; - -export class AuthorizeWebhookVerifier { - private logger = createLogger({ - name: "AuthorizeWebhookVerifier", - }); - private authorizeSignature = "X-ANET-Signature"; - - constructor(private config: AuthorizeProviderConfig.FullShape) {} - - /** - * @see https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification - * @todo use in webhook handler - */ - async verifyAuthorizeWebhook(response: Response) { - const headers = response.headers; - - this.logger.debug({ headers }, "Webhook headers"); - const xAnetSignature = headers.get(this.authorizeSignature); - - if (!xAnetSignature) { - throw new AuthorizeNetInvalidWebhookSignatureError( - `Missing ${this.authorizeSignature} header`, - ); - } - - const body = await response.text(); - const hash = crypto - .createHmac("sha512", this.config.signatureKey) - .update(body) - .digest("base64"); - - const validSignature = `sha512=${hash}`; - - if (validSignature !== xAnetSignature) { - throw new AuthorizeNetInvalidWebhookSignatureError("Invalid signature"); - } - } -} diff --git a/src/modules/configuration/app-configurator.ts b/src/modules/configuration/app-configurator.ts index 18b4a82..c8ac3a9 100644 --- a/src/modules/configuration/app-configurator.ts +++ b/src/modules/configuration/app-configurator.ts @@ -13,7 +13,7 @@ export namespace AppConfig { } export class AppConfigurator { - private rootData: AppConfig.Shape = { + rootData: AppConfig.Shape = { providers: [], connections: [], }; diff --git a/src/modules/webhooks/transaction-cancelation-requested.ts b/src/modules/webhooks/transaction-cancelation-requested.ts index 6053658..991d833 100644 --- a/src/modules/webhooks/transaction-cancelation-requested.ts +++ b/src/modules/webhooks/transaction-cancelation-requested.ts @@ -75,7 +75,10 @@ export class TransactionCancelationRequestedService { const createTransactionClient = new CreateTransactionClient(this.authorizeConfig); - await createTransactionClient.createTransaction(transactionInput); + await createTransactionClient.createTransaction({ + transactionInput, + saleorTransactionId, + }); this.logger.debug("Successfully voided the transaction"); diff --git a/src/modules/webhooks/transaction-process-session.ts b/src/modules/webhooks/transaction-process-session.ts index 3603f7e..bf58803 100644 --- a/src/modules/webhooks/transaction-process-session.ts +++ b/src/modules/webhooks/transaction-process-session.ts @@ -63,15 +63,24 @@ export class TransactionProcessSessionService { */ private mapTransactionToWebhookResponse( response: GetTransactionDetailsResponse, - ): Pick { + ): TransactionProcessSessionResponse { + const baseResponse: Pick< + TransactionProcessSessionResponse, + "amount" | "message" | "pspReference" + > = { + amount: response.transaction.authAmount, + message: response.transaction.responseReasonDescription, + pspReference: response.transaction.refTransId, + }; + const { transactionStatus } = response.transaction; if (transactionStatus === "authorizedPendingCapture") { - return { result: "AUTHORIZATION_SUCCESS", actions: ["CANCEL", "REFUND"] }; + return { ...baseResponse, result: "AUTHORIZATION_SUCCESS", actions: ["CANCEL", "REFUND"] }; } if (transactionStatus === "FDSPendingReview") { - return { result: "AUTHORIZATION_REQUEST", actions: [] }; + return { ...baseResponse, result: "AUTHORIZATION_REQUEST", actions: [] }; } throw new TransactionProcessError(`Unexpected transaction status: ${transactionStatus}`); diff --git a/src/modules/webhooks/transaction-refund-requested.ts b/src/modules/webhooks/transaction-refund-requested.ts index c2ebbe5..0beb5fa 100644 --- a/src/modules/webhooks/transaction-refund-requested.ts +++ b/src/modules/webhooks/transaction-refund-requested.ts @@ -72,7 +72,7 @@ export class TransactionRefundRequestedService { const createTransactionClient = new CreateTransactionClient(this.authorizeConfig); - await createTransactionClient.createTransaction(transactionInput); + await createTransactionClient.createTransaction({ transactionInput, saleorTransactionId }); this.logger.debug("Successfully refunded the transaction"); diff --git a/src/pages/api/webhooks/authorize.ts b/src/pages/api/webhooks/authorize.ts new file mode 100644 index 0000000..f3ac786 --- /dev/null +++ b/src/pages/api/webhooks/authorize.ts @@ -0,0 +1,22 @@ +import { type NextApiRequest, type NextApiResponse } from "next"; +import { createLogger } from "@/lib/logger"; + +import { AuthorizeNetWebhookHandler } from "@/modules/authorize-net/webhook/authorize-net-webhook-handler"; + +const logger = createLogger({ + name: "AuthorizeNetWebhooksHandler", +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + logger.debug({ url: req.url }, "Received webhook request"); + const handler = new AuthorizeNetWebhookHandler(req); + + await handler.handle(); + res.status(200); + } catch (error) { + // eslint-disable-next-line @saleor/saleor-app/logger-leak + logger.error({ error }, "Error in webhook handler"); + res.status(500).json({ error }); + } +} diff --git a/src/pages/api/webhooks/transaction-cancelation-requested.ts b/src/pages/api/webhooks/transaction-cancelation-requested.ts index 39830e9..26bd09c 100644 --- a/src/pages/api/webhooks/transaction-cancelation-requested.ts +++ b/src/pages/api/webhooks/transaction-cancelation-requested.ts @@ -13,7 +13,7 @@ import { UntypedTransactionCancelationRequestedDocument, type TransactionCancelationRequestedEventFragment, } from "generated/graphql"; -import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhook/authorize-net-webhook-manager"; export const config = { api: { diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index b9c2d9b..d06cb39 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -3,7 +3,7 @@ import * as Sentry from "@sentry/nextjs"; import { normalizeError } from "@/errors"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; -import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhook/authorize-net-webhook-manager"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; diff --git a/src/pages/api/webhooks/transaction-process-session.ts b/src/pages/api/webhooks/transaction-process-session.ts index 6672537..7052164 100644 --- a/src/pages/api/webhooks/transaction-process-session.ts +++ b/src/pages/api/webhooks/transaction-process-session.ts @@ -1,6 +1,6 @@ import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next"; import * as Sentry from "@sentry/nextjs"; -import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhook/authorize-net-webhook-manager"; import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; diff --git a/src/pages/api/webhooks/transaction-refund-requested.ts b/src/pages/api/webhooks/transaction-refund-requested.ts index e469c3c..1f284bf 100644 --- a/src/pages/api/webhooks/transaction-refund-requested.ts +++ b/src/pages/api/webhooks/transaction-refund-requested.ts @@ -4,7 +4,7 @@ import { createLogger } from "@/lib/logger"; import { SynchronousWebhookResponseBuilder } from "@/lib/webhook-response-builder"; import { TransactionRefundRequestedError } from "@/modules/webhooks/transaction-refund-requested"; -import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhooks-client/authorize-net-webhook-manager"; +import { AuthorizeWebhookManager } from "@/modules/authorize-net/webhook/authorize-net-webhook-manager"; import { resolveAppConfigFromCtx } from "@/modules/configuration/app-config-resolver"; import { resolveAuthorizeConfigFromAppConfig } from "@/modules/configuration/authorize-config-resolver"; import { createAppWebhookManager } from "@/modules/webhooks/webhook-manager-service"; From e14ec6eba37d4895127835501edaef46d5b1efe8 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 2 Jan 2024 12:46:35 +0100 Subject: [PATCH 15/33] Add new webhook event for priorAuthCapture --- .../authorize-net/webhook/authorize-net-webhook-manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts index 1fb12a1..9f9bc5a 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts @@ -69,6 +69,7 @@ export class AuthorizeWebhookManager { "net.authorize.payment.authcapture.created", "net.authorize.payment.authorization.created", "net.authorize.payment.capture.created", + "net.authorize.payment.priorAuthCapture.created", ], status: "active", url: `${appUrl}/api/webhooks/authorize`, From 7a77a4d16ed95bb3b11f2e9b0c6fb366a2765b4e Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 4 Jan 2024 08:43:31 +0100 Subject: [PATCH 16/33] Refactor webhook signature verification and parsing --- .../webhook/authorize-net-webhook-handler.ts | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts index cb40366..b918352 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts @@ -29,7 +29,7 @@ export type EventPayload = z.infer; * @description This class is used to handle webhook calls from Authorize.net */ export class AuthorizeNetWebhookHandler { - private authorizeSignature = "X-ANET-Signature"; + private authorizeSignature = "x-anet-signature"; private authData: AuthData | null = null; private authorizeConfig: AuthorizeProviderConfig.FullShape | null = null; @@ -56,40 +56,54 @@ export class AuthorizeNetWebhookHandler { return authData; } - private async decryptAuthorizeWebhookBody({ - authorizeConfig, - }: { - authorizeConfig: AuthorizeProviderConfig.FullShape; - }) { - const logger = createLogger({ - name: "AuthorizeNetWebhookRequestProcessor", - }); - + /** + * @description This method follows the process described in the documentation: + * @see https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification + * @description I was unable to find the reason for why the verification works during a "test webhook" run, but fails on an actual webhook call. + */ + private async verifyWebhook() { + this.logger.debug("Verifying webhook signature..."); + const authorizeConfig = await this.getAuthorizeConfig(); const headers = this.request.headers; const xAnetSignature = headers[this.authorizeSignature]; + if (!xAnetSignature) { throw new AuthorizeNetInvalidWebhookSignatureError( `Missing ${this.authorizeSignature} header`, ); } - logger.debug("Got xAnetSignature from webhook"); + this.logger.debug("Got xAnetSignature from webhook"); const body = this.request.body; + this.logger.trace({ body }, "Got body from webhook"); + const hash = crypto .createHmac("sha512", authorizeConfig.signatureKey) - .update(body) - .digest("base64"); + .update(JSON.stringify(body)) + .digest("hex"); - const validSignature = `sha512=${hash}`; + const validSignature = `sha512=${hash.toUpperCase()}`; + // ! If this check fails, the webhook should not be processed because we can't be sure that it's coming from Authorize.net. However, due to the issue described in the function description, we will fall back to checking the URL of the webhook. + // todo: this should be captured in Sentry if (validSignature !== xAnetSignature) { - throw new AuthorizeNetInvalidWebhookSignatureError("Invalid signature"); + // throw new AuthorizeNetInvalidWebhookSignatureError("The signature does not match"); + this.logger.warn("The signature does not match"); } - logger.debug("Signature is valid"); + this.logger.debug("Webhook verified successfully"); + } + + private parseWebhookBody() { + const body = this.request.body; + const parseResult = eventPayloadSchema.safeParse(body); + + if (!parseResult.success) { + throw new AuthorizeNetInvalidWebhookSignatureError("Unexpected shape of the webhook body"); + } - const eventPayload = eventPayloadSchema.parse(body); + const eventPayload = parseResult.data; return eventPayload; } @@ -126,16 +140,10 @@ export class AuthorizeNetWebhookHandler { } async handle() { - const authorizeConfig = await this.getAuthorizeConfig(); - this.logger.debug("Decrypting webhook body..."); - const eventPayload = await this.decryptAuthorizeWebhookBody({ - authorizeConfig, - }); - - this.logger.debug("Webhook body decrypted"); - this.logger.trace({ eventPayload }, "Webhook body decrypted"); - + await this.verifyWebhook(); + const eventPayload = this.parseWebhookBody(); await this.processAuthorizeWebhook(eventPayload); - this.logger.info("Webhook processed successfully"); + + this.logger.info("Finished processing webhook"); } } From 801265449761378706cc4bcaf53474dfa981150a Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 4 Jan 2024 09:19:37 +0100 Subject: [PATCH 17/33] Refactor createTransaction method in CreateTransactionClient --- .../authorize-net/client/create-transaction.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/modules/authorize-net/client/create-transaction.ts b/src/modules/authorize-net/client/create-transaction.ts index e6c84ad..5a9f294 100644 --- a/src/modules/authorize-net/client/create-transaction.ts +++ b/src/modules/authorize-net/client/create-transaction.ts @@ -8,24 +8,11 @@ const ApiControllers = AuthorizeNet.APIControllers; const createTransactionSchema = baseAuthorizeObjectSchema.and(z.unknown()); -type CreateTransactionResponse = z.infer; - export class CreateTransactionClient extends AuthorizeNetClient { - async createTransaction({ - transactionInput, - saleorTransactionId, - }: { - transactionInput: AuthorizeNet.APIContracts.TransactionRequestType; - saleorTransactionId: string; - }): Promise { + async createTransaction(transactionInput: AuthorizeNet.APIContracts.TransactionRequestType) { const createRequest = new ApiContracts.CreateTransactionRequest(); createRequest.setMerchantAuthentication(this.merchantAuthenticationType); createRequest.setTransactionRequest(transactionInput); - /** - * @description refId is needed to update the state of the transaction on Authorize.net webhook. - * @see AuthorizeNetWebhookHandler - */ - createRequest.setRefId(saleorTransactionId); const transactionController = new ApiControllers.CreateTransactionController( createRequest.getJSON(), From 4f90ee970a5f449d16e3971cf6c6f84d8669306a Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 4 Jan 2024 09:20:05 +0100 Subject: [PATCH 18/33] Remove logger.trace statement in TransactionInitializeSessionService --- src/modules/webhooks/transaction-initialize-session.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/webhooks/transaction-initialize-session.ts b/src/modules/webhooks/transaction-initialize-session.ts index dba9126..df25bd7 100644 --- a/src/modules/webhooks/transaction-initialize-session.ts +++ b/src/modules/webhooks/transaction-initialize-session.ts @@ -105,8 +105,6 @@ export class TransactionInitializeSessionService { transactionRequest.setProfile(profile); } - this.logger.trace("Finished building transaction request."); - return transactionRequest; } From 0592da785f3d105b2d3fc603f504e1d5cadbe7d5 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 4 Jan 2024 12:52:18 +0100 Subject: [PATCH 19/33] Update AuthorizeNetWebhookManager eventTypes --- src/modules/authorize-net/authorize-net-config.ts | 2 +- .../authorize-net/webhook/authorize-net-webhook-manager.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index 8780b57..f79e3e6 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -8,7 +8,7 @@ export const authorizeNetEventSchema = z.enum([ "net.authorize.payment.capture.created", "net.authorize.payment.refund.created", "net.authorize.payment.priorAuthCapture.created", - "net.authorize.payment.void.create", + "net.authorize.payment.void.created", ]); export type AuthorizeNetEvent = z.infer; diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts index 9f9bc5a..c07c4bb 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts @@ -66,10 +66,10 @@ export class AuthorizeWebhookManager { const webhookParams: AuthorizeNetWebhookInput = { eventTypes: [ - "net.authorize.payment.authcapture.created", - "net.authorize.payment.authorization.created", "net.authorize.payment.capture.created", "net.authorize.payment.priorAuthCapture.created", + "net.authorize.payment.void.created", + "net.authorize.payment.refund.created", ], status: "active", url: `${appUrl}/api/webhooks/authorize`, From 8bc85039426ec305e2e63989e9528d66fdb9bb06 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 4 Jan 2024 13:11:57 +0100 Subject: [PATCH 20/33] Add Transaction GraphQL fragment --- graphql/fragments/Transaction.graphql | 22 +++++++++++++++++++ ...ansactionCancelationRequestedEvent.graphql | 17 +------------- .../TransactionChargeRequestedEvent.graphql | 13 +---------- .../TransactionInitializeSessionEvent.graphql | 2 +- .../TransactionProcessSessionEvent.graphql | 2 +- .../TransactionRefundRequestedEvent.graphql | 17 +------------- 6 files changed, 27 insertions(+), 46 deletions(-) create mode 100644 graphql/fragments/Transaction.graphql diff --git a/graphql/fragments/Transaction.graphql b/graphql/fragments/Transaction.graphql new file mode 100644 index 0000000..df5e4e0 --- /dev/null +++ b/graphql/fragments/Transaction.graphql @@ -0,0 +1,22 @@ +fragment Transaction on TransactionItem { + id + pspReference + sourceObject: order { + ... on Order { + total { + gross { + ...Money + } + } + } + ...OrderOrCheckoutLines + } + privateMetadata { + key + value + } + authorizedAmount { + amount + currency + } +} diff --git a/graphql/fragments/TransactionCancelationRequestedEvent.graphql b/graphql/fragments/TransactionCancelationRequestedEvent.graphql index d47c3fa..cab19f0 100644 --- a/graphql/fragments/TransactionCancelationRequestedEvent.graphql +++ b/graphql/fragments/TransactionCancelationRequestedEvent.graphql @@ -8,21 +8,6 @@ fragment TransactionCancelationRequestedEvent on TransactionCancelationRequested amount } transaction { - id - pspReference - sourceObject: order { - channel { - id - slug - } - } - privateMetadata { - key - value - } - authorizedAmount { - amount - currency - } + ...Transaction } } diff --git a/graphql/fragments/TransactionChargeRequestedEvent.graphql b/graphql/fragments/TransactionChargeRequestedEvent.graphql index a8d3a18..1f316b8 100644 --- a/graphql/fragments/TransactionChargeRequestedEvent.graphql +++ b/graphql/fragments/TransactionChargeRequestedEvent.graphql @@ -8,17 +8,6 @@ fragment TransactionChargeRequestedEvent on TransactionChargeRequested { actionType } transaction { - id - pspReference - sourceObject: order { - ... on Order { - total { - gross { - ...Money - } - } - } - ...OrderOrCheckoutLines - } + ...Transaction } } diff --git a/graphql/fragments/TransactionInitializeSessionEvent.graphql b/graphql/fragments/TransactionInitializeSessionEvent.graphql index 03d96a9..9cebaa9 100644 --- a/graphql/fragments/TransactionInitializeSessionEvent.graphql +++ b/graphql/fragments/TransactionInitializeSessionEvent.graphql @@ -16,7 +16,7 @@ fragment TransactionInitializeSessionEvent on TransactionInitializeSession { } } transaction { - id + ...Transaction } sourceObject { __typename diff --git a/graphql/fragments/TransactionProcessSessionEvent.graphql b/graphql/fragments/TransactionProcessSessionEvent.graphql index da65873..6d91c99 100644 --- a/graphql/fragments/TransactionProcessSessionEvent.graphql +++ b/graphql/fragments/TransactionProcessSessionEvent.graphql @@ -11,7 +11,7 @@ fragment TransactionProcessSessionEvent on TransactionProcessSession { actionType } transaction { - id + ...Transaction } sourceObject { __typename diff --git a/graphql/fragments/TransactionRefundRequestedEvent.graphql b/graphql/fragments/TransactionRefundRequestedEvent.graphql index 96897cd..40e5b07 100644 --- a/graphql/fragments/TransactionRefundRequestedEvent.graphql +++ b/graphql/fragments/TransactionRefundRequestedEvent.graphql @@ -8,21 +8,6 @@ fragment TransactionRefundRequestedEvent on TransactionRefundRequested { actionType } transaction { - id - pspReference - sourceObject: order { - ... on Order { - total { - gross { - ...Money - } - } - } - ...OrderOrCheckoutLines - } - privateMetadata { - key - value - } + ...Transaction } } From 88ee40241011f2cd14a7e0c15b950750ff474344 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Thu, 4 Jan 2024 13:21:23 +0100 Subject: [PATCH 21/33] Add synchronized transaction functionality --- .../client/transaction-details-client.ts | 5 +- ...create-synchronized-transaction-request.ts | 31 +++++++++++++ .../saleor-transaction-id-converter.ts | 24 ++++++++++ .../synchronized-transaction-id-resolver.ts | 41 +++++++++++++++++ ...ze-net-webhook-transaction-synchronizer.ts | 9 +++- .../transaction-cancelation-requested.ts | 46 ++++++++----------- .../transaction-initialize-session.ts | 16 ++++++- .../webhooks/transaction-process-session.ts | 21 +++++---- .../webhooks/transaction-refund-requested.ts | 39 ++++++++-------- 9 files changed, 172 insertions(+), 60 deletions(-) create mode 100644 src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts create mode 100644 src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts create mode 100644 src/modules/authorize-net/synchronized-transaction/synchronized-transaction-id-resolver.ts diff --git a/src/modules/authorize-net/client/transaction-details-client.ts b/src/modules/authorize-net/client/transaction-details-client.ts index 87cf912..42d8fa6 100644 --- a/src/modules/authorize-net/client/transaction-details-client.ts +++ b/src/modules/authorize-net/client/transaction-details-client.ts @@ -9,11 +9,14 @@ const ApiControllers = AuthorizeNet.APIControllers; const getTransactionDetailsSchema = baseAuthorizeObjectSchema.and( z.object({ transaction: z.object({ + transId: z.string().min(1), transactionStatus: z.string().min(1), authAmount: z.number(), responseReasonDescription: z.string().min(1), - refTransId: z.string().min(1), submitTimeLocal: z.string().min(1), + order: z.object({ + description: z.string().min(1), + }), }), }), ); diff --git a/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts b/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts new file mode 100644 index 0000000..fe6dd52 --- /dev/null +++ b/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts @@ -0,0 +1,31 @@ +import AuthorizeNet from "authorizenet"; + +export const ApiContracts = AuthorizeNet.APIContracts; + +/** + * + * @description This function creates a transaction request that will include information needed to connect a Saleor transaction with an Authorize.net transaction. + * The saleorTransactionId is stored in the description field of the Authorize.net transaction. This is a hack. Unfortunately, no other field is fitting because the + * `refId` field is max. 20 characters long. + */ + +export function createSynchronizedTransactionRequest({ + saleorTransactionId, + authorizeTransactionId, +}: { + saleorTransactionId: string; + authorizeTransactionId?: string; +}) { + const transactionRequest = new ApiContracts.TransactionRequestType(); + + if (authorizeTransactionId) { + transactionRequest.setRefTransId(authorizeTransactionId); + } + + const order = new ApiContracts.OrderType(); + order.setDescription(saleorTransactionId); + + transactionRequest.setOrder(order); + + return transactionRequest; +} diff --git a/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts b/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts new file mode 100644 index 0000000..1798878 --- /dev/null +++ b/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts @@ -0,0 +1,24 @@ +import { type GetTransactionDetailsResponse } from "../client/transaction-details-client"; +import { type TransactionFragment } from "generated/graphql"; + +/** + * What you can see below is a classic example of a hack. + * + * We need to pass the saleorTransactionId to Authorize.net transaction so that we can + * later (in the Authorize → Saleor webhook) match the Authorize.net transaction with the Saleor transaction. + * + * The logical way to do it would be by using the `refId` field, but it's limited to 20 characters. Saleor transaction id is longer than that. + * Thus, why we use the `order.description` field, which is limited to 255 characters. + * + * `transactionIdConverter` makes sure the format of the string is the same on both sides. + */ +export const saleorTransactionIdConverter = { + fromSaleorTransaction(saleorTransaction: TransactionFragment) { + // remove last two characters + return saleorTransaction.id.slice(0, -2); // strip the last two "==" characters from the end of the string, as Authorize can't parse it 🤷 + }, + fromAuthorizeNetTransaction(authorizeTransaction: GetTransactionDetailsResponse) { + const orderDescription = authorizeTransaction.transaction.order.description; + return `${orderDescription}==`; // add the "==" characters back to the end of the string, so that it's a valid Saleor transaction ID. + }, +}; diff --git a/src/modules/authorize-net/synchronized-transaction/synchronized-transaction-id-resolver.ts b/src/modules/authorize-net/synchronized-transaction/synchronized-transaction-id-resolver.ts new file mode 100644 index 0000000..0a6f45a --- /dev/null +++ b/src/modules/authorize-net/synchronized-transaction/synchronized-transaction-id-resolver.ts @@ -0,0 +1,41 @@ +import { type Client } from "urql"; +import { TransactionMetadataManager } from "../../configuration/transaction-metadata-manager"; +import { type MetadataItem, type TransactionFragment } from "generated/graphql"; +import { BaseError } from "@/errors"; + +export const TransactionIdResolverError = BaseError.subclass("TransactionIdResolverError"); + +// The term "synchronized transaction" is used to describe a logic that is used to synchronize transaction between Saleor and Authorize.net. +export class SynchronizedTransactionIdResolver { + constructor(private apiClient: Client) {} + + private async getTransactionIdFromMetadata({ metadata }: { metadata: readonly MetadataItem[] }) { + const metadataManager = new TransactionMetadataManager({ apiClient: this.apiClient }); + const transactionId = await metadataManager.getAuthorizeTransactionId({ metadata }); + + return transactionId; + } + + async resolveFromTransaction(transaction: TransactionFragment) { + const saleorTransactionId = transaction?.id; + + if (!saleorTransactionId) { + throw new TransactionIdResolverError("Missing saleorTransactionId in payload"); + } + + const metadata = transaction.privateMetadata; + + if (!metadata) { + throw new TransactionIdResolverError("Missing metadata in payload"); + } + + const authorizeTransactionId = await this.getTransactionIdFromMetadata({ + metadata, + }); + + return { + authorizeTransactionId, + saleorTransactionId, + }; + } +} diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts index 028fcd9..fdd10e4 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-transaction-synchronizer.ts @@ -9,6 +9,7 @@ import { TransactionEventReportDocument, TransactionEventTypeEnum, } from "generated/graphql"; +import { saleorTransactionIdConverter } from "@/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter"; const TransactionEventReportMutationError = AuthorizeNetError.subclass( "TransactionEventReportMutationError", @@ -62,13 +63,17 @@ export class AuthorizeNetWebhookTransactionSynchronizer { const transactionId = eventPayload.payload.id; const authorizeTransaction = await this.getAuthorizeTransaction({ id: transactionId }); - const saleorTransactionId = authorizeTransaction.transaction.refTransId; + const saleorTransactionId = + saleorTransactionIdConverter.fromAuthorizeNetTransaction(authorizeTransaction); + + const authorizeTransactionId = authorizeTransaction.transaction.transId; + const type = this.mapEventType(eventPayload.eventType); await this.transactionEventReport({ amount: authorizeTransaction.transaction.authAmount, availableActions: [], - pspReference: authorizeTransaction.transaction.refTransId, + pspReference: authorizeTransactionId, time: authorizeTransaction.transaction.submitTimeLocal, transactionId: saleorTransactionId, type, diff --git a/src/modules/webhooks/transaction-cancelation-requested.ts b/src/modules/webhooks/transaction-cancelation-requested.ts index 991d833..d0d2f10 100644 --- a/src/modules/webhooks/transaction-cancelation-requested.ts +++ b/src/modules/webhooks/transaction-cancelation-requested.ts @@ -2,13 +2,12 @@ import AuthorizeNet from "authorizenet"; import { type Client } from "urql"; import { type AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; import { CreateTransactionClient } from "../authorize-net/client/create-transaction"; -import { TransactionMetadataManager } from "../configuration/transaction-metadata-manager"; -import { - type MetadataItem, - type TransactionCancelationRequestedEventFragment, -} from "generated/graphql"; +import { SynchronizedTransactionIdResolver } from "../authorize-net/synchronized-transaction/synchronized-transaction-id-resolver"; +import { createSynchronizedTransactionRequest } from "../authorize-net/synchronized-transaction/create-synchronized-transaction-request"; +import { type TransactionCancelationRequestedEventFragment } from "generated/graphql"; import { BaseError } from "@/errors"; +import { invariant } from "@/lib/invariant"; import { createLogger } from "@/lib/logger"; import { type TransactionCancelationRequestedResponse } from "@/schemas/TransactionCancelationRequested/TransactionCancelationRequestedResponse.mjs"; @@ -37,21 +36,19 @@ export class TransactionCancelationRequestedService { this.apiClient = apiClient; } - private async getTransactionIdFromMetadata({ metadata }: { metadata: readonly MetadataItem[] }) { - const metadataManager = new TransactionMetadataManager({ apiClient: this.apiClient }); - const transactionId = await metadataManager.getAuthorizeTransactionId({ metadata }); - - return transactionId; - } - private async buildTransactionFromPayload({ authorizeTransactionId, + saleorTransactionId, }: { authorizeTransactionId: string; + saleorTransactionId: string; }): Promise { - const transactionRequest = new ApiContracts.TransactionRequestType(); + const transactionRequest = createSynchronizedTransactionRequest({ + saleorTransactionId, + authorizeTransactionId, + }); + transactionRequest.setTransactionType(ApiContracts.TransactionTypeEnum.VOIDTRANSACTION); - transactionRequest.setRefTransId(authorizeTransactionId); return transactionRequest; } @@ -61,24 +58,21 @@ export class TransactionCancelationRequestedService { ): Promise { this.logger.debug({ id: payload.transaction?.id }, "Canceling transaction"); - const saleorTransactionId = payload.transaction?.id; + invariant(payload.transaction, "Transaction is missing"); - if (!saleorTransactionId) { - throw new TransactionCancelationRequestedError("Missing saleorTransactionId in payload"); - } + const idResolver = new SynchronizedTransactionIdResolver(this.apiClient); + const { saleorTransactionId, authorizeTransactionId } = await idResolver.resolveFromTransaction( + payload.transaction, + ); - const authorizeTransactionId = await this.getTransactionIdFromMetadata({ - metadata: payload.transaction.privateMetadata ?? [], + const transactionInput = await this.buildTransactionFromPayload({ + authorizeTransactionId, + saleorTransactionId, }); - const transactionInput = await this.buildTransactionFromPayload({ authorizeTransactionId }); - const createTransactionClient = new CreateTransactionClient(this.authorizeConfig); - await createTransactionClient.createTransaction({ - transactionInput, - saleorTransactionId, - }); + await createTransactionClient.createTransaction(transactionInput); this.logger.debug("Successfully voided the transaction"); diff --git a/src/modules/webhooks/transaction-initialize-session.ts b/src/modules/webhooks/transaction-initialize-session.ts index df25bd7..c0f0c1b 100644 --- a/src/modules/webhooks/transaction-initialize-session.ts +++ b/src/modules/webhooks/transaction-initialize-session.ts @@ -9,6 +9,8 @@ import { type GetHostedPaymentPageResponse, } from "../authorize-net/client/hosted-payment-page-client"; import { CustomerProfileManager } from "../customer-profile/customer-profile-manager"; +import { saleorTransactionIdConverter } from "../authorize-net/synchronized-transaction/saleor-transaction-id-converter"; +import { createSynchronizedTransactionRequest } from "../authorize-net/synchronized-transaction/create-synchronized-transaction-request"; import { type TransactionInitializeSessionEventFragment } from "generated/graphql"; import { BaseError } from "@/errors"; @@ -58,10 +60,22 @@ export class TransactionInitializeSessionService { private async buildTransactionFromPayload( payload: TransactionInitializeSessionEventFragment, ): Promise { - const transactionRequest = new ApiContracts.TransactionRequestType(); + const saleorTransactionId = saleorTransactionIdConverter.fromSaleorTransaction( + payload.transaction, + ); + + const transactionRequest = createSynchronizedTransactionRequest({ + saleorTransactionId, + }); + transactionRequest.setTransactionType(ApiContracts.TransactionTypeEnum.AUTHONLYTRANSACTION); transactionRequest.setAmount(payload.action.amount); + const order = new ApiContracts.OrderType(); + order.setDescription(saleorTransactionId); + + transactionRequest.setOrder(order); + const userEmail = payload.sourceObject.userEmail; if (!userEmail) { diff --git a/src/modules/webhooks/transaction-process-session.ts b/src/modules/webhooks/transaction-process-session.ts index bf58803..56baca2 100644 --- a/src/modules/webhooks/transaction-process-session.ts +++ b/src/modules/webhooks/transaction-process-session.ts @@ -1,5 +1,5 @@ -import { z } from "zod"; import { type Client } from "urql"; +import { z } from "zod"; import { type AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; import { TransactionDetailsClient, @@ -8,8 +8,8 @@ import { import { TransactionMetadataManager } from "../configuration/transaction-metadata-manager"; import { type TransactionProcessSessionEventFragment } from "generated/graphql"; import { type TransactionProcessSessionResponse } from "@/schemas/TransactionProcessSession/TransactionProcessSessionResponse.mjs"; -import { BaseError } from "@/errors"; import { createLogger } from "@/lib/logger"; +import { BaseError } from "@/errors"; export const TransactionProcessError = BaseError.subclass("TransactionProcessError"); @@ -41,9 +41,9 @@ export class TransactionProcessSessionService { } /** - * @description Saves Authorize transaction ID in metadata for future usage in other operations (e.g. `transaction-cancelation-requested`). + * @description Saves Authorize transaction ID in metadata for future usage in other operations (e.g. `transaction-cancelation-requested`). Also saves Saleor transaction ID in Authorize transaction as order.description. */ - private async saveTransactionIdInMetadata({ + private async synchronizeTransaction({ saleorTransactionId, authorizeTransactionId, }: { @@ -51,6 +51,7 @@ export class TransactionProcessSessionService { authorizeTransactionId: string; }) { const metadataManager = new TransactionMetadataManager({ apiClient: this.apiClient }); + await metadataManager.saveTransactionId({ saleorTransactionId, authorizeTransactionId, @@ -70,7 +71,7 @@ export class TransactionProcessSessionService { > = { amount: response.transaction.authAmount, message: response.transaction.responseReasonDescription, - pspReference: response.transaction.refTransId, + pspReference: response.transaction.transId, }; const { transactionStatus } = response.transaction; @@ -98,16 +99,16 @@ export class TransactionProcessSessionService { }); } - const { transactionId } = dataParseResult.data; + const { transactionId: authorizeTransactionId } = dataParseResult.data; - await this.saveTransactionIdInMetadata({ + await this.synchronizeTransaction({ saleorTransactionId: payload.transaction.id, - authorizeTransactionId: transactionId, + authorizeTransactionId, }); const transactionDetailsClient = new TransactionDetailsClient(this.authorizeConfig); const details = await transactionDetailsClient.getTransactionDetailsRequest({ - transactionId, + transactionId: authorizeTransactionId, }); const { result, actions } = this.mapTransactionToWebhookResponse(details); @@ -117,7 +118,7 @@ export class TransactionProcessSessionService { result, actions, message: details.transaction.responseReasonDescription, - pspReference: transactionId, + pspReference: authorizeTransactionId, }; } } diff --git a/src/modules/webhooks/transaction-refund-requested.ts b/src/modules/webhooks/transaction-refund-requested.ts index 0beb5fa..c602d82 100644 --- a/src/modules/webhooks/transaction-refund-requested.ts +++ b/src/modules/webhooks/transaction-refund-requested.ts @@ -2,12 +2,14 @@ import AuthorizeNet from "authorizenet"; import { type Client } from "urql"; import { type AuthorizeProviderConfig } from "../authorize-net/authorize-net-config"; import { CreateTransactionClient } from "../authorize-net/client/create-transaction"; -import { TransactionMetadataManager } from "../configuration/transaction-metadata-manager"; -import { type MetadataItem, type TransactionRefundRequestedEventFragment } from "generated/graphql"; +import { SynchronizedTransactionIdResolver } from "../authorize-net/synchronized-transaction/synchronized-transaction-id-resolver"; +import { createSynchronizedTransactionRequest } from "../authorize-net/synchronized-transaction/create-synchronized-transaction-request"; +import { type TransactionRefundRequestedEventFragment } from "generated/graphql"; import { BaseError } from "@/errors"; import { createLogger } from "@/lib/logger"; import { type TransactionRefundRequestedResponse } from "@/schemas/TransactionRefundRequested/TransactionRefundRequestedResponse.mjs"; +import { invariant } from "@/lib/invariant"; const ApiContracts = AuthorizeNet.APIContracts; @@ -34,21 +36,18 @@ export class TransactionRefundRequestedService { this.apiClient = apiClient; } - private async getTransactionIdFromMetadata({ metadata }: { metadata: readonly MetadataItem[] }) { - const metadataManager = new TransactionMetadataManager({ apiClient: this.apiClient }); - const transactionId = await metadataManager.getAuthorizeTransactionId({ metadata }); - - return transactionId; - } - private async buildTransactionFromPayload({ authorizeTransactionId, + saleorTransactionId, }: { authorizeTransactionId: string; + saleorTransactionId: string; }): Promise { - const transactionRequest = new ApiContracts.TransactionRequestType(); + const transactionRequest = createSynchronizedTransactionRequest({ + saleorTransactionId, + authorizeTransactionId, + }); transactionRequest.setTransactionType(ApiContracts.TransactionTypeEnum.VOIDTRANSACTION); - transactionRequest.setRefTransId(authorizeTransactionId); return transactionRequest; } @@ -58,21 +57,21 @@ export class TransactionRefundRequestedService { ): Promise { this.logger.debug({ id: payload.transaction?.id }, "Refunding the transaction"); - const saleorTransactionId = payload.transaction?.id; + invariant(payload.transaction, "Transaction is missing"); - if (!saleorTransactionId) { - throw new TransactionRefundRequestedError("Missing saleorTransactionId in payload"); - } + const idResolver = new SynchronizedTransactionIdResolver(this.apiClient); + const { saleorTransactionId, authorizeTransactionId } = await idResolver.resolveFromTransaction( + payload.transaction, + ); - const authorizeTransactionId = await this.getTransactionIdFromMetadata({ - metadata: payload.transaction.privateMetadata ?? [], + const transactionInput = await this.buildTransactionFromPayload({ + authorizeTransactionId, + saleorTransactionId, }); - const transactionInput = await this.buildTransactionFromPayload({ authorizeTransactionId }); - const createTransactionClient = new CreateTransactionClient(this.authorizeConfig); - await createTransactionClient.createTransaction({ transactionInput, saleorTransactionId }); + await createTransactionClient.createTransaction(transactionInput); this.logger.debug("Successfully refunded the transaction"); From e4f378492f560c1af2e599c10e82ce11f8f72d3a Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 5 Jan 2024 10:20:52 +0100 Subject: [PATCH 22/33] Refactor hosted payment page settings, hide order details --- .../client/hosted-payment-page-client.ts | 48 ++++--------------- .../transaction-initialize-session.ts | 43 ++++++++++++++++- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/modules/authorize-net/client/hosted-payment-page-client.ts b/src/modules/authorize-net/client/hosted-payment-page-client.ts index f09f4e0..782a59f 100644 --- a/src/modules/authorize-net/client/hosted-payment-page-client.ts +++ b/src/modules/authorize-net/client/hosted-payment-page-client.ts @@ -1,4 +1,3 @@ -import { env } from "process"; import AuthorizeNet from "authorizenet"; import { z } from "zod"; @@ -16,46 +15,17 @@ const getHostedPaymentPageResponseSchema = baseAuthorizeObjectSchema.and( export type GetHostedPaymentPageResponse = z.infer; export class HostedPaymentPageClient extends AuthorizeNetClient { - private getHostedPaymentPageSettings(): AuthorizeNet.APIContracts.ArrayOfSetting { - const settings = { - hostedPaymentReturnOptions: { - showReceipt: false, // must be false if we want to receive the transaction response in the payment form iframe - }, - hostedPaymentIFrameCommunicatorUrl: { - url: `${env.AUTHORIZE_PAYMENT_FORM_URL}/accept-hosted.html`, // url where the payment form iframe will be hosted, - }, - hostedPaymentCustomerOptions: { - showEmail: false, - requiredEmail: false, - addPaymentProfile: true, - }, - }; - - const settingsArray: AuthorizeNet.APIContracts.SettingType[] = []; - - Object.entries(settings).forEach(([settingName, settingValue]) => { - const setting = new ApiContracts.SettingType(); - setting.setSettingName(settingName); - setting.setSettingValue(JSON.stringify(settingValue)); - settingsArray.push(setting); - }); - - const arrayOfSettings = new ApiContracts.ArrayOfSetting(); - arrayOfSettings.setSetting(settingsArray); - - return arrayOfSettings; - } - - async getHostedPaymentPageRequest( - transactionInput: AuthorizeNet.APIContracts.TransactionRequestType, - ): Promise { + async getHostedPaymentPageRequest({ + transactionInput, + settingsInput, + }: { + transactionInput: AuthorizeNet.APIContracts.TransactionRequestType; + settingsInput: AuthorizeNet.APIContracts.ArrayOfSetting; + }): Promise { const createRequest = new ApiContracts.GetHostedPaymentPageRequest(); createRequest.setMerchantAuthentication(this.merchantAuthenticationType); createRequest.setTransactionRequest(transactionInput); - - const settings = this.getHostedPaymentPageSettings(); - - createRequest.setHostedPaymentSettings(settings); + createRequest.setHostedPaymentSettings(settingsInput); const transactionController = new ApiControllers.GetHostedPaymentPageController( createRequest.getJSON(), @@ -67,7 +37,7 @@ export class HostedPaymentPageClient extends AuthorizeNetClient { transactionController.execute(() => { try { this.logger.debug( - { settings }, + { settings: settingsInput }, "Calling getHostedPaymentPageRequest with the following settings:", ); diff --git a/src/modules/webhooks/transaction-initialize-session.ts b/src/modules/webhooks/transaction-initialize-session.ts index c0f0c1b..7d4e9b8 100644 --- a/src/modules/webhooks/transaction-initialize-session.ts +++ b/src/modules/webhooks/transaction-initialize-session.ts @@ -1,3 +1,4 @@ +import { env } from "process"; import AuthorizeNet from "authorizenet"; import { z } from "zod"; import { @@ -142,6 +143,41 @@ export class TransactionInitializeSessionService { return dataParseResult.data; } + private getHostedPaymentPageSettings(): AuthorizeNet.APIContracts.ArrayOfSetting { + const settings = { + hostedPaymentReturnOptions: { + showReceipt: false, // must be false if we want to receive the transaction response in the payment form iframe + }, + hostedPaymentIFrameCommunicatorUrl: { + url: `${env.AUTHORIZE_PAYMENT_FORM_URL}/accept-hosted.html`, // url where the payment form iframe will be hosted, + }, + hostedPaymentCustomerOptions: { + showEmail: false, + requiredEmail: false, + addPaymentProfile: true, + }, + hostedPaymentOrderOptions: { + /** we need to hide order details because we are using order.description to store the saleorTransactionId. + * @see: createSynchronizedTransactionRequest */ + show: false, + }, + }; + + const settingsArray: AuthorizeNet.APIContracts.SettingType[] = []; + + Object.entries(settings).forEach(([settingName, settingValue]) => { + const setting = new ApiContracts.SettingType(); + setting.setSettingName(settingName); + setting.setSettingValue(JSON.stringify(settingValue)); + settingsArray.push(setting); + }); + + const arrayOfSettings = new ApiContracts.ArrayOfSetting(); + arrayOfSettings.setSetting(settingsArray); + + return arrayOfSettings; + } + async execute( payload: TransactionInitializeSessionEventFragment, ): Promise { @@ -151,11 +187,14 @@ export class TransactionInitializeSessionService { ); const transactionInput = await this.buildTransactionFromPayload(payload); + const settingsInput = this.getHostedPaymentPageSettings(); const hostedPaymentPageClient = new HostedPaymentPageClient(this.authorizeConfig); - const hostedPaymentPageResponse = - await hostedPaymentPageClient.getHostedPaymentPageRequest(transactionInput); + const hostedPaymentPageResponse = await hostedPaymentPageClient.getHostedPaymentPageRequest({ + transactionInput, + settingsInput, + }); this.logger.trace("Successfully called getHostedPaymentPageRequest"); From 53801ef88e5b9557ea349bba200538d559003d75 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Fri, 5 Jan 2024 11:25:57 +0100 Subject: [PATCH 23/33] Refactor authorize-net-config and saleor-transaction-id-converter --- src/modules/authorize-net/authorize-net-config.ts | 2 +- .../saleor-transaction-id-converter.ts | 7 +++---- src/modules/webhooks/transaction-initialize-session.ts | 2 ++ 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/modules/authorize-net/authorize-net-config.ts b/src/modules/authorize-net/authorize-net-config.ts index f79e3e6..2adf6cb 100644 --- a/src/modules/authorize-net/authorize-net-config.ts +++ b/src/modules/authorize-net/authorize-net-config.ts @@ -13,7 +13,7 @@ export const authorizeNetEventSchema = z.enum([ export type AuthorizeNetEvent = z.infer; -export const webhookInputSchema = z.object({ +const webhookInputSchema = z.object({ url: z.string(), eventTypes: z.array(authorizeNetEventSchema), status: z.enum(["active", "inactive"]), diff --git a/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts b/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts index 1798878..580a257 100644 --- a/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts +++ b/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts @@ -14,11 +14,10 @@ import { type TransactionFragment } from "generated/graphql"; */ export const saleorTransactionIdConverter = { fromSaleorTransaction(saleorTransaction: TransactionFragment) { - // remove last two characters - return saleorTransaction.id.slice(0, -2); // strip the last two "==" characters from the end of the string, as Authorize can't parse it 🤷 + return btoa(saleorTransaction.id); // we need to encode the string to base64, because Authorize.net can't parse the "=" character that is in the Saleor transaction ID }, fromAuthorizeNetTransaction(authorizeTransaction: GetTransactionDetailsResponse) { - const orderDescription = authorizeTransaction.transaction.order.description; - return `${orderDescription}==`; // add the "==" characters back to the end of the string, so that it's a valid Saleor transaction ID. + const orderDescription = authorizeTransaction.transaction.order.description; // we need to decode it back to use the Saleor transaction ID + return atob(orderDescription); }, }; diff --git a/src/modules/webhooks/transaction-initialize-session.ts b/src/modules/webhooks/transaction-initialize-session.ts index 7d4e9b8..90c163f 100644 --- a/src/modules/webhooks/transaction-initialize-session.ts +++ b/src/modules/webhooks/transaction-initialize-session.ts @@ -65,6 +65,8 @@ export class TransactionInitializeSessionService { payload.transaction, ); + this.logger.trace({ saleorTransactionId }, "Saleor transaction id"); + const transactionRequest = createSynchronizedTransactionRequest({ saleorTransactionId, }); From c63097b264dbf5d0e043319bc7c2ae80648abac2 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 8 Jan 2024 10:37:34 +0100 Subject: [PATCH 24/33] Update README.md with Checkout UI instructions --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c917fe..f8401c1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ Important: -- Checkout UI relies on "Authorize transactions instead of charging" in the Saleor Dashboard -> Configuration -> Channels -> [channel] settings. +- The `example` Checkout UI relies on the "Authorize transactions instead of charging" setting in the Saleor Dashboard -> Configuration -> Channels -> [channel] settings. + +- The Saleor transaction id is stored in Authorize transaction `order.description`. Ideally, we would store it in `refId` but that field is max. 20 characters long and the Saleor transaction id is longer than that. From 5dd735535a547a0b10791a65244f95c887d750ba Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 8 Jan 2024 10:53:16 +0100 Subject: [PATCH 25/33] add comment --- .../authorize-net/webhook/authorize-net-webhook-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts index c07c4bb..939c864 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts @@ -58,7 +58,7 @@ export class AuthorizeWebhookManager { } private getWebhookParams() { - const appUrl = isDevelopment() ? env.LOCAL_APP_URL : `https://${env.VERCEL_URL}`; + const appUrl = isDevelopment() ? env.LOCAL_APP_URL : `https://${env.VERCEL_URL}`; // todo: get rid of it if (!appUrl) { throw new MissingAppUrlError("Missing appUrl needed for registering the webhook"); From 3cec9d2213c07a0113f4ac65a01aad8b49a7d901 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 8 Jan 2024 10:57:16 +0100 Subject: [PATCH 26/33] cleanup --- .../create-synchronized-transaction-request.ts | 1 + .../saleor-transaction-id-converter.ts | 2 -- .../webhook/authorize-net-webhook-client.ts | 15 --------------- .../webhook/authorize-net-webhook-errors.ts | 2 -- .../webhook/authorize-net-webhook-handler.ts | 3 ++- 5 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts b/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts index fe6dd52..10bc00e 100644 --- a/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts +++ b/src/modules/authorize-net/synchronized-transaction/create-synchronized-transaction-request.ts @@ -19,6 +19,7 @@ export function createSynchronizedTransactionRequest({ const transactionRequest = new ApiContracts.TransactionRequestType(); if (authorizeTransactionId) { + // refTransId is the transaction ID of the original transaction being referenced transactionRequest.setRefTransId(authorizeTransactionId); } diff --git a/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts b/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts index 580a257..26c870b 100644 --- a/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts +++ b/src/modules/authorize-net/synchronized-transaction/saleor-transaction-id-converter.ts @@ -2,8 +2,6 @@ import { type GetTransactionDetailsResponse } from "../client/transaction-detail import { type TransactionFragment } from "generated/graphql"; /** - * What you can see below is a classic example of a hack. - * * We need to pass the saleorTransactionId to Authorize.net transaction so that we can * later (in the Authorize → Saleor webhook) match the Authorize.net transaction with the Saleor transaction. * diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-client.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-client.ts index a7d66e5..4296d6e 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-client.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-client.ts @@ -17,10 +17,6 @@ const webhookResponseSchema = z }) .and(webhookSchema); -export type AuthorizeNetWebhookResponse = z.infer; - -const listWebhooksResponseSchema = z.array(webhookResponseSchema); - /** * @description Authorize.net has a separate API for registering webhooks. This class communicates with that API. * @see AuthorizeNetClient for managing transactions etc. @@ -50,15 +46,4 @@ export class AuthorizeNetWebhookClient { return parsedResult; } - - async listWebhooks() { - const response = await this.fetch({ - method: "GET", - }); - - const result = await response.json(); - const parsedResult = listWebhooksResponseSchema.parse(result); - - return parsedResult; - } } diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts index 63c97c2..628cf18 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-errors.ts @@ -2,8 +2,6 @@ import { BaseError } from "@/errors"; const AuthorizeNetWebhookError = BaseError.subclass("AuthorizeNetWebhookError", {}); -export const IncorrectQueryError = AuthorizeNetWebhookError.subclass("IncorrectQueryError", {}); - export const MissingAuthDataError = AuthorizeNetWebhookError.subclass("MissingAuthDataError", {}); export const MissingAppUrlError = AuthorizeNetWebhookError.subclass("MissingAppUrlError", {}); diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts index b918352..2219f9d 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts @@ -86,8 +86,9 @@ export class AuthorizeNetWebhookHandler { const validSignature = `sha512=${hash.toUpperCase()}`; // ! If this check fails, the webhook should not be processed because we can't be sure that it's coming from Authorize.net. However, due to the issue described in the function description, we will fall back to checking the URL of the webhook. - // todo: this should be captured in Sentry + // todo: revisit if (validSignature !== xAnetSignature) { + // todo: this should be captured in Sentry // throw new AuthorizeNetInvalidWebhookSignatureError("The signature does not match"); this.logger.warn("The signature does not match"); } From 9c2e4673895e24a7871b3a0afcf11d53f70f64f5 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 8 Jan 2024 11:04:58 +0100 Subject: [PATCH 27/33] fix test --- src/modules/configuration/app-configurator.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/configuration/app-configurator.test.ts b/src/modules/configuration/app-configurator.test.ts index 6532f26..0de3020 100644 --- a/src/modules/configuration/app-configurator.test.ts +++ b/src/modules/configuration/app-configurator.test.ts @@ -25,6 +25,7 @@ describe("AppConfigurator", () => { transactionKey: "transaction-key", environment: "sandbox", publicClientKey: "public-client-key", + signatureKey: "signature-key", webhook: null, }, ], From c6e645a5b3ddf656fa1b61ab1fdba82d8d6190b5 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Mon, 8 Jan 2024 15:50:30 +0100 Subject: [PATCH 28/33] build: :construction_worker: add changeset --- .changeset/funny-horses-destroy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/funny-horses-destroy.md diff --git a/.changeset/funny-horses-destroy.md b/.changeset/funny-horses-destroy.md new file mode 100644 index 0000000..61e9dd8 --- /dev/null +++ b/.changeset/funny-horses-destroy.md @@ -0,0 +1,5 @@ +--- +"saleor-app-authorize-net": minor +--- + +Added registering and handling Authorize.net webhooks. Enclosing the two-way synchronization between Saleor and Authorize.net transactions. Known issues: hard-coded channels, skipped webhook body verification. Both will be addressed in the next release. From a868e7e8003cbed5497660506c6eae5668fce771 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 9 Jan 2024 10:44:58 +0100 Subject: [PATCH 29/33] Update env.mjs file with required variables --- src/lib/env.mjs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/lib/env.mjs b/src/lib/env.mjs index 8ca26ea..9c5397b 100644 --- a/src/lib/env.mjs +++ b/src/lib/env.mjs @@ -25,36 +25,30 @@ export const env = createEnv({ UPSTASH_TOKEN: z.string().optional(), REST_APL_ENDPOINT: z.string().optional(), REST_APL_TOKEN: z.string().optional(), - // The following variables should be back to "optional" once the app features adding config through the UI. AUTHORIZE_API_LOGIN_ID: z .string() .min(1) - // .optional() .describe("API Login ID. You can find it in Account → API Credentials & Keys."), AUTHORIZE_TRANSACTION_KEY: z .string() .min(1) - // .optional() .describe( "Transaction key needed to authenticate Authorize.net SDK. You can generate it in Account → API Credentials & Keys.", ), AUTHORIZE_SIGNATURE_KEY: z .string() .min(1) - // .optional() .describe( "Signature Key needed to verify webhooks. You can generate it in Account → API Credentials & Keys.", ), AUTHORIZE_PUBLIC_CLIENT_KEY: z .string() .min(1) - // .optional() .describe("Public client key. You can generate it in Account -> Manage Public Client Key"), - AUTHORIZE_ENVIRONMENT: z.enum(["sandbox", "production"]) /*.optional()*/, + AUTHORIZE_ENVIRONMENT: z.enum(["sandbox", "production"]), AUTHORIZE_SALEOR_CHANNEL_SLUG: z .string() .min(1) - // .optional() .default("default-channel") .describe( "Saleor channel slug. When configuring the app through env, you can only use one channel.", @@ -62,7 +56,6 @@ export const env = createEnv({ AUTHORIZE_PAYMENT_FORM_URL: z .string() .min(1) - // .optional() .describe( "Payment form URL. This is the address your front-end UI is running on. Make sure it is on https. Otherwise the Accept Hosted form will not work.", ), From 18b9826e262d42371b28316f254300116621d34c Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 9 Jan 2024 12:13:20 +0100 Subject: [PATCH 30/33] Add micro package and its dependencies --- package.json | 1 + pnpm-lock.yaml | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/package.json b/package.json index 91b90a1..0bd316b 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "jose": "4.14.4", "jsdom": "22.1.0", "lodash-es": "4.17.21", + "micro": "10.0.1", "modern-errors": "6.0.0", "modern-errors-http": "4.0.0", "modern-errors-serialize": "5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17c1217..237d375 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,6 +88,9 @@ dependencies: lodash-es: specifier: 4.17.21 version: 4.17.21 + micro: + specifier: 10.0.1 + version: 10.0.1 modern-errors: specifier: 6.0.0 version: 6.0.0 @@ -4972,6 +4975,10 @@ packages: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: true + /arg@4.1.0: + resolution: {integrity: sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==} + dev: false + /arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} dev: true @@ -5363,6 +5370,11 @@ packages: dependencies: streamsearch: 1.1.0 + /bytes@3.1.0: + resolution: {integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==} + engines: {node: '>= 0.8'} + dev: false + /bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -5682,6 +5694,11 @@ packages: safe-buffer: 5.2.1 dev: true + /content-type@1.0.4: + resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} + engines: {node: '>= 0.6'} + dev: false + /content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} @@ -5933,6 +5950,11 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + /depd@1.1.2: + resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==} + engines: {node: '>= 0.6'} + dev: false + /depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -7496,6 +7518,17 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /http-errors@1.7.3: + resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} + engines: {node: '>= 0.6'} + dependencies: + depd: 1.1.2 + inherits: 2.0.4 + setprototypeof: 1.1.1 + statuses: 1.5.0 + toidentifier: 1.0.0 + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -8582,6 +8615,16 @@ packages: engines: {node: '>= 0.6'} dev: true + /micro@10.0.1: + resolution: {integrity: sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q==} + engines: {node: '>= 16.0.0'} + hasBin: true + dependencies: + arg: 4.1.0 + content-type: 1.0.4 + raw-body: 2.4.1 + dev: false + /micromatch@4.0.5: resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} engines: {node: '>=8.6'} @@ -9519,6 +9562,16 @@ packages: engines: {node: '>= 0.6'} dev: true + /raw-body@2.4.1: + resolution: {integrity: sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.0 + http-errors: 1.7.3 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + /raw-body@2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} engines: {node: '>= 0.8'} @@ -10134,6 +10187,10 @@ packages: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} dev: true + /setprototypeof@1.1.1: + resolution: {integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==} + dev: false + /setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -10337,6 +10394,11 @@ packages: type-fest: 0.7.1 dev: false + /statuses@1.5.0: + resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} + engines: {node: '>= 0.6'} + dev: false + /statuses@2.0.1: resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} engines: {node: '>= 0.8'} @@ -10642,6 +10704,11 @@ packages: dependencies: is-number: 7.0.0 + /toidentifier@1.0.0: + resolution: {integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==} + engines: {node: '>=0.6'} + dev: false + /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} From f92db39eaad216aad638921c7dcf8aa54c88d34e Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 9 Jan 2024 12:18:34 +0100 Subject: [PATCH 31/33] Make webhook verification work --- .../webhook/authorize-net-webhook-handler.ts | 29 +++++++++---------- src/pages/api/webhooks/authorize.ts | 10 ++++++- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts index 2219f9d..7428a75 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-handler.ts @@ -1,5 +1,6 @@ import crypto from "crypto"; import { type AuthData } from "@saleor/app-sdk/APL"; +import { buffer } from "micro"; import { type NextApiRequest } from "next"; import { z } from "zod"; import { authorizeNetEventSchema, type AuthorizeProviderConfig } from "../authorize-net-config"; @@ -56,16 +57,20 @@ export class AuthorizeNetWebhookHandler { return authData; } + private async getRawBody() { + const rawBody = (await buffer(this.request)).toString(); + return rawBody; + } + /** * @description This method follows the process described in the documentation: * @see https://developer.authorize.net/api/reference/features/webhooks.html#Verifying_the_Notification - * @description I was unable to find the reason for why the verification works during a "test webhook" run, but fails on an actual webhook call. */ private async verifyWebhook() { this.logger.debug("Verifying webhook signature..."); const authorizeConfig = await this.getAuthorizeConfig(); const headers = this.request.headers; - const xAnetSignature = headers[this.authorizeSignature]; + const xAnetSignature = headers[this.authorizeSignature]?.toString(); if (!xAnetSignature) { throw new AuthorizeNetInvalidWebhookSignatureError( @@ -73,31 +78,25 @@ export class AuthorizeNetWebhookHandler { ); } - this.logger.debug("Got xAnetSignature from webhook"); - - const body = this.request.body; - this.logger.trace({ body }, "Got body from webhook"); + const rawBody = await this.getRawBody(); const hash = crypto .createHmac("sha512", authorizeConfig.signatureKey) - .update(JSON.stringify(body)) + .update(rawBody) .digest("hex"); const validSignature = `sha512=${hash.toUpperCase()}`; - // ! If this check fails, the webhook should not be processed because we can't be sure that it's coming from Authorize.net. However, due to the issue described in the function description, we will fall back to checking the URL of the webhook. - // todo: revisit if (validSignature !== xAnetSignature) { - // todo: this should be captured in Sentry - // throw new AuthorizeNetInvalidWebhookSignatureError("The signature does not match"); - this.logger.warn("The signature does not match"); + throw new AuthorizeNetInvalidWebhookSignatureError("The signature does not match"); } this.logger.debug("Webhook verified successfully"); } - private parseWebhookBody() { - const body = this.request.body; + private async parseWebhookBody() { + const rawBody = await this.getRawBody(); + const body = JSON.parse(rawBody); const parseResult = eventPayloadSchema.safeParse(body); if (!parseResult.success) { @@ -142,7 +141,7 @@ export class AuthorizeNetWebhookHandler { async handle() { await this.verifyWebhook(); - const eventPayload = this.parseWebhookBody(); + const eventPayload = await this.parseWebhookBody(); await this.processAuthorizeWebhook(eventPayload); this.logger.info("Finished processing webhook"); diff --git a/src/pages/api/webhooks/authorize.ts b/src/pages/api/webhooks/authorize.ts index f3ac786..88be6d0 100644 --- a/src/pages/api/webhooks/authorize.ts +++ b/src/pages/api/webhooks/authorize.ts @@ -7,13 +7,21 @@ const logger = createLogger({ name: "AuthorizeNetWebhooksHandler", }); +export const config = { + api: { + bodyParser: false /** Disables automatic body parsing, so we can use raw-body. + @see: authorize-net-webhook-handler.ts + */, + }, +}; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { logger.debug({ url: req.url }, "Received webhook request"); const handler = new AuthorizeNetWebhookHandler(req); await handler.handle(); - res.status(200); + res.status(200).end(); } catch (error) { // eslint-disable-next-line @saleor/saleor-app/logger-leak logger.error({ error }, "Error in webhook handler"); From 9bbd1231950d4c6fe1b0a03684031e00c8a48549 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 9 Jan 2024 12:43:37 +0100 Subject: [PATCH 32/33] address pr feedback --- .env.example | 3 +- README.md | 32 ++++++++++++++++++- src/lib/env.mjs | 4 +-- .../webhook/authorize-net-webhook-manager.ts | 6 ++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index ae89046..48e2886 100644 --- a/.env.example +++ b/.env.example @@ -6,4 +6,5 @@ AUTHORIZE_TRANSACTION_KEY= AUTHORIZE_PUBLIC_CLIENT_KEY= AUTHORIZE_ENVIRONMENT="sandbox" AUTHORIZE_SALEOR_CHANNEL_SLUG="default-channel" -AUTHORIZE_PAYMENT_FORM_URL="" \ No newline at end of file +AUTHORIZE_PAYMENT_FORM_URL="" +APP_API_BASE_URL="" diff --git a/README.md b/README.md index f8401c1..7bb1abd 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,34 @@ -Important: +# Authorize.net App + +## Development + +### Build + +```bash +pnpm run build +``` + +### Run + +#### Running the Authorize.net App + +```bash +pnpm run dev +``` + +The app will run on port 8000. + +#### Running the Authorize.net UI example + +```bash +cd example +pnpm run dev +``` + +> [!IMPORTANT] +> Both the example and the app need to be [tunneled](https://docs.saleor.io/docs/3.x/developer/extending/apps/developing-with-tunnels). + +## Important - The `example` Checkout UI relies on the "Authorize transactions instead of charging" setting in the Saleor Dashboard -> Configuration -> Channels -> [channel] settings. diff --git a/src/lib/env.mjs b/src/lib/env.mjs index 9c5397b..ffe1ace 100644 --- a/src/lib/env.mjs +++ b/src/lib/env.mjs @@ -19,7 +19,7 @@ export const env = createEnv({ .optional() .default("error"), VERCEL_URL: z.string().optional(), - LOCAL_APP_URL: z.string().optional(), + APP_API_BASE_URL: z.string().optional(), PORT: z.coerce.number().optional(), UPSTASH_URL: z.string().optional(), UPSTASH_TOKEN: z.string().optional(), @@ -85,7 +85,7 @@ export const env = createEnv({ CI: process.env.CI, APP_DEBUG: process.env.APP_DEBUG, VERCEL_URL: process.env.VERCEL_URL, - LOCAL_APP_URL: process.env.LOCAL_APP_URL, + APP_API_BASE_URL: process.env.APP_API_BASE_URL, PORT: process.env.PORT, UPSTASH_URL: process.env.UPSTASH_URL, UPSTASH_TOKEN: process.env.UPSTASH_TOKEN, diff --git a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts index 939c864..3537512 100644 --- a/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts +++ b/src/modules/authorize-net/webhook/authorize-net-webhook-manager.ts @@ -58,12 +58,14 @@ export class AuthorizeWebhookManager { } private getWebhookParams() { - const appUrl = isDevelopment() ? env.LOCAL_APP_URL : `https://${env.VERCEL_URL}`; // todo: get rid of it + const appUrl = isDevelopment() ? env.APP_API_BASE_URL : `https://${env.VERCEL_URL}`; // todo: get rid of it if (!appUrl) { throw new MissingAppUrlError("Missing appUrl needed for registering the webhook"); } + const url = new URL("/api/webhooks/authorize", appUrl); + const webhookParams: AuthorizeNetWebhookInput = { eventTypes: [ "net.authorize.payment.capture.created", @@ -72,7 +74,7 @@ export class AuthorizeWebhookManager { "net.authorize.payment.refund.created", ], status: "active", - url: `${appUrl}/api/webhooks/authorize`, + url: url.href, }; return webhookParams; From a0db74e75ab18d58b7460dfab51b2bc7fbb2e6f3 Mon Sep 17 00:00:00 2001 From: Adrian Pilarczyk Date: Tue, 9 Jan 2024 13:19:25 +0100 Subject: [PATCH 33/33] Update amount in transaction initialize session webhook --- src/pages/api/webhooks/transaction-initialize-session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/api/webhooks/transaction-initialize-session.ts b/src/pages/api/webhooks/transaction-initialize-session.ts index d06cb39..f9b1f4a 100644 --- a/src/pages/api/webhooks/transaction-initialize-session.ts +++ b/src/pages/api/webhooks/transaction-initialize-session.ts @@ -82,7 +82,7 @@ export default transactionInitializeSessionSyncWebhook.createHandler( const normalizedError = normalizeError(error); return responseBuilder.ok({ - amount: 0, // 0 or real amount? + amount: ctx.payload.action.amount, result: "AUTHORIZATION_FAILURE", message: "Failure", data: {