├── .editorconfig ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── check-pr.yaml │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierrc ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── examples └── example-react │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ └── logo-pix.png │ ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── src ├── assembler.ts ├── crc.ts ├── create.ts ├── dynamicPayload.ts ├── emvHandler.ts ├── index.ts ├── parser.ts ├── types │ ├── helpers.ts │ ├── pixCreate.ts │ ├── pixDynamicPayload.ts │ ├── pixElements.ts │ ├── pixEmvSchema.ts │ ├── pixError.ts │ └── pixFunctions.ts ├── utils │ ├── generateErrorObject.ts │ ├── numToHex.ts │ ├── qrcodeGenerator.ts │ ├── textParser.ts │ └── zeroPad.ts └── validate.ts ├── tests ├── crc.test.ts ├── creator.test.ts ├── emvCodes.ts ├── extractor.test.ts ├── parser.test.ts └── throwIfError.test.ts ├── tsconfig.json ├── tsconfig.module.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["@typescript-eslint", "prettier"], 18 | "rules": { 19 | "prettier/prettier": "error" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | - **Summary** 8 | 9 | - **Other information** (e.g. detailed explanation, stack traces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | - **What is the current behavior?** (You can also link to an open issue here) 4 | 5 | - **What is the new behavior (if this is a feature change)?** 6 | 7 | - **Other information**: 8 | -------------------------------------------------------------------------------- /.github/workflows/check-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Check PR 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | check-pr: 8 | needs: tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set up Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | cache: yarn 21 | 22 | - name: Check commits messages 23 | uses: wagoid/commitlint-github-action@v6 24 | 25 | - name: Check PR name 26 | uses: amannn/action-semantic-pull-request@v5 27 | 28 | - name: Install dependencies 29 | run: yarn install --frozen-lockfile 30 | 31 | - name: Check formatting 32 | run: yarn format 33 | 34 | - name: Check linting 35 | run: yarn lint 36 | 37 | - name: Run build 38 | run: yarn build 39 | 40 | tests: 41 | uses: ./.github/workflows/tests.yaml 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - next 8 | 9 | jobs: 10 | tests: 11 | uses: ./.github/workflows/tests.yaml 12 | release: 13 | needs: tests 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | fetch-depth: 0 22 | 23 | - name: Set up Node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: lts/* 27 | cache: yarn 28 | 29 | - name: Install dependencies 30 | run: yarn install --frozen-lockfile 31 | 32 | - name: Check formatting 33 | run: yarn format 34 | 35 | - name: Check linting 36 | run: yarn lint 37 | 38 | - name: Run build 39 | run: yarn build 40 | 41 | - name: Run Semantic-Release 42 | run: yarn release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | workflow_call: 5 | 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x, 20.x, 22.x, 24.x] 13 | 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: yarn 23 | 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile --ignore-engines 26 | 27 | - name: Run Tests 28 | run: yarn test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node,osx 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,osx 3 | 4 | ## Build folder 5 | build 6 | 7 | ### Node ### 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | 138 | ### Node Patch ### 139 | # Serverless Webpack directories 140 | .webpack/ 141 | 142 | # Optional stylelint cache 143 | 144 | # SvelteKit build / generate output 145 | .svelte-kit 146 | 147 | ### OSX ### 148 | # General 149 | .DS_Store 150 | .AppleDouble 151 | .LSOverride 152 | 153 | # Icon must end with two \r 154 | Icon 155 | 156 | 157 | # Thumbnails 158 | ._* 159 | 160 | # Files that might appear in the root of a volume 161 | .DocumentRevisions-V100 162 | .fseventsd 163 | .Spotlight-V100 164 | .TemporaryItems 165 | .Trashes 166 | .VolumeIcon.icns 167 | .com.apple.timemachine.donotpresent 168 | 169 | # Directories potentially created on remote AFP share 170 | .AppleDB 171 | .AppleDesktop 172 | Network Trash Folder 173 | Temporary Items 174 | .apdisk 175 | 176 | # End of https://www.toptal.com/developers/gitignore/api/node,osx 177 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | npx --no -- commitlint --edit "${1}" 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx lint-staged 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "plugins": [ 5 | "@semantic-release/commit-analyzer", 6 | "@semantic-release/release-notes-generator", 7 | "@semantic-release/npm", 8 | [ 9 | "@semantic-release/changelog", 10 | { 11 | "changelogFile": "CHANGELOG.md" 12 | } 13 | ], 14 | [ 15 | "@semantic-release/git", 16 | { 17 | "message": "chore(release): ${nextRelease.version} \n\n${nextRelease.notes}" 18 | } 19 | ], 20 | "@semantic-release/github" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.8.1](https://github.com/thalesog/pix-utils/compare/v2.8.0...v2.8.1) (2025-05-30) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * export pixDynamicPayload types and update PIXRecPayload structure ([#36](https://github.com/thalesog/pix-utils/issues/36)) ([8316595](https://github.com/thalesog/pix-utils/commit/8316595182f5aef338a85970766612e09ba3ee18)) 7 | 8 | # [2.8.0](https://github.com/thalesog/pix-utils/compare/v2.7.1...v2.8.0) (2025-05-28) 9 | 10 | 11 | ### Features 12 | 13 | * rename composite functionality to recurrence ([#35](https://github.com/thalesog/pix-utils/issues/35)) ([5ebee6a](https://github.com/thalesog/pix-utils/commit/5ebee6aebdf0ec877a6295e73d26c3bff9f6eb06)) 14 | 15 | ## [2.7.1](https://github.com/thalesog/pix-utils/compare/v2.7.0...v2.7.1) (2025-05-23) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * add new enums and types for Pix payload structure ([#34](https://github.com/thalesog/pix-utils/issues/34)) ([081e586](https://github.com/thalesog/pix-utils/commit/081e5867be9ac9e203429eb98919db760a6aeaa8)) 21 | 22 | # [2.7.0](https://github.com/thalesog/pix-utils/compare/v2.6.0...v2.7.0) (2025-05-22) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **ci:** correct test workflow file extension ([#33](https://github.com/thalesog/pix-utils/issues/33)) ([68bc907](https://github.com/thalesog/pix-utils/commit/68bc9078b016b06ca4ae463852b8946065029229)) 28 | 29 | 30 | ### Features 31 | 32 | * add funcionality for composite emv ([#32](https://github.com/thalesog/pix-utils/issues/32)) ([94d9587](https://github.com/thalesog/pix-utils/commit/94d9587019644739776ec880628ebb963caa3580)) 33 | 34 | # [2.6.0](https://github.com/thalesog/pix-utils/compare/v2.5.0...v2.6.0) (2024-08-19) 35 | 36 | 37 | ### Features 38 | 39 | * expose `oneTime` option to add the `Point of Initiation Method` tag to the EMV ([#25](https://github.com/thalesog/pix-utils/issues/25)) ([b1d3db5](https://github.com/thalesog/pix-utils/commit/b1d3db5eebe0bde3d1b6ae802eaf0d548272e64e)) 40 | 41 | # [2.5.0](https://github.com/thalesog/pix-utils/compare/v2.4.2...v2.5.0) (2023-01-06) 42 | 43 | 44 | ### Features 45 | 46 | * added fss field for generating and parsing brCodes ([#21](https://github.com/thalesog/pix-utils/issues/21)) ([55216e2](https://github.com/thalesog/pix-utils/commit/55216e2a6270c2c40e971f88c78496a53fb61baa)) 47 | 48 | ## [2.4.2](https://github.com/thalesog/pix-utils/compare/v2.4.1...v2.4.2) (2022-11-29) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * added textencoding polyfill ([#20](https://github.com/thalesog/pix-utils/issues/20)) ([e2edea6](https://github.com/thalesog/pix-utils/commit/e2edea6f66a9a1749b2d24b2d90de36011481e42)) 54 | 55 | ## [2.4.1](https://github.com/thalesog/pix-utils/compare/v2.4.0...v2.4.1) (2022-11-20) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * remove amount tag on no amount emv ([#19](https://github.com/thalesog/pix-utils/issues/19)) ([f90f98a](https://github.com/thalesog/pix-utils/commit/f90f98a2fe2378995202e7b2d96387dcadb778c2)) 61 | 62 | # [2.4.0](https://github.com/thalesog/pix-utils/compare/v2.3.3...v2.4.0) (2022-10-11) 63 | 64 | 65 | ### Features 66 | 67 | * merchantCity field normalized ([#17](https://github.com/thalesog/pix-utils/issues/17)) ([7f799a9](https://github.com/thalesog/pix-utils/commit/7f799a94f05867fd43d16a07972f75a7c8528644)) 68 | 69 | ## [2.3.3](https://github.com/thalesog/pix-utils/compare/v2.3.2...v2.3.3) (2022-08-28) 70 | 71 | 72 | ### Bug Fixes 73 | 74 | * undefined txid length ([#12](https://github.com/thalesog/pix-utils/issues/12)) ([25b5624](https://github.com/thalesog/pix-utils/commit/25b562460e9a89aa0103e07670c2c202d0e8c803)) 75 | 76 | ## [2.3.2](https://github.com/thalesog/pix-utils/compare/v2.3.1...v2.3.2) (2022-08-28) 77 | 78 | 79 | ### Bug Fixes 80 | 81 | * undefined throwIfError when object is ok ([#7](https://github.com/thalesog/pix-utils/issues/7)) ([1470770](https://github.com/thalesog/pix-utils/commit/14707708bcf7cb2cdca1e24a2f890c6c1ecb435f)) 82 | 83 | ## [2.3.1](https://github.com/thalesog/pix-utils/compare/v2.3.0...v2.3.1) (2022-08-28) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * missing build step ([#6](https://github.com/thalesog/pix-utils/issues/6)) ([96d8296](https://github.com/thalesog/pix-utils/commit/96d829688167166d1c33060a77b40e2ed0515333)) 89 | 90 | # [2.3.0](https://github.com/thalesog/pix-utils/compare/v2.2.2...v2.3.0) (2022-08-28) 91 | 92 | 93 | ### Features 94 | 95 | * throw if error fn ([#5](https://github.com/thalesog/pix-utils/issues/5)) ([5de222b](https://github.com/thalesog/pix-utils/commit/5de222b84217a1a21a55600957dbae6c2a442ea1)) 96 | 97 | # Changelog 98 | 99 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 100 | 101 | ## 2.1.0 (2021-09-27) 102 | 103 | 104 | ### Features 105 | 106 | * error type rewriten, pix type enum ([b70ab11](https://github.com/thalesog/pix-utils/commit/b70ab11cf61201e466e8c436135128932af93f6e)) 107 | * pix creation, some refactoring and directory organization ([c3957ea](https://github.com/thalesog/pix-utils/commit/c3957eaf80ce0ae6fdadb64549d50ab28a7c8139)) 108 | * pix to base64 image and dynamic payload fetch ([24746bc](https://github.com/thalesog/pix-utils/commit/24746bc152817e10d94cbdbf7c0616d48ab592b0)) 109 | 110 | 111 | ### Bug Fixes 112 | 113 | * fix missing `HTMLCanvasElement` ([0ac4854](https://github.com/thalesog/pix-utils/commit/0ac4854d5802e693e90610e3e56405037cd8a3ff)) 114 | * static pixKey and dynamic url parameters emv id ([657a087](https://github.com/thalesog/pix-utils/commit/657a08798b1aa0cad3a6c96496ece21ef1c654db)) 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Thales Ogliari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

pix-utils

2 | 3 | #

Pix-Utils

4 | 5 |

6 | 10 | License 14 | 15 | 16 | Stars 20 | 21 |
22 | 23 | Version 27 | 28 | 31 | Build Status 35 | 36 |

37 | 38 | > Pix-Utils is a set of tools to parse, generate and validate payments of Brazil Instant Payment System (Pix), making fast and simple to handle charges and proccess then in your project. 39 | 40 | # 🚀 Usage 41 | 42 | ### Install the package in your project 43 | 44 | ```sh 45 | yarn add pix-utils 46 | ``` 47 | 48 | ### Create Static Pix 49 | 50 | ```ts 51 | import { createStaticPix, hasError } from 'pix-utils'; 52 | 53 | const pix = createStaticPix({ 54 | merchantName: 'Thales Ogliari', 55 | merchantCity: 'Sao Paulo', 56 | pixKey: 'nubank@thalesog.com', 57 | infoAdicional: 'Gerado por Pix-Utils', 58 | transactionAmount: 1, 59 | }); 60 | 61 | if (!hasError(pix)) { 62 | const brCode = pix.toBRCode(); 63 | // 00020126650014br.gov.bcb.pix0119nubank@thalesog.com0220Gerado por Pix-Utils52040000530398654041.005802BR5914Thales Ogliari6009Sao Paulo62070503***63046069 64 | } 65 | ``` 66 | 67 | ### Create Dynamic Pix 68 | 69 | ```ts 70 | import { createDynamicPix, hasError } from 'pix-utils'; 71 | 72 | const pix = createDynamicPix({ 73 | merchantName: 'Thales Ogliari', 74 | merchantCity: 'Sao Paulo', 75 | url: 'https://pix.thalesogliari.com.br', 76 | }); 77 | 78 | if (!hasError(pix)) { 79 | const brCode = pix.toBRCode(); 80 | // 00020126540014br.gov.bcb.pix2532https://pix.thalesogliari.com.br5204000053039865802BR5914Thales Ogliari6009SAO PAULO62070503***63043FD3 81 | } 82 | ``` 83 | 84 | ### Throw errors 85 | 86 | By default, pix-utils wont throw an error when parsing an invalid pix, but you can enable it by using the `throwIfError` function. 87 | 88 | ```js 89 | import { createDynamicPix } from 'pix-utils'; 90 | 91 | const pix = createDynamicPix({ 92 | merchantName: 'Thales Ogliari', 93 | merchantCity: 'Sao Paulo', 94 | url: 'https://pix.thalesogliari.com.br', 95 | }).throwIfError(); 96 | 97 | const brCode = pix.toBRCode(); 98 | // 00020126540014br.gov.bcb.pix2532https://pix.thalesogliari.com.br5204000053039865802BR5914Thales Ogliari6009SAO PAULO62070503***63043FD3 99 | ``` 100 | 101 | ### Parse BRCode 102 | 103 | ```js 104 | const pix = parsePix( 105 | '00020126650014br.gov.bcb.pix0119nubank@thalesog.com0220Gerado por Pix-Utils52040000530398654041.005802BR5914Thales Ogliari6015SAO MIGUEL DO O62070503***6304059A' 106 | ); 107 | 108 | // { 109 | // type: 'STATIC', 110 | // merchantCategoryCode: '0000', 111 | // transactionCurrency: '986', 112 | // countryCode: 'BR', 113 | // merchantName: 'Thales Ogliari', 114 | // merchantCity: 'SAO MIGUEL DO O', 115 | // pixKey: 'nubank@thalesog.com', 116 | // transactionAmount: 1, 117 | // infoAdicional: 'Gerado por Pix-Utils', 118 | // txid: '***', 119 | // toBRCode: [Function: toBRCode], 120 | // toImage: [Function: toImage] 121 | // } 122 | ``` 123 | 124 | ### Export to Base64 Image 125 | 126 | ```js 127 | const pix = parsePix( 128 | '00020126650014br.gov.bcb.pix0119nubank@thalesog.com0220Gerado por Pix-Utils52040000530398654041.005802BR5914Thales Ogliari6015SAO MIGUEL DO O62070503***6304059A' 129 | ); 130 | 131 | pix.toImage(); 132 | // data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOQAAADkCAYAAACIV4iNAAAAAklEQVR4AewaftIAAAwHSURBVO3BQW4sy7LgQDKh/W... 133 | ``` 134 | 135 | # 🍰 Contributing 136 | 137 | Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/thalesog/pix-utils/compare). 138 | 139 | # 📝 License 140 | 141 | This project is under [MIT](https://github.com/thalesog/pix-utils/blob/master/LICENSE) license. 142 | 143 | # 144 | 145 |

146 | Developed with 💚 by @thalesog 🇧🇷 147 |

148 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /examples/example-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/example-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "pix-utils": "../../" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.17", 18 | "@types/react-dom": "^18.0.6", 19 | "@vitejs/plugin-react": "^2.0.1", 20 | "typescript": "^4.6.4", 21 | "vite": "^3.0.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/example-react/public/logo-pix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thalesog/pix-utils/ef792fb2f7607f6df85aaf7d024406fa6e7e6730/examples/example-react/public/logo-pix.png -------------------------------------------------------------------------------- /examples/example-react/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /examples/example-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createStaticPix, hasError } from "pix-utils"; 2 | import { useEffect, useState } from "react"; 3 | import "./App.css"; 4 | 5 | function App() { 6 | const [brCode, setBRCode] = useState(""); 7 | const [qrCode, setQRCode] = useState(""); 8 | 9 | useEffect(() => { 10 | const pix = createStaticPix({ 11 | merchantName: "Thales Ogliari", 12 | merchantCity: "Sao Miguel do", 13 | pixKey: "nubank@thalesog.com", 14 | infoAdicional: "Gerado por Pix-Utils", 15 | transactionAmount: 1, 16 | txid: "", 17 | }); 18 | 19 | if (!hasError(pix)) { 20 | setBRCode(pix.toBRCode()); 21 | pix.toImage().then((img) => { 22 | setQRCode(img); 23 | }); 24 | } 25 | }, []); 26 | 27 | return ( 28 |
29 |
30 | 31 | Pix logo 32 | 33 |
34 |

Pix Utils

35 |
36 |

Generated BRCode:

37 | {brCode} 38 | 39 |

Generated QRCode:

40 | 41 |
42 |
43 | ); 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /examples/example-react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | @media (prefers-color-scheme: light) { 60 | :root { 61 | color: #213547; 62 | background-color: #ffffff; 63 | } 64 | a:hover { 65 | color: #747bff; 66 | } 67 | button { 68 | background-color: #f9f9f9; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/example-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /examples/example-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/example-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /examples/example-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /examples/example-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "name": "pix-utils", 4 | "version": "2.8.1", 5 | "author": "Thales Ogliari", 6 | "description": "Set of tools to parse, generate and validate payments of Brazil Instant Payment System (Pix)", 7 | "keywords": [ 8 | "pix", 9 | "bacen", 10 | "pix-utils", 11 | "utils", 12 | "emv", 13 | "emvqr", 14 | "qrcode" 15 | ], 16 | "main": "dist/main/index.js", 17 | "typings": "dist/main/index.d.ts", 18 | "module": "dist/module/index.js", 19 | "homepage": "https://github.com/thalesog/pix-utils#readme", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/thalesog/pix-utils.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/thalesog/pix-utils/issues" 26 | }, 27 | "engines": { 28 | "npm": ">=6", 29 | "node": ">=12" 30 | }, 31 | "scripts": { 32 | "build": "run-p build:*", 33 | "build:main": "tsc -p tsconfig.json", 34 | "build:module": "tsc -p tsconfig.module.json", 35 | "lint": "eslint --ignore-path .gitignore \"src/**/*.ts\"", 36 | "format": "prettier --check --ignore-path .gitignore \"src/**/*.ts\"", 37 | "test": "vitest", 38 | "coverage": "vitest --coverage", 39 | "reset-hard": "git clean -dfx && git reset --hard && yarn install", 40 | "prepare-release": "run-s reset-hard test", 41 | "prepare": "husky || true", 42 | "release": "semantic-release" 43 | }, 44 | "dependencies": { 45 | "axios": "^1.7.4", 46 | "fast-text-encoding": "^1.0.6", 47 | "qrcode": "^1.5.4" 48 | }, 49 | "devDependencies": { 50 | "@commitlint/cli": "^19.4.0", 51 | "@commitlint/config-conventional": "^19.2.2", 52 | "@semantic-release/changelog": "^6.0.3", 53 | "@semantic-release/git": "^10.0.1", 54 | "@types/node": "^18.11.9", 55 | "@types/qrcode": "^1.5.5", 56 | "@typescript-eslint/eslint-plugin": "^5.43.0", 57 | "@typescript-eslint/parser": "^5.43.0", 58 | "@vitest/coverage-v8": "^2.0.5", 59 | "eslint": "8", 60 | "eslint-config-prettier": "^8.5.0", 61 | "eslint-plugin-prettier": "^4.2.1", 62 | "husky": "^9.1.4", 63 | "lint-staged": "^15.2.9", 64 | "npm-run-all": "^4.1.5", 65 | "prettier": "2", 66 | "semantic-release": "^24.1.0", 67 | "ts-node": "^10.9.2", 68 | "typescript": "^5.5.4", 69 | "vitest": "^2.0.5" 70 | }, 71 | "files": [ 72 | "dist/main", 73 | "dist/module", 74 | "!**/*.test.*", 75 | "!**/*.json", 76 | "CHANGELOG.md", 77 | "LICENSE", 78 | "README.md" 79 | ], 80 | "lint-staged": { 81 | "./src/**/*.ts": [ 82 | "yarn lint", 83 | "yarn format" 84 | ] 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/assembler.ts: -------------------------------------------------------------------------------- 1 | import { fetchPayload, fetchRecPayload } from './dynamicPayload'; 2 | import { createEmv } from './emvHandler'; 3 | import { PixElement, PixElementType, PixObject } from './types/pixElements'; 4 | import { toBase64 } from './utils/qrcodeGenerator'; 5 | 6 | function generateThrowFn( 7 | obj: PixElement[T] 8 | ): () => PixObject[T] { 9 | return () => ({ 10 | ...obj, 11 | throwIfError: () => obj, 12 | }); 13 | } 14 | 15 | export function generatePixObject( 16 | elements: PixElement[T] 17 | ): PixObject[T] { 18 | if (elements.type === PixElementType.INVALID) { 19 | throw new Error('Invalid Pix type'); 20 | } 21 | 22 | const emvCode = createEmv(elements); 23 | 24 | const generatedObject = { 25 | ...elements, 26 | toBRCode: () => emvCode, 27 | toImage: () => toBase64(emvCode), 28 | ...(elements.type === PixElementType.STATIC && elements.urlRec 29 | ? { 30 | fetchRecPayload: () => fetchRecPayload({ url: elements.urlRec }), 31 | } 32 | : {}), 33 | ...(elements.type === PixElementType.DYNAMIC 34 | ? { 35 | fetchPayload: ({ DPP, codMun }) => 36 | fetchPayload({ url: elements.url, DPP, codMun }), 37 | fetchRecPayload: () => fetchRecPayload({ url: elements.urlRec }), 38 | } 39 | : {}), 40 | ...(elements.type === PixElementType.RECURRENCE 41 | ? { 42 | fetchRecPayload: () => fetchRecPayload({ url: elements.urlRec }), 43 | } 44 | : {}), 45 | }; 46 | 47 | return { 48 | ...generatedObject, 49 | throwIfError: generateThrowFn(generatedObject), 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/crc.ts: -------------------------------------------------------------------------------- 1 | import 'fast-text-encoding'; 2 | import numToHex from './utils/numToHex'; 3 | 4 | const crcTable = [ 5 | 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 6 | 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 7 | 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 8 | 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 9 | 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 10 | 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 11 | 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 12 | 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 13 | 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 14 | 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 15 | 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 16 | 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 17 | 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 18 | 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 19 | 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 20 | 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 21 | 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 22 | 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 23 | 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 24 | 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 25 | 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 26 | 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 27 | 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 28 | 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 29 | 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 30 | 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 31 | 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 32 | 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 33 | 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, 34 | ]; 35 | 36 | export function computeCRC(str: string, invert = false): string { 37 | // normalize prev calculated crc 38 | if (str.match(/6304\w{4}$/)) return computeCRC(removePrevCRC(str), invert); 39 | const crc = new TextEncoder().encode(str).reduce((crc, c) => { 40 | const j = (c ^ (crc >> 8)) & 0xff; 41 | return crcTable[j] ^ (crc << 8); 42 | }, 0xffff); 43 | 44 | const answer = (crc ^ 0) & 0xffff; 45 | 46 | const hex = numToHex(answer, 4); 47 | 48 | if (invert) return hex.slice(2) + hex.slice(0, 2); 49 | 50 | return hex; 51 | } 52 | 53 | function removePrevCRC(str: string): string { 54 | if (str.match(/6304\w{4}$/)) { 55 | return str.replace(/\w{4}$/, ''); 56 | } 57 | return str; 58 | } 59 | -------------------------------------------------------------------------------- /src/create.ts: -------------------------------------------------------------------------------- 1 | import { generatePixObject } from './assembler'; 2 | import { 3 | CreateDynamicPixParams, 4 | CreateRecurrencePixParams, 5 | CreateStaticPixParams, 6 | } from './types/pixCreate'; 7 | import { 8 | DynamicPixEmvElements, 9 | PixRecurrenceObject, 10 | PixDynamicObject, 11 | PixElementType, 12 | PixStaticObject, 13 | StaticPixEmvElements, 14 | RecurrencePixEmvElements, 15 | } from './types/pixElements'; 16 | import { PixError } from './types/pixError'; 17 | import { generateErrorObject } from './utils/generateErrorObject'; 18 | 19 | const defaultPixFields = { 20 | merchantCategoryCode: '0000', 21 | transactionCurrency: '986', 22 | countryCode: 'BR', 23 | }; 24 | 25 | const defaultStaticFields = { 26 | ...defaultPixFields, 27 | isTransactionUnique: false, 28 | txid: '***', 29 | }; 30 | 31 | export function createStaticPix( 32 | params: CreateStaticPixParams 33 | ): PixStaticObject | PixError { 34 | if (params.merchantName.length > 25) 35 | return generateErrorObject('merchantName character limit exceeded (> 25)'); 36 | 37 | if (params.txid && params.txid.length > 25) 38 | return generateErrorObject('txid character limit exceeded (> 25)'); 39 | 40 | if (params.merchantCity === '') 41 | return generateErrorObject('merchantCity is required'); 42 | 43 | if (params.merchantCity.length > 15) 44 | return generateErrorObject('merchantCity character limit exceeded (> 15)'); 45 | 46 | const elements = { 47 | type: PixElementType.STATIC, 48 | ...defaultStaticFields, 49 | ...params, 50 | } as StaticPixEmvElements; 51 | 52 | return generatePixObject(elements) as PixStaticObject; 53 | } 54 | 55 | export function createDynamicPix( 56 | params: CreateDynamicPixParams 57 | ): PixDynamicObject | PixError { 58 | if (params.merchantName.length > 25) 59 | return generateErrorObject('merchantName character limit exceeded (> 25)'); 60 | 61 | if (params.merchantCity === '') 62 | return generateErrorObject('merchantCity is required'); 63 | 64 | if (params.merchantCity.length > 15) 65 | return generateErrorObject('merchantCity character limit exceeded (> 15)'); 66 | 67 | const elements = { 68 | type: PixElementType.DYNAMIC, 69 | ...defaultStaticFields, 70 | ...params, 71 | } as DynamicPixEmvElements; 72 | 73 | return generatePixObject(elements) as PixDynamicObject; 74 | } 75 | 76 | export function createRecurrencePix( 77 | params: CreateRecurrencePixParams 78 | ): PixRecurrenceObject | PixError { 79 | if (params.merchantName.length > 25) 80 | return generateErrorObject('merchantName character limit exceeded (> 25)'); 81 | 82 | if (params.merchantCity === '') 83 | return generateErrorObject('merchantCity is required'); 84 | 85 | if (params.merchantCity.length > 15) 86 | return generateErrorObject('merchantCity character limit exceeded (> 15)'); 87 | 88 | const elements = { 89 | type: PixElementType.RECURRENCE, 90 | ...defaultStaticFields, 91 | ...params, 92 | url: undefined, 93 | } as RecurrencePixEmvElements; 94 | 95 | return generatePixObject(elements) as PixRecurrenceObject; 96 | } 97 | -------------------------------------------------------------------------------- /src/dynamicPayload.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; 2 | import { Buffer } from 'buffer'; 3 | import { PIXPayload, PIXRecPayload } from './types/pixDynamicPayload'; 4 | import { PixError } from './types/pixError'; 5 | import { generateErrorObject } from './utils/generateErrorObject'; 6 | import zeroPad from './utils/zeroPad'; 7 | 8 | export type PIXFetchResults = { 9 | readonly jwsString: string; 10 | readonly jws: { 11 | readonly hdr: Uint8Array; 12 | readonly payload: Uint8Array; 13 | readonly signature: Uint8Array; 14 | }; 15 | readonly header: Record; 16 | readonly payload: PIXPayload; 17 | }; 18 | 19 | export type PIXFetchParams = { 20 | readonly url: string; 21 | readonly DPP?: string; 22 | readonly codMun?: number; 23 | }; 24 | 25 | export type PIXRecFetchResults = { 26 | readonly jwsString: string; 27 | readonly jws: { 28 | readonly hdr: Uint8Array; 29 | readonly payload: Uint8Array; 30 | readonly signature: Uint8Array; 31 | }; 32 | readonly header: Record; 33 | readonly payload: PIXRecPayload; 34 | }; 35 | 36 | export type PIXRecFetchParams = { 37 | readonly url: string; 38 | }; 39 | 40 | export async function fetchPayload({ 41 | url, 42 | DPP = new Date().toISOString().substring(0, 10), 43 | codMun = 5300108, 44 | }: PIXFetchParams): Promise { 45 | const axiosOptions: AxiosRequestConfig = { 46 | params: { 47 | DPP, 48 | codMun: zeroPad(codMun, 7), 49 | }, 50 | }; 51 | return axios 52 | .get(`https://${url}`, axiosOptions) 53 | .then(({ data, status }: AxiosResponse) => { 54 | if (status !== 200) return generateErrorObject('Status != 200'); 55 | return data; 56 | }) 57 | .then((jws: string) => { 58 | const parts = jws.split('.').map((b64) => Buffer.from(b64, 'base64')); 59 | const pixFetch: PIXFetchResults = { 60 | jwsString: jws, 61 | jws: { 62 | hdr: parts[0], 63 | payload: parts[1], 64 | signature: parts[2], 65 | }, 66 | header: JSON.parse(parts[0].toString()), 67 | payload: JSON.parse(parts[1].toString()) as PIXPayload, 68 | }; 69 | return pixFetch; 70 | }) 71 | .catch((error) => { 72 | return generateErrorObject(error.message); 73 | }); 74 | } 75 | 76 | export async function fetchRecPayload({ 77 | url, 78 | }: PIXRecFetchParams): Promise { 79 | return axios 80 | .get(`https://${url}`) 81 | .then(({ data, status }: AxiosResponse) => { 82 | if (status !== 200) return generateErrorObject('Status != 200'); 83 | return data; 84 | }) 85 | .then((jws: string) => { 86 | const parts = jws.split('.').map((b64) => Buffer.from(b64, 'base64')); 87 | const pixFetch: PIXRecFetchResults = { 88 | jwsString: jws, 89 | jws: { 90 | hdr: parts[0], 91 | payload: parts[1], 92 | signature: parts[2], 93 | }, 94 | header: JSON.parse(parts[0].toString()), 95 | payload: JSON.parse(parts[1].toString()) as PIXRecPayload, 96 | }; 97 | return pixFetch; 98 | }) 99 | .catch((error) => { 100 | return generateErrorObject(error.message); 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/emvHandler.ts: -------------------------------------------------------------------------------- 1 | import { computeCRC } from './crc'; 2 | import { PixElements, PixElementType } from './types/pixElements'; 3 | import { 4 | EmvAdditionalDataSchema, 5 | EmvMaiSchema, 6 | EmvSchema, 7 | ParsedTags, 8 | TagsWithSubTags, 9 | } from './types/pixEmvSchema'; 10 | import { normalizeCity, normalizeName } from './utils/textParser'; 11 | import zeroPad from './utils/zeroPad'; 12 | 13 | function generateEmvElement(elementId: number, value: string) { 14 | if (!value) return ''; 15 | const parsedElementId = zeroPad(elementId, 2); 16 | const parsedLength = zeroPad(value.length, 2); 17 | return `${parsedElementId}${parsedLength}${value}`; 18 | } 19 | 20 | function generateMAI(elements: PixElements): string { 21 | if (elements.type === PixElementType.STATIC) { 22 | return [ 23 | generateEmvElement(EmvMaiSchema.TAG_MAI_GUI, EmvMaiSchema.BC_GUI), 24 | generateEmvElement(EmvMaiSchema.TAG_MAI_PIXKEY, elements.pixKey), 25 | generateEmvElement(EmvMaiSchema.TAG_MAI_INFO_ADD, elements.infoAdicional), 26 | generateEmvElement(EmvMaiSchema.TAG_MAI_FSS, elements.fss), 27 | ].join(''); 28 | } else if ( 29 | elements.type === PixElementType.DYNAMIC || 30 | elements.type === PixElementType.RECURRENCE 31 | ) { 32 | return [ 33 | generateEmvElement(EmvMaiSchema.TAG_MAI_GUI, EmvMaiSchema.BC_GUI), 34 | generateEmvElement(EmvMaiSchema.TAG_MAI_URL, elements.url), 35 | ].join(''); 36 | } 37 | return undefined; 38 | } 39 | 40 | function generateAdditionalData(txid: string) { 41 | return generateEmvElement(EmvAdditionalDataSchema.TAG_TXID, txid || '***'); 42 | } 43 | 44 | export function createEmv(elements: PixElements): string { 45 | if ( 46 | ![ 47 | PixElementType.STATIC, 48 | PixElementType.DYNAMIC, 49 | PixElementType.RECURRENCE, 50 | ].includes(elements.type) 51 | ) 52 | return 'INVALID'; 53 | 54 | const emvElements = [ 55 | generateEmvElement(EmvSchema.TAG_INIT, '01'), 56 | generateEmvElement(EmvSchema.TAG_ONETIME, elements.oneTime ? '12' : ''), 57 | generateEmvElement(EmvSchema.TAG_MAI, generateMAI(elements)), 58 | generateEmvElement(EmvSchema.TAG_MCC, elements.merchantCategoryCode), 59 | generateEmvElement( 60 | EmvSchema.TAG_TRANSACTION_CURRENCY, 61 | elements.transactionCurrency 62 | ), 63 | elements.type === PixElementType.STATIC 64 | ? generateEmvElement( 65 | EmvSchema.TAG_TRANSACTION_AMOUNT, 66 | elements.transactionAmount > 0 67 | ? elements.transactionAmount.toFixed(2) 68 | : '' 69 | ) 70 | : '', 71 | generateEmvElement(EmvSchema.TAG_COUNTRY_CODE, elements.countryCode), 72 | generateEmvElement( 73 | EmvSchema.TAG_MERCHANT_NAME, 74 | normalizeName(elements.merchantName) 75 | ), 76 | generateEmvElement( 77 | EmvSchema.TAG_MERCHANT_CITY, 78 | normalizeCity(elements.merchantCity) 79 | ), 80 | 81 | generateEmvElement( 82 | EmvSchema.TAG_ADDITIONAL_DATA, 83 | generateAdditionalData( 84 | elements.type === PixElementType.STATIC ? elements.txid : '' 85 | ) 86 | ), 87 | elements.urlRec 88 | ? generateEmvElement( 89 | EmvSchema.TAG_UNRESERVED_TEMPLATE, 90 | [ 91 | generateEmvElement(EmvMaiSchema.TAG_MAI_GUI, EmvMaiSchema.BC_GUI), 92 | generateEmvElement(EmvMaiSchema.TAG_MAI_URL, elements.urlRec), 93 | ].join('') 94 | ) 95 | : '', 96 | generateEmvElement(EmvSchema.TAG_CRC, '0000'), 97 | ]; 98 | const generatedEmv = emvElements.join(''); 99 | return generatedEmv.replace(/\w{4}$/, computeCRC(generatedEmv)); 100 | } 101 | 102 | export function parseEmv({ 103 | emvCode, 104 | currentIndex = 0, 105 | currentData = {}, 106 | }): ParsedTags { 107 | const tag = +emvCode.substring(currentIndex, currentIndex + 2); 108 | const length = Number(emvCode.substring(currentIndex + 2, currentIndex + 4)); 109 | const value = emvCode.substring(currentIndex + 4, currentIndex + 4 + length); 110 | 111 | if (!length || !value.length || length !== value.length) { 112 | return { 113 | isValid: false, 114 | rawTags: currentData, 115 | }; 116 | } 117 | 118 | const newData = { 119 | ...currentData, 120 | [tag]: { 121 | tag: tag, 122 | length: length, 123 | value: value, 124 | ...(Object.values(TagsWithSubTags).includes(tag) 125 | ? { subTags: parseEmv({ emvCode: value }) } 126 | : {}), 127 | }, 128 | }; 129 | 130 | if (currentIndex + 4 + length === emvCode.length) { 131 | return { 132 | isValid: true, 133 | rawTags: newData, 134 | getTag: (tag: string | number) => newData?.[Number(tag)]?.value, 135 | getSubTag: (tag: string | number, mainTag: string | number) => 136 | newData?.[Number(mainTag)]?.subTags?.getTag(Number(tag)), 137 | }; 138 | } else { 139 | return parseEmv({ 140 | emvCode, 141 | currentIndex: currentIndex + 4 + length, 142 | currentData: newData, 143 | }); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createDynamicPix, 3 | createStaticPix, 4 | createRecurrencePix, 5 | } from './create'; 6 | export { parsePix } from './parser'; 7 | export { 8 | hasError, 9 | isStaticPix, 10 | isDynamicPix, 11 | isRecurrencePix, 12 | } from './validate'; 13 | export { PixError } from './types/pixError'; 14 | export * from './types/pixElements'; 15 | export * from './types/pixDynamicPayload'; 16 | -------------------------------------------------------------------------------- /src/parser.ts: -------------------------------------------------------------------------------- 1 | import { generatePixObject } from './assembler'; 2 | import { computeCRC } from './crc'; 3 | import { parseEmv } from './emvHandler'; 4 | import { 5 | PixElements, 6 | PixElementType, 7 | PixEmvMandatoryElements, 8 | PixObjects, 9 | } from './types/pixElements'; 10 | import { 11 | EmvAdditionalDataSchema, 12 | EmvMaiSchema, 13 | EmvSchema, 14 | ValidTags, 15 | } from './types/pixEmvSchema'; 16 | import { PixError } from './types/pixError'; 17 | import { generateErrorObject } from './utils/generateErrorObject'; 18 | import { hasElementError, isPix } from './validate'; 19 | 20 | export function parsePix(brCode: string): PixObjects | PixError { 21 | // Parse EMV Code 22 | const emvElements = parseEmv({ emvCode: brCode }); 23 | if (!emvElements.isValid) return generateErrorObject('invalid emv code'); 24 | 25 | // Validate CRC16 26 | const crc = computeCRC(brCode); 27 | if (crc !== emvElements.getTag(EmvSchema.TAG_CRC)) 28 | return generateErrorObject('invalid crc'); 29 | 30 | // Extract Elements 31 | const elements = extractElements(emvElements); 32 | 33 | if (hasElementError(elements)) return generateErrorObject(elements.message); 34 | 35 | return generatePixObject(elements) as PixObjects; 36 | } 37 | 38 | export function extractMandatoryElements( 39 | emvElements: ValidTags 40 | ): PixEmvMandatoryElements { 41 | return { 42 | merchantCategoryCode: emvElements.getTag(EmvSchema.TAG_MCC), 43 | transactionCurrency: emvElements.getTag(EmvSchema.TAG_TRANSACTION_CURRENCY), 44 | countryCode: emvElements.getTag(EmvSchema.TAG_COUNTRY_CODE), 45 | merchantName: emvElements.getTag(EmvSchema.TAG_MERCHANT_NAME), 46 | merchantCity: emvElements.getTag(EmvSchema.TAG_MERCHANT_CITY), 47 | }; 48 | } 49 | 50 | export function extractElements( 51 | emvElements: ValidTags 52 | ): PixElements | PixError { 53 | const basicElements = extractMandatoryElements(emvElements); 54 | const isRecurrence = isPix(emvElements, 'recurrence'); 55 | if (isPix(emvElements, 'static')) { 56 | const amountNumber = +emvElements.getTag(EmvSchema.TAG_TRANSACTION_AMOUNT); 57 | const transactionAmount = !isNaN(amountNumber) ? amountNumber : 0; 58 | return { 59 | type: PixElementType.STATIC, 60 | ...basicElements, 61 | pixKey: emvElements.getSubTag( 62 | EmvMaiSchema.TAG_MAI_PIXKEY, 63 | EmvSchema.TAG_MAI 64 | ), 65 | transactionAmount, 66 | infoAdicional: emvElements.getSubTag( 67 | EmvMaiSchema.TAG_MAI_INFO_ADD, 68 | EmvSchema.TAG_MAI 69 | ), 70 | txid: emvElements.getSubTag( 71 | EmvAdditionalDataSchema.TAG_TXID, 72 | EmvSchema.TAG_ADDITIONAL_DATA 73 | ), 74 | fss: emvElements.getSubTag(EmvMaiSchema.TAG_MAI_FSS, EmvSchema.TAG_MAI), 75 | urlRec: isRecurrence 76 | ? emvElements.getSubTag( 77 | EmvMaiSchema.TAG_MAI_URL, 78 | EmvSchema.TAG_UNRESERVED_TEMPLATE 79 | ) 80 | : undefined, 81 | }; 82 | } 83 | 84 | if (isPix(emvElements, 'dynamic')) { 85 | return { 86 | type: PixElementType.DYNAMIC, 87 | ...basicElements, 88 | url: emvElements.getSubTag(EmvMaiSchema.TAG_MAI_URL, EmvSchema.TAG_MAI), 89 | urlRec: isRecurrence 90 | ? emvElements.getSubTag( 91 | EmvMaiSchema.TAG_MAI_URL, 92 | EmvSchema.TAG_UNRESERVED_TEMPLATE 93 | ) 94 | : undefined, 95 | }; 96 | } 97 | 98 | if (isRecurrence) { 99 | return { 100 | type: PixElementType.RECURRENCE, 101 | ...basicElements, 102 | url: undefined, 103 | urlRec: emvElements.getSubTag( 104 | EmvMaiSchema.TAG_MAI_URL, 105 | EmvSchema.TAG_UNRESERVED_TEMPLATE 106 | ), 107 | }; 108 | } 109 | 110 | if (!isPix(emvElements, 'pix') || !isPix(emvElements, 'valid')) 111 | return generateErrorObject('invalid pix'); 112 | 113 | return generateErrorObject('error'); 114 | } 115 | -------------------------------------------------------------------------------- /src/types/helpers.ts: -------------------------------------------------------------------------------- 1 | export type ValueOf = T[keyof T]; 2 | -------------------------------------------------------------------------------- /src/types/pixCreate.ts: -------------------------------------------------------------------------------- 1 | export type CreateStaticPixParams = { 2 | merchantName: string; 3 | merchantCity: string; 4 | infoAdicional?: string; 5 | pixKey: string; 6 | pss?: string; 7 | txid?: string; 8 | fss?: string; 9 | transactionAmount: number; 10 | isTransactionUnique?: boolean; 11 | urlRec?: string; 12 | }; 13 | 14 | export type CreateDynamicPixParams = { 15 | merchantName: string; 16 | merchantCity: string; 17 | url: string; 18 | urlRec?: string; 19 | oneTime?: boolean; 20 | }; 21 | 22 | export type CreateRecurrencePixParams = { 23 | merchantName: string; 24 | merchantCity: string; 25 | oneTime?: boolean; 26 | urlRec: string; 27 | }; 28 | -------------------------------------------------------------------------------- /src/types/pixDynamicPayload.ts: -------------------------------------------------------------------------------- 1 | export enum PixDynamicStatus { 2 | ATIVA = 'ATIVA', 3 | CONCLUIDA = 'CONCLUIDA', 4 | REMOVIDA_PELO_USUARIO_RECEBEDOR = 'REMOVIDA_PELO_USUARIO_RECEBEDOR', 5 | REMOVIDA_PELO_PSP = 'REMOVIDA_PELO_PSP', 6 | } 7 | 8 | export enum Periodicidade { 9 | SEMANAL = 'SEMANAL', 10 | MENSAL = 'MENSAL', 11 | TRIMESTRAL = 'TRIMESTRAL', 12 | SEMESTRAL = 'SEMESTRAL', 13 | ANUAL = 'ANUAL', 14 | } 15 | 16 | export enum PoliticaRetentativa { 17 | NAO_PERMITE = 'NAO_PERMITE', 18 | PERMITE_3R_7D = 'PERMITE_3R_7D', 19 | } 20 | 21 | export enum StatusRec { 22 | CRIADA = 'CRIADA', 23 | APROVADA = 'APROVADA', 24 | REJEITADA = 'REJEITADA', 25 | EXPIRADA = 'EXPIRADA', 26 | CANCELADA = 'CANCELADA', 27 | } 28 | 29 | export enum ModalidadeAgente { 30 | AGTEC = 'AGTEC', 31 | AGTOT = 'AGTOT', 32 | AGPSS = 'AGPSS', 33 | } 34 | 35 | export enum ModalidadeAgenteTroco { 36 | AGTEC = 'AGTEC', 37 | AGTOT = 'AGTOT', 38 | } 39 | 40 | type InfoAdicional = { 41 | readonly nome: string; 42 | readonly valor: string; 43 | }; 44 | 45 | export type PIXPaylodParams = { 46 | readonly DPP?: string; 47 | readonly codMun?: string; 48 | }; 49 | 50 | export type DevedorPF = { 51 | readonly cpf: string; 52 | readonly nome: string; 53 | }; 54 | 55 | export type DevedorPJ = { 56 | readonly cnpj: string; 57 | readonly nome: string; 58 | }; 59 | 60 | export type Devedor = DevedorPF | DevedorPJ; 61 | 62 | export type RetiradaSaque = { 63 | readonly valor: string; 64 | readonly modalidadeAlteracao?: 0 | 1; 65 | readonly modalidadeAgente: ModalidadeAgente; 66 | readonly prestadorDoServicoDeSaque: string; 67 | }; 68 | 69 | export type RetiradaTroco = { 70 | readonly valor: string; 71 | readonly modalidadeAlteracao?: 0 | 1; 72 | readonly modalidadeAgente: ModalidadeAgenteTroco; 73 | readonly prestadorDoServicoDeSaque: string; 74 | }; 75 | 76 | export type Retirada = { 77 | readonly saque?: RetiradaSaque; 78 | readonly troco?: RetiradaTroco; 79 | }; 80 | 81 | export type PIXFuturePayload = { 82 | readonly revisao: number; 83 | readonly calendario: { 84 | readonly criacao: string; 85 | readonly apresentacao: string; 86 | readonly dataDeVencimento?: string; 87 | readonly validadeAposVencimento?: number; 88 | }; 89 | readonly devedor?: Devedor; 90 | readonly recebedor?: { 91 | readonly cpf?: string; 92 | readonly cnpj?: string; 93 | readonly nome: string; 94 | readonly nomeFantasia?: string; 95 | readonly logradouro: string; 96 | readonly cidade: string; 97 | readonly utf: string; 98 | readonly cep: string; 99 | }; 100 | readonly valor: { 101 | readonly original?: string; 102 | readonly multa?: string; 103 | readonly juros?: string; 104 | readonly abatimento?: string; 105 | readonly desconto?: string; 106 | readonly final: string; 107 | }; 108 | readonly chave: string; 109 | readonly txid: string; 110 | readonly solicitacaoPagador?: string; 111 | readonly infoAdicionais: readonly InfoAdicional[]; 112 | readonly status: PixDynamicStatus; 113 | }; 114 | 115 | export type PIXInstantPayload = { 116 | readonly revisao: number; 117 | readonly calendario: { 118 | readonly criacao: string; 119 | readonly apresentacao: string; 120 | readonly expiracao: number; 121 | }; 122 | readonly devedor?: Devedor; 123 | readonly valor: { 124 | readonly original: string; 125 | readonly modalidadeAlteracao?: 0 | 1; 126 | readonly retirada?: Retirada; 127 | }; 128 | readonly chave: string; 129 | readonly txid: string; 130 | readonly solicitacaoPagador?: string; 131 | readonly infoAdicionais?: readonly InfoAdicional[]; 132 | readonly status: PixDynamicStatus; 133 | }; 134 | 135 | export type PIXRecPayload = { 136 | readonly idRec: string; 137 | readonly vinculo: { 138 | readonly objeto: string; 139 | readonly devedor: Devedor; 140 | readonly contrato: string; 141 | }; 142 | readonly calendario: { 143 | readonly dataInicial: string; 144 | readonly dataFinal?: string; 145 | readonly periodicidade: Periodicidade; 146 | }; 147 | readonly valor?: { 148 | readonly valorRec?: string; 149 | readonly valorMinimoRecebedor?: string; 150 | }; 151 | readonly recebedor: { 152 | readonly ispbParticipante: string; 153 | readonly cnpj: string; 154 | readonly nome: string; 155 | }; 156 | readonly politicaRetentativa: PoliticaRetentativa; 157 | readonly atualizacao: { 158 | readonly status: StatusRec; 159 | readonly data: string; 160 | }[]; 161 | }; 162 | 163 | export type PIXPayload = PIXInstantPayload | PIXFuturePayload | PIXRecPayload; 164 | -------------------------------------------------------------------------------- /src/types/pixElements.ts: -------------------------------------------------------------------------------- 1 | import { ValueOf } from './helpers'; 2 | import { PixDynamicFn, PixRecurrenceFn, PixStaticFn } from './pixFunctions'; 3 | 4 | export interface PixEmvMandatoryElements { 5 | readonly merchantCategoryCode: string; // EL52 6 | readonly transactionCurrency: string; // EL53 7 | readonly countryCode: string; // EL58 8 | readonly merchantName: string; // EL59 9 | readonly merchantCity: string; // EL60 10 | } 11 | 12 | export interface PixEmvBasicElements extends PixEmvMandatoryElements { 13 | readonly oneTime?: boolean; // EL02 14 | } 15 | 16 | export enum PixElementType { 17 | DYNAMIC = 'DYNAMIC', 18 | STATIC = 'STATIC', 19 | RECURRENCE = 'RECURRENCE', 20 | INVALID = 'INVALID', 21 | } 22 | 23 | export interface DynamicPixEmvElements extends PixEmvBasicElements { 24 | readonly type: PixElementType.DYNAMIC; 25 | readonly url: string; 26 | readonly urlRec?: string; 27 | } 28 | 29 | export interface RecurrencePixEmvElements extends PixEmvBasicElements { 30 | readonly type: PixElementType.RECURRENCE; 31 | readonly url: undefined; 32 | readonly urlRec: string; 33 | } 34 | 35 | export interface StaticPixEmvElements extends PixEmvBasicElements { 36 | readonly type: PixElementType.STATIC; 37 | readonly transactionAmount?: number; // EL54 38 | readonly pixKey: string; 39 | readonly txid?: string; 40 | readonly infoAdicional?: string; 41 | readonly fss?: string; 42 | readonly urlRec?: string; 43 | } 44 | 45 | export interface InvalidPixEmvElements { 46 | readonly type: PixElementType.INVALID; 47 | readonly details: string; 48 | } 49 | 50 | export type PixElement = { 51 | [PixElementType.DYNAMIC]: DynamicPixEmvElements; 52 | [PixElementType.STATIC]: StaticPixEmvElements; 53 | [PixElementType.INVALID]: InvalidPixEmvElements; 54 | [PixElementType.RECURRENCE]: RecurrencePixEmvElements; 55 | }; 56 | export type PixElements = 57 | | StaticPixEmvElements 58 | | DynamicPixEmvElements 59 | | RecurrencePixEmvElements; 60 | 61 | export type PixObject = { 62 | [PixElementType.DYNAMIC]: DynamicPixEmvElements; 63 | [PixElementType.STATIC]: StaticPixEmvElements; 64 | [PixElementType.INVALID]: InvalidPixEmvElements; 65 | [PixElementType.RECURRENCE]: RecurrencePixEmvElements; 66 | }; 67 | 68 | export type PixObjects = ValueOf; 69 | 70 | export type PixStaticObject = StaticPixEmvElements & PixStaticFn; 71 | 72 | export type PixDynamicObject = DynamicPixEmvElements & PixDynamicFn; 73 | 74 | export type PixRecurrenceObject = RecurrencePixEmvElements & PixRecurrenceFn; 75 | -------------------------------------------------------------------------------- /src/types/pixEmvSchema.ts: -------------------------------------------------------------------------------- 1 | export enum EmvSchema { 2 | TAG_INIT = 0, 3 | TAG_ONETIME = 1, 4 | TAG_MAI = 26, 5 | TAG_MCC = 52, 6 | TAG_TRANSACTION_CURRENCY = 53, 7 | TAG_TRANSACTION_AMOUNT = 54, 8 | TAG_COUNTRY_CODE = 58, 9 | TAG_MERCHANT_NAME = 59, 10 | TAG_MERCHANT_CITY = 60, 11 | TAG_POSTAL_CODE = 61, 12 | TAG_UNRESERVED_TEMPLATE = 80, 13 | TAG_ADDITIONAL_DATA = 62, 14 | TAG_CRC = 63, 15 | } 16 | 17 | export enum EmvAdditionalDataSchema { 18 | TAG_TXID = 5, 19 | } 20 | 21 | export enum EmvMaiSchema { 22 | TAG_MAI_GUI = 0, 23 | TAG_MAI_PIXKEY = 1, 24 | TAG_MAI_INFO_ADD = 2, 25 | TAG_MAI_FSS = 3, 26 | TAG_MAI_URL = 25, 27 | BC_GUI = 'br.gov.bcb.pix', 28 | } 29 | 30 | export enum EmvMandatory { 31 | TAG_MCC = EmvSchema.TAG_MCC, // EL52 32 | TAG_TRANSACTION_CURRENCY = EmvSchema.TAG_TRANSACTION_CURRENCY, // EL53 33 | TAG_COUNTRY_CODE = EmvSchema.TAG_COUNTRY_CODE, //EL58 34 | TAG_MERCHANT_NAME = EmvSchema.TAG_MERCHANT_NAME, //EL59 35 | TAG_MERCHANT_CITY = EmvSchema.TAG_MERCHANT_CITY, //EL60 36 | TAG_POSTAL_CODE = EmvSchema.TAG_POSTAL_CODE, //EL61 37 | } 38 | 39 | export enum TagsWithSubTags { 40 | TAG_MAI = EmvSchema.TAG_MAI, //EL26 41 | TAG_ADDITIONAL_DATA = EmvSchema.TAG_ADDITIONAL_DATA, //EL62 42 | TAG_UT = EmvSchema.TAG_UNRESERVED_TEMPLATE, //EL80 43 | } 44 | 45 | export type ValidTags = { 46 | readonly isValid: true; 47 | readonly [key: number]: { 48 | readonly tag: number; 49 | readonly length: number; 50 | readonly value: string; 51 | }; 52 | readonly rawTags: { 53 | readonly [key: number]: { 54 | readonly tag: number; 55 | readonly length: number; 56 | readonly value: string; 57 | }; 58 | }; 59 | readonly getTag: (tag: number) => string; 60 | readonly getSubTag: (mainTag: number, tag: number) => string; 61 | }; 62 | 63 | export type InvalidTags = { 64 | readonly isValid: false; 65 | readonly rawTags: { 66 | readonly [key: number]: { 67 | readonly tag: number; 68 | readonly length: number; 69 | readonly value: string; 70 | }; 71 | }; 72 | }; 73 | 74 | export type ParsedTags = ValidTags | InvalidTags; 75 | -------------------------------------------------------------------------------- /src/types/pixError.ts: -------------------------------------------------------------------------------- 1 | export type PixError = { 2 | readonly error: boolean; 3 | readonly message: string; 4 | readonly throwIfError: () => never; 5 | }; 6 | -------------------------------------------------------------------------------- /src/types/pixFunctions.ts: -------------------------------------------------------------------------------- 1 | import { PIXFetchResults } from '../dynamicPayload'; 2 | import { 3 | PixDynamicObject, 4 | PixRecurrenceObject, 5 | PixStaticObject, 6 | } from './pixElements'; 7 | import { PixError } from './pixError'; 8 | 9 | export interface PixFnDefault { 10 | readonly toBRCode: () => string; 11 | readonly toImage: () => Promise; 12 | } 13 | 14 | export interface PixStaticFn extends PixFnDefault { 15 | readonly throwIfError: () => PixStaticObject; 16 | readonly fetchRecPayload?: () => Promise; 17 | } 18 | 19 | type FetchPayloadParams = { 20 | DPP: string; 21 | codMun: number; 22 | }; 23 | 24 | export interface PixDynamicFn extends PixFnDefault { 25 | readonly fetchPayload: ( 26 | params: FetchPayloadParams 27 | ) => Promise; 28 | readonly fetchRecPayload?: () => Promise; 29 | readonly throwIfError: () => PixDynamicObject; 30 | } 31 | 32 | export interface PixRecurrenceFn extends PixFnDefault { 33 | readonly fetchRecPayload: () => Promise; 34 | readonly throwIfError: () => PixRecurrenceObject; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/generateErrorObject.ts: -------------------------------------------------------------------------------- 1 | import { PixError } from '../types/pixError'; 2 | 3 | export function generateErrorObject(message: string): PixError { 4 | return { 5 | error: true, 6 | message, 7 | throwIfError: () => { 8 | throw new Error(message); 9 | }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/numToHex.ts: -------------------------------------------------------------------------------- 1 | export default function numToHex(n: number, digits?: number): string { 2 | const hex = n.toString(16).toUpperCase(); 3 | if (digits) { 4 | return ('0'.repeat(digits) + hex).slice(-digits); 5 | } 6 | return hex.length % 2 === 0 ? hex : '0' + hex; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/qrcodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import { toDataURL } from 'qrcode'; 2 | 3 | export async function toBase64(brCode: string): Promise { 4 | return toDataURL(brCode); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/textParser.ts: -------------------------------------------------------------------------------- 1 | function normalizeString(str: string) { 2 | return str 3 | .normalize('NFD') 4 | .replace(/[\u0300-\u036f]/g, '') 5 | .toUpperCase(); 6 | } 7 | 8 | export function normalizeCity(city: string) { 9 | return normalizeString(city).substring(0, 15); 10 | } 11 | 12 | export function normalizeName(name: string) { 13 | return normalizeString(name).substring(0, 25); 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/zeroPad.ts: -------------------------------------------------------------------------------- 1 | export default function zeroPad(value: number, size: number) { 2 | return `${value}`.padStart(size, '0'); 3 | } 4 | -------------------------------------------------------------------------------- /src/validate.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PixDynamicObject, 3 | PixElements, 4 | PixElementType, 5 | PixObjects, 6 | PixRecurrenceObject, 7 | PixStaticObject, 8 | } from './types/pixElements'; 9 | import { EmvMaiSchema, EmvSchema, ValidTags } from './types/pixEmvSchema'; 10 | import { PixError } from './types/pixError'; 11 | 12 | export function isPix( 13 | emvElements: ValidTags, 14 | test: 'pix' | 'valid' | 'static' | 'dynamic' | 'recurrence' 15 | ): boolean { 16 | if (!emvElements.getTag(EmvSchema.TAG_MAI)) return false; 17 | 18 | const isDynamic = emvElements.getSubTag( 19 | EmvMaiSchema.TAG_MAI_URL, 20 | EmvSchema.TAG_MAI 21 | ); 22 | const isStatic = emvElements.getSubTag( 23 | EmvMaiSchema.TAG_MAI_PIXKEY, 24 | EmvSchema.TAG_MAI 25 | ); 26 | 27 | const isRecurrence = emvElements.getSubTag( 28 | EmvMaiSchema.TAG_MAI_URL, 29 | EmvSchema.TAG_UNRESERVED_TEMPLATE 30 | ); 31 | 32 | switch (test) { 33 | case 'pix': 34 | return true; 35 | case 'valid': 36 | return !!isStatic || !!isDynamic || !!isRecurrence; 37 | case 'static': 38 | return !!isStatic; 39 | case 'dynamic': 40 | return !!isDynamic; 41 | case 'recurrence': 42 | return !!isRecurrence; 43 | default: 44 | return false; 45 | } 46 | } 47 | 48 | export function hasError( 49 | pixElement: PixObjects | PixError 50 | ): pixElement is PixError { 51 | return !!(pixElement as PixError).error; 52 | } 53 | 54 | export function hasElementError( 55 | pixElement: PixElements | PixError 56 | ): pixElement is PixError { 57 | return !!(pixElement as PixError).error; 58 | } 59 | 60 | export function isStaticPix( 61 | pixElement: PixObjects 62 | ): pixElement is PixStaticObject { 63 | return pixElement && pixElement.type === PixElementType.STATIC; 64 | } 65 | 66 | export function isDynamicPix( 67 | pixElement: PixObjects 68 | ): pixElement is PixDynamicObject { 69 | return pixElement && pixElement.type === PixElementType.DYNAMIC; 70 | } 71 | 72 | export function isRecurrencePix( 73 | pixElement: PixObjects 74 | ): pixElement is PixRecurrenceObject { 75 | return pixElement && pixElement.type === PixElementType.RECURRENCE; 76 | } 77 | -------------------------------------------------------------------------------- /tests/crc.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { computeCRC } from '../src/crc'; 3 | import { DYNAMIC_TEST_EMV, STATIC_TEST_EMV } from './emvCodes'; 4 | 5 | describe('CRC Calculator and Parser', () => { 6 | it('should be able to calculate crc', () => { 7 | const calculatedCrc = computeCRC(STATIC_TEST_EMV); 8 | 9 | expect(calculatedCrc).toEqual('0DC6'); 10 | }); 11 | 12 | it('should be able to replace wrong crc calculation', () => { 13 | const brCodeWithWrongCRC = DYNAMIC_TEST_EMV.replace('42C5', 'ABCD'); 14 | const calculatedCrc = computeCRC(brCodeWithWrongCRC); 15 | 16 | expect(calculatedCrc).toEqual('42C5'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/creator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { 3 | createRecurrencePix, 4 | createDynamicPix, 5 | createStaticPix, 6 | hasError, 7 | } from '../src'; 8 | import { 9 | DYNAMIC_ONE_TIME_TEST_EMV, 10 | RECURRENCE_DYNAMIC_UNRESERVED_EMV, 11 | RECURRENCE_REC_ONLY, 12 | RECURRENCE_STATIC_UNRESERVED_EMV2, 13 | DYNAMIC_TEST_EMV, 14 | DYNAMIC_TEST_NORMALIZED_NAME, 15 | STATIC_TEST_EMV, 16 | STATIC_TEST_NO_VALUE_EMV, 17 | STATIC_TEST_NO_VALUE_WITHFSS_EMV, 18 | } from './emvCodes'; 19 | 20 | describe('EMV Code Creation', () => { 21 | it('should be able to create a static pix from mandatory fields', () => { 22 | const staticPixFn = createStaticPix({ 23 | merchantName: 'Thales Ogliari', 24 | merchantCity: 'SÃO MIGUÉL DO O', 25 | pixKey: 'thalesog@me.com', 26 | infoAdicional: 'Pedido 123', 27 | transactionAmount: 10, 28 | }); 29 | 30 | expect(hasError(staticPixFn)).toBe(false); 31 | if (hasError(staticPixFn)) return; 32 | 33 | expect(staticPixFn.toBRCode()).toBe(STATIC_TEST_EMV); 34 | }); 35 | 36 | it('should be able to create a static pix from mandatory fields and empty txid', () => { 37 | const staticPixFn = createStaticPix({ 38 | merchantName: 'Thales Ogliari', 39 | merchantCity: 'SÃO MIGUÉL DO O', 40 | pixKey: 'thalesog@me.com', 41 | infoAdicional: 'Pedido 123', 42 | txid: '', 43 | transactionAmount: 10, 44 | }); 45 | 46 | expect(hasError(staticPixFn)).toBe(false); 47 | if (hasError(staticPixFn)) return; 48 | 49 | expect(staticPixFn.toBRCode()).toBe(STATIC_TEST_EMV); 50 | }); 51 | 52 | it('should be able to create a static pix from mandatory fields with no amount defined', () => { 53 | const staticPixFn = createStaticPix({ 54 | merchantName: 'Thales Ogliari', 55 | merchantCity: 'SÃO MIGUÉL DO O', 56 | pixKey: 'thalesog@me.com', 57 | infoAdicional: 'Pedido 123', 58 | txid: '', 59 | transactionAmount: 0, 60 | }); 61 | 62 | expect(hasError(staticPixFn)).toBe(false); 63 | if (hasError(staticPixFn)) return; 64 | 65 | expect(staticPixFn.toBRCode()).toBe(STATIC_TEST_NO_VALUE_EMV); 66 | }); 67 | 68 | it('should be able to create a static pix from mandatory fields with no amount defined and set fss', () => { 69 | const staticPixFn = createStaticPix({ 70 | merchantName: 'Thales Ogliari', 71 | merchantCity: 'SÃO MIGUÉL DO O', 72 | pixKey: 'thalesog@me.com', 73 | infoAdicional: 'Pedido 123', 74 | fss: '12341234', 75 | txid: '', 76 | transactionAmount: 0, 77 | }); 78 | 79 | expect(hasError(staticPixFn)).toBe(false); 80 | if (hasError(staticPixFn)) return; 81 | 82 | expect(staticPixFn.toBRCode()).toBe(STATIC_TEST_NO_VALUE_WITHFSS_EMV); 83 | }); 84 | 85 | it('should be able to create a dynamic pix from mandatory fields', () => { 86 | const dynamicPixFn = createDynamicPix({ 87 | merchantName: 'Thales Ogliari', 88 | merchantCity: 'SÃO MIGUÉL DO O', 89 | url: 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87', 90 | }); 91 | 92 | expect(hasError(dynamicPixFn)).toBe(false); 93 | if (hasError(dynamicPixFn)) return; 94 | 95 | expect(dynamicPixFn.toBRCode()).toBe(DYNAMIC_TEST_EMV); 96 | }); 97 | 98 | it('should be able to normalize city input', () => { 99 | const dynamicPixFn = createDynamicPix({ 100 | merchantName: 'Thales Ogliari', 101 | merchantCity: 'SÃO MIGUÉL DO O', 102 | url: 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87', 103 | }); 104 | 105 | expect(hasError(dynamicPixFn)).toBe(false); 106 | if (hasError(dynamicPixFn)) return; 107 | 108 | expect(dynamicPixFn.toBRCode()).toBe(DYNAMIC_TEST_EMV); 109 | }); 110 | 111 | it('should be able to normalize name input', () => { 112 | const dynamicPixFn = createDynamicPix({ 113 | merchantName: 'Bárbara Pelé', 114 | merchantCity: 'SÃO MIGUÉL DO O', 115 | url: 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87', 116 | }); 117 | 118 | expect(hasError(dynamicPixFn)).toBe(false); 119 | if (hasError(dynamicPixFn)) return; 120 | 121 | expect(dynamicPixFn.toBRCode()).toBe(DYNAMIC_TEST_NORMALIZED_NAME); 122 | }); 123 | 124 | it('should be able to create a dynamic pix with one time tag', () => { 125 | const dynamicPixFn = createDynamicPix({ 126 | merchantName: 'Thales Ogliari', 127 | merchantCity: 'SÃO MIGUÉL DO O', 128 | url: 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87', 129 | oneTime: true, 130 | }); 131 | 132 | expect(hasError(dynamicPixFn)).toBe(false); 133 | if (hasError(dynamicPixFn)) return; 134 | 135 | expect(dynamicPixFn.toBRCode()).toBe(DYNAMIC_ONE_TIME_TEST_EMV); 136 | }); 137 | 138 | it('should be able to create a recurrence pix from mandatory fields', () => { 139 | const recurrencePixFn = createDynamicPix({ 140 | url: 'qr-h.sandbox.pix.bcb.gov.br/rest/api/v2/7b2d64c4eb744a2d92a4dd5f8cfc4dfa', 141 | urlRec: 142 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/3d29b94249c54b3f8c533d729f59b5e5', 143 | merchantCity: 'BRASILIA', 144 | merchantName: 'FULANO DE TAL', 145 | oneTime: true, 146 | }); 147 | 148 | expect(hasError(recurrencePixFn)).toBe(false); 149 | if (hasError(recurrencePixFn)) return; 150 | 151 | expect(recurrencePixFn.toBRCode()).toBe(RECURRENCE_DYNAMIC_UNRESERVED_EMV); 152 | }); 153 | 154 | it('should be able to create a recurrence pix from mandatory fields', () => { 155 | const staticPixFn = createStaticPix({ 156 | urlRec: 'payload.psp.com/rec/3ec9d2f9-5f03-4e0e-820d-63a81e769e87', 157 | merchantCity: 'SÃO MIGUÉL DO O', 158 | merchantName: 'Thales Ogliari', 159 | pixKey: 'thalesog@me.com', 160 | transactionAmount: 10, 161 | }); 162 | 163 | expect(hasError(staticPixFn)).toBe(false); 164 | if (hasError(staticPixFn)) return; 165 | 166 | expect(staticPixFn.toBRCode()).toBe(RECURRENCE_STATIC_UNRESERVED_EMV2); 167 | }); 168 | 169 | it('should be able to create a recurrence pix for rec only', () => { 170 | const recurrencePixFn = createRecurrencePix({ 171 | urlRec: 'payload.psp.com/rec/3ec9d2f9-5f03-4e0e-820d-63a81e769e87', 172 | merchantCity: 'SÃO MIGUÉL DO O', 173 | merchantName: 'Thales Ogliari', 174 | }); 175 | 176 | expect(hasError(recurrencePixFn)).toBe(false); 177 | if (hasError(recurrencePixFn)) return; 178 | 179 | expect(recurrencePixFn.toBRCode()).toBe(RECURRENCE_REC_ONLY); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /tests/emvCodes.ts: -------------------------------------------------------------------------------- 1 | export const STATIC_TEST_EMV = 2 | '00020126510014br.gov.bcb.pix0115thalesog@me.com0210Pedido 123520400005303986540510.005802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***63040DC6'; 3 | 4 | export const STATIC_TEST_WITH_FSS = `00020126950014br.gov.bcb.pix01364004901d-bd85-4769-8e52-cb4c42c506dc0221Jornada pagador 431070308999990085204000053039865406577.575802BR5903Pix6008BRASILIA6229052599ed983ea570413d8d336301d630418B7`; 5 | 6 | export const STATIC_TEST_NO_VALUE_EMV = 7 | '00020126510014br.gov.bcb.pix0115thalesog@me.com0210Pedido 1235204000053039865802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***630462CA'; 8 | 9 | export const STATIC_TEST_NO_VALUE_WITHFSS_EMV = 10 | '00020126630014br.gov.bcb.pix0115thalesog@me.com0210Pedido 1230308123412345204000053039865802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***6304901C'; 11 | 12 | export const STATIC_TEST_NO_VALUE_ELEMENT_EMV = 13 | '00020126510014br.gov.bcb.pix0115thalesog@me.com0210Pedido 1235204000053039865802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***630462CA'; 14 | 15 | export const DYNAMIC_TEST_EMV = 16 | '00020126740014br.gov.bcb.pix2552payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e875204000053039865802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***630442C5'; 17 | 18 | export const DYNAMIC_TEST_NORMALIZED_NAME = 19 | '00020126740014br.gov.bcb.pix2552payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e875204000053039865802BR5912BARBARA PELE6015SAO MIGUEL DO O62070503***63040433'; 20 | 21 | export const DYNAMIC_ONE_TIME_TEST_EMV = 22 | '00020101021226740014br.gov.bcb.pix2552payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e875204000053039865802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***63042895'; 23 | 24 | export const RECURRENCE_STATIC_UNRESERVED_EMV = 25 | '00020126780014br.gov.bcb.pix0136f4c6089a-bfde-4c00-a2d9-9eaa584b02190216CobrancaEstatica5204000053039865406650.345802BR5903Pix6008BRASILIA6229052580a2d0a923984e8dbb80b4adf80950014br.gov.bcb.pix2573qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/d7913bcbfc4947c9811669db12b403746304595A'; 26 | 27 | export const RECURRENCE_STATIC_UNRESERVED_EMV2 = 28 | '00020126370014br.gov.bcb.pix0115thalesog@me.com520400005303986540510.005802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***80780014br.gov.bcb.pix2556payload.psp.com/rec/3ec9d2f9-5f03-4e0e-820d-63a81e769e876304B6DE'; 29 | 30 | export const RECURRENCE_DYNAMIC_UNRESERVED_EMV = 31 | '00020101021226940014br.gov.bcb.pix2572qr-h.sandbox.pix.bcb.gov.br/rest/api/v2/7b2d64c4eb744a2d92a4dd5f8cfc4dfa5204000053039865802BR5913FULANO DE TAL6008BRASILIA62070503***80950014br.gov.bcb.pix2573qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/3d29b94249c54b3f8c533d729f59b5e56304DBE0'; 32 | 33 | export const RECURRENCE_DYNAMIC_UNRESERVED_REC = 34 | '00020126180014br.gov.bcb.pix5204000053039865802BR5913Fulano de Tal6008BRASILIA62070503***80950014br.gov.bcb.pix2573qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/5ee5232ead29422396b44f5eb67180d663041AD9'; 35 | 36 | export const RECURRENCE_REC_ONLY = 37 | '00020126180014br.gov.bcb.pix5204000053039865802BR5914THALES OGLIARI6015SAO MIGUEL DO O62070503***80780014br.gov.bcb.pix2556payload.psp.com/rec/3ec9d2f9-5f03-4e0e-820d-63a81e769e876304A8B8'; 38 | -------------------------------------------------------------------------------- /tests/extractor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { parsePix, PixElementType } from '../src'; 3 | import { hasError, isDynamicPix, isStaticPix } from '../src/validate'; 4 | import { 5 | RECURRENCE_DYNAMIC_UNRESERVED_EMV, 6 | DYNAMIC_TEST_EMV, 7 | STATIC_TEST_EMV, 8 | } from './emvCodes'; 9 | 10 | describe('EMV Data Extractor', () => { 11 | it('should be able to extract basic elements from a static pix', () => { 12 | const parsedPix = parsePix(STATIC_TEST_EMV); 13 | 14 | expect(hasError(parsedPix)).toBe(false); 15 | if (hasError(parsedPix)) return; 16 | 17 | expect(isStaticPix(parsedPix)).toBe(true); 18 | if (!isStaticPix(parsedPix)) return; 19 | 20 | // PARSE MAI 21 | expect(parsedPix.type).toBe(PixElementType.STATIC); 22 | expect(parsedPix.merchantCategoryCode).toBe('0000'); 23 | expect(parsedPix.transactionCurrency).toBe('986'); 24 | expect(parsedPix.countryCode).toBe('BR'); 25 | expect(parsedPix.merchantName).toBe('THALES OGLIARI'); 26 | expect(parsedPix.merchantCity).toBe('SAO MIGUEL DO O'); 27 | expect(parsedPix.pixKey).toBe('thalesog@me.com'); 28 | expect(parsedPix.urlRec).toBe(undefined); 29 | }); 30 | 31 | it('should be able to extract basic elements from a dynamic pix', () => { 32 | const parsedPix = parsePix(DYNAMIC_TEST_EMV); 33 | 34 | expect(hasError(parsedPix)).toBe(false); 35 | if (hasError(parsedPix)) return; 36 | 37 | expect(isDynamicPix(parsedPix)).toBe(true); 38 | if (!isDynamicPix(parsedPix)) return; 39 | 40 | expect(parsedPix.type).toBe(PixElementType.DYNAMIC); 41 | expect(parsedPix.merchantCategoryCode).toBe('0000'); 42 | expect(parsedPix.transactionCurrency).toBe('986'); 43 | expect(parsedPix.countryCode).toBe('BR'); 44 | expect(parsedPix.merchantName).toBe('THALES OGLIARI'); 45 | expect(parsedPix.merchantCity).toBe('SAO MIGUEL DO O'); 46 | expect(parsedPix.url).toBe( 47 | 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87' 48 | ); 49 | expect(parsedPix.urlRec).toBe(undefined); 50 | }); 51 | 52 | it('should be able to extract basic elements from a dynamic pix', () => { 53 | const parsedPix = parsePix(RECURRENCE_DYNAMIC_UNRESERVED_EMV); 54 | 55 | expect(hasError(parsedPix)).toBe(false); 56 | if (hasError(parsedPix)) return; 57 | 58 | expect(isDynamicPix(parsedPix)).toBe(true); 59 | if (!isDynamicPix(parsedPix)) return; 60 | 61 | expect(parsedPix.type).toBe(PixElementType.DYNAMIC); 62 | expect(parsedPix.merchantCategoryCode).toBe('0000'); 63 | expect(parsedPix.transactionCurrency).toBe('986'); 64 | expect(parsedPix.countryCode).toBe('BR'); 65 | expect(parsedPix.merchantName).toBe('FULANO DE TAL'); 66 | expect(parsedPix.merchantCity).toBe('BRASILIA'); 67 | 68 | expect(parsedPix.urlRec).toBe( 69 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/3d29b94249c54b3f8c533d729f59b5e5' 70 | ); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { 3 | parsePix, 4 | PixDynamicObject, 5 | PixRecurrenceObject, 6 | PixStaticObject, 7 | } from '../src'; 8 | import { parseEmv } from '../src/emvHandler'; 9 | import { 10 | EmvMaiSchema, 11 | EmvMandatory, 12 | EmvSchema, 13 | ValidTags, 14 | } from '../src/types/pixEmvSchema'; 15 | import { 16 | RECURRENCE_DYNAMIC_UNRESERVED_EMV, 17 | DYNAMIC_TEST_EMV, 18 | RECURRENCE_STATIC_UNRESERVED_EMV, 19 | STATIC_TEST_EMV, 20 | STATIC_TEST_NO_VALUE_ELEMENT_EMV, 21 | STATIC_TEST_NO_VALUE_EMV, 22 | RECURRENCE_DYNAMIC_UNRESERVED_REC, 23 | STATIC_TEST_WITH_FSS, 24 | } from './emvCodes'; 25 | 26 | describe('EMV Parser', () => { 27 | it('should be able to parse mandatory elements from a qrcode', () => { 28 | const { getTag } = parseEmv({ emvCode: STATIC_TEST_EMV }) as ValidTags; 29 | 30 | // PARSE MAI 31 | expect(getTag(EmvMandatory.TAG_MCC)).toBe('0000'); 32 | expect(getTag(EmvMandatory.TAG_TRANSACTION_CURRENCY)).toBe('986'); 33 | expect(getTag(EmvMandatory.TAG_COUNTRY_CODE)).toBe('BR'); 34 | expect(getTag(EmvMandatory.TAG_MERCHANT_NAME)).toBe('THALES OGLIARI'); 35 | expect(getTag(EmvMandatory.TAG_MERCHANT_CITY)).toBe('SAO MIGUEL DO O'); 36 | }); 37 | 38 | it('should be able to parse additional data from a dynamic emv code', () => { 39 | const { getTag, getSubTag } = parseEmv({ 40 | emvCode: RECURRENCE_STATIC_UNRESERVED_EMV, 41 | }) as ValidTags; 42 | 43 | // PARSE UT 44 | expect(getTag(EmvMandatory.TAG_MCC)).toBe('0000'); 45 | expect(getTag(EmvMandatory.TAG_TRANSACTION_CURRENCY)).toBe('986'); 46 | expect(getTag(EmvMandatory.TAG_COUNTRY_CODE)).toBe('BR'); 47 | expect(getTag(EmvMandatory.TAG_MERCHANT_NAME)).toBe('Pix'); 48 | expect(getTag(EmvMandatory.TAG_MERCHANT_CITY)).toBe('BRASILIA'); 49 | expect( 50 | getSubTag(EmvMaiSchema.TAG_MAI_URL, EmvSchema.TAG_UNRESERVED_TEMPLATE) 51 | ).toBe( 52 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/d7913bcbfc4947c9811669db12b40374' 53 | ); 54 | }); 55 | 56 | it('should be able to parse merchant account information from a static emv code', () => { 57 | const { getSubTag } = parseEmv({ emvCode: STATIC_TEST_EMV }) as ValidTags; 58 | 59 | // PARSE MAI 60 | expect(getSubTag(EmvMaiSchema.TAG_MAI_GUI, EmvSchema.TAG_MAI)).toBe( 61 | 'br.gov.bcb.pix' 62 | ); 63 | expect(getSubTag(EmvMaiSchema.TAG_MAI_PIXKEY, EmvSchema.TAG_MAI)).toBe( 64 | 'thalesog@me.com' 65 | ); 66 | expect(getSubTag(EmvMaiSchema.TAG_MAI_INFO_ADD, EmvSchema.TAG_MAI)).toBe( 67 | 'Pedido 123' 68 | ); 69 | }); 70 | 71 | it('should be able to parse merchant account information from a dynamic emv code', () => { 72 | const { getSubTag } = parseEmv({ emvCode: DYNAMIC_TEST_EMV }) as ValidTags; 73 | 74 | // PARSE MAI 75 | expect(getSubTag(EmvMaiSchema.TAG_MAI_GUI, EmvSchema.TAG_MAI)).toBe( 76 | 'br.gov.bcb.pix' 77 | ); 78 | expect(getSubTag(EmvMaiSchema.TAG_MAI_URL, EmvSchema.TAG_MAI)).toBe( 79 | 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87' 80 | ); 81 | }); 82 | it('should be able to parse a static pix', () => { 83 | const pix = parsePix(STATIC_TEST_EMV) as PixStaticObject; 84 | 85 | expect(pix.type).toBe('STATIC'); 86 | expect(pix.merchantCategoryCode).toBe('0000'); 87 | expect(pix.transactionCurrency).toBe('986'); 88 | expect(pix.countryCode).toBe('BR'); 89 | expect(pix.merchantName).toBe('THALES OGLIARI'); 90 | expect(pix.merchantCity).toBe('SAO MIGUEL DO O'); 91 | expect(pix.pixKey).toBe('thalesog@me.com'); 92 | expect(pix.transactionAmount).toBe(10); 93 | expect(pix.infoAdicional).toBe('Pedido 123'); 94 | expect(pix.txid).toBe('***'); 95 | }); 96 | 97 | it('should be able to parse a static pix with no value', () => { 98 | const pix = parsePix(STATIC_TEST_NO_VALUE_EMV) as PixStaticObject; 99 | 100 | expect(pix.type).toBe('STATIC'); 101 | expect(pix.merchantCategoryCode).toBe('0000'); 102 | expect(pix.transactionCurrency).toBe('986'); 103 | expect(pix.countryCode).toBe('BR'); 104 | expect(pix.merchantName).toBe('THALES OGLIARI'); 105 | expect(pix.merchantCity).toBe('SAO MIGUEL DO O'); 106 | expect(pix.pixKey).toBe('thalesog@me.com'); 107 | expect(pix.transactionAmount).toBe(0); 108 | expect(pix.infoAdicional).toBe('Pedido 123'); 109 | expect(pix.txid).toBe('***'); 110 | }); 111 | 112 | it('should be able to parse a static pix with no value element', () => { 113 | const pix = parsePix(STATIC_TEST_NO_VALUE_ELEMENT_EMV) as PixStaticObject; 114 | 115 | expect(pix.type).toBe('STATIC'); 116 | expect(pix.merchantCategoryCode).toBe('0000'); 117 | expect(pix.transactionCurrency).toBe('986'); 118 | expect(pix.countryCode).toBe('BR'); 119 | expect(pix.merchantName).toBe('THALES OGLIARI'); 120 | expect(pix.merchantCity).toBe('SAO MIGUEL DO O'); 121 | expect(pix.pixKey).toBe('thalesog@me.com'); 122 | expect(pix.transactionAmount).toBe(0); 123 | expect(pix.infoAdicional).toBe('Pedido 123'); 124 | expect(pix.txid).toBe('***'); 125 | }); 126 | 127 | it('should be able to parse a dynamic pix', () => { 128 | const pix = parsePix(DYNAMIC_TEST_EMV) as PixDynamicObject; 129 | 130 | expect(pix.type).toBe('DYNAMIC'); 131 | expect(pix.merchantCategoryCode).toBe('0000'); 132 | expect(pix.transactionCurrency).toBe('986'); 133 | expect(pix.countryCode).toBe('BR'); 134 | expect(pix.merchantName).toBe('THALES OGLIARI'); 135 | expect(pix.merchantCity).toBe('SAO MIGUEL DO O'); 136 | expect(pix.url).toBe( 137 | 'payload.psp.com/3ec9d2f9-5f03-4e0e-820d-63a81e769e87' 138 | ); 139 | }); 140 | 141 | it('should be able to parse a pix with additional data', () => { 142 | const pix = parsePix(RECURRENCE_STATIC_UNRESERVED_EMV) as PixStaticObject; 143 | 144 | expect(pix.type).toBe('STATIC'); 145 | expect(pix.merchantCategoryCode).toBe('0000'); 146 | expect(pix.transactionCurrency).toBe('986'); 147 | expect(pix.countryCode).toBe('BR'); 148 | expect(pix.merchantName).toBe('Pix'); 149 | expect(pix.merchantCity).toBe('BRASILIA'); 150 | expect(pix.txid).toBe('80a2d0a923984e8dbb80b4adf'); 151 | expect(pix.urlRec).toBe( 152 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/d7913bcbfc4947c9811669db12b40374' 153 | ); 154 | }); 155 | 156 | it('should be able to parse a dynamic pix additional data', () => { 157 | const pix = parsePix( 158 | RECURRENCE_DYNAMIC_UNRESERVED_EMV 159 | ) as PixRecurrenceObject; 160 | 161 | expect(pix.type).toBe('DYNAMIC'); 162 | expect(pix.merchantCategoryCode).toBe('0000'); 163 | expect(pix.transactionCurrency).toBe('986'); 164 | expect(pix.countryCode).toBe('BR'); 165 | expect(pix.merchantName).toBe('FULANO DE TAL'); 166 | expect(pix.merchantCity).toBe('BRASILIA'); 167 | expect(pix.url).toBe( 168 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/v2/7b2d64c4eb744a2d92a4dd5f8cfc4dfa' 169 | ); 170 | expect(pix.urlRec).toBe( 171 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/3d29b94249c54b3f8c533d729f59b5e5' 172 | ); 173 | }); 174 | 175 | it('should be able to parse a pix with rec additional data only', () => { 176 | const pix = parsePix( 177 | RECURRENCE_DYNAMIC_UNRESERVED_REC 178 | ) as PixRecurrenceObject; 179 | 180 | expect(pix.type).toBe('RECURRENCE'); 181 | expect(pix.merchantCategoryCode).toBe('0000'); 182 | expect(pix.transactionCurrency).toBe('986'); 183 | expect(pix.countryCode).toBe('BR'); 184 | expect(pix.merchantName).toBe('Fulano de Tal'); 185 | expect(pix.merchantCity).toBe('BRASILIA'); 186 | expect(pix.urlRec).toBe( 187 | 'qr-h.sandbox.pix.bcb.gov.br/rest/api/rec/5ee5232ead29422396b44f5eb67180d6' 188 | ); 189 | expect(pix.url).toBe(undefined); 190 | }); 191 | 192 | it('should be able to parse a static pix with fss', () => { 193 | const pix = parsePix(STATIC_TEST_WITH_FSS) as PixStaticObject; 194 | 195 | expect(pix.type).toBe('STATIC'); 196 | expect(pix.merchantCategoryCode).toBe('0000'); 197 | expect(pix.transactionCurrency).toBe('986'); 198 | expect(pix.countryCode).toBe('BR'); 199 | expect(pix.merchantName).toBe('Pix'); 200 | expect(pix.merchantCity).toBe('BRASILIA'); 201 | expect(pix.fss).toBe('99999008'); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /tests/throwIfError.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest'; 2 | import { createStaticPix, PixElementType } from '../src'; 3 | 4 | describe('throwIfError', () => { 5 | it('should be able to return the correct object when no errors are detected', () => { 6 | const staticPixFn = createStaticPix({ 7 | merchantName: 'Thales Ogliari', 8 | merchantCity: 'SAO MIGUEL', 9 | pixKey: 'nubank@thalesog.com', 10 | infoAdicional: 'Pedido 123', 11 | txid: '', 12 | transactionAmount: 10, 13 | }).throwIfError(); 14 | 15 | expect(staticPixFn).toEqual( 16 | expect.objectContaining({ 17 | type: PixElementType.STATIC, 18 | transactionAmount: 10, 19 | pixKey: 'nubank@thalesog.com', 20 | infoAdicional: 'Pedido 123', 21 | }) 22 | ); 23 | }); 24 | 25 | it('should throw if merchant city is longer than 15 characters', () => { 26 | const staticPixFn = createStaticPix({ 27 | merchantName: 'Thales Ogliari', 28 | merchantCity: 'SAO MIGUEL DO OESTE', 29 | pixKey: 'nubank@thalesog.com', 30 | infoAdicional: 'Pedido 123', 31 | txid: '', 32 | transactionAmount: 10, 33 | }); 34 | 35 | expect(staticPixFn.throwIfError).toThrow( 36 | 'merchantCity character limit exceeded (> 15)' 37 | ); 38 | }); 39 | 40 | it('should throw if merchant name is longer than 25 characters', () => { 41 | const staticPixFn = createStaticPix({ 42 | merchantName: 'Thales Ogliari Thales Ogliari Thales Ogliari', 43 | merchantCity: 'SAO MIGUEL', 44 | pixKey: 'nubank@thalesog.com', 45 | infoAdicional: 'Pedido 123', 46 | txid: '', 47 | transactionAmount: 10, 48 | }); 49 | 50 | expect(staticPixFn.throwIfError).toThrow( 51 | 'merchantName character limit exceeded (> 25)' 52 | ); 53 | }); 54 | 55 | it('should throw if txid is longer than 25 characters', () => { 56 | const staticPixFn = createStaticPix({ 57 | merchantName: 'Thales Ogliari', 58 | merchantCity: 'SAO MIGUEL', 59 | pixKey: 'nubank@thalesog.com', 60 | infoAdicional: 'Pedido 123', 61 | txid: 'F2B8073B0A52461997F53FB2A85FE7E8', 62 | transactionAmount: 10, 63 | }); 64 | 65 | expect(staticPixFn.throwIfError).toThrow( 66 | 'txid character limit exceeded (> 25)' 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2017", 5 | "outDir": "dist/main", 6 | "rootDir": "src", 7 | "moduleResolution": "node", 8 | "module": "commonjs", 9 | "declaration": true, 10 | "inlineSourceMap": true, 11 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 12 | "resolveJsonModule": true /* Include modules imported with .json extension. */, 13 | 14 | // "strict": true /* Enable all strict type-checking options. */, 15 | 16 | /* Strict Type-Checking Options */ 17 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 18 | // "strictNullChecks": true /* Enable strict null checks. */, 19 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 20 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 21 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 22 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 23 | 24 | /* Additional Checks */ 25 | "noUnusedLocals": true /* Report errors on unused locals. */, 26 | "noUnusedParameters": true /* Report errors on unused parameters. */, 27 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 28 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 29 | 30 | /* Debugging Options */ 31 | "traceResolution": false /* Report module resolution log messages. */, 32 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 33 | "listFiles": false /* Print names of files part of the compilation. */, 34 | "pretty": true /* Stylize errors and messages using color and context. */, 35 | 36 | /* Experimental Options */ 37 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 38 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 39 | 40 | "lib": ["es2017", "es2016", "dom"], 41 | "types": ["node"], 42 | "typeRoots": ["node_modules/@types", "src/types"] 43 | }, 44 | "include": ["src/**/*.ts"], 45 | "exclude": ["node_modules/**"], 46 | "compileOnSave": false 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "dist/module", 6 | "module": "esnext" 7 | }, 8 | "exclude": ["node_modules/**"] 9 | } 10 | --------------------------------------------------------------------------------