├── .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 |

2 |
3 | # Pix-Utils
4 |
5 |
6 |
10 |
14 |
15 |
16 |
20 |
21 |
22 |
23 |
27 |
28 |
31 |
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 |
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 |
--------------------------------------------------------------------------------