├── .npmignore ├── test ├── .eslintrc.yml ├── setup.js ├── mocha.opts ├── auth.test.js ├── certification.test.js ├── vbank.test.js ├── subscription.test.js └── payment.test.js ├── .coveralls.yml ├── index.js ├── .editorconfig ├── lib ├── IamporterError.js └── Iamporter.js ├── circle.yml ├── LICENSE ├── .gitignore ├── package.json ├── .eslintrc.yml └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .travis.yml 3 | .editorconfig -------------------------------------------------------------------------------- /test/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | mocha: true 3 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: SWb2QTJuXnaiA5QQcSIupxjG5uSQFt5zt 2 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | 6 | chai.use(chaiAsPromised); 7 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --check-leaks 2 | --recursive 3 | --reporter spec 4 | --require co-mocha 5 | --require ./test/setup.js 6 | --slow 1000 7 | --timeout 5000 8 | --ui bdd 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Iamporter = require('./lib/Iamporter'); 4 | const IamporterError = require('./lib/IamporterError'); 5 | /* 6 | * The entry point. 7 | * 8 | * @module Iamporter 9 | */ 10 | module.exports = { 11 | Iamporter, 12 | IamporterError 13 | }; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = LF 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.js] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.{yml,yaml,json}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /lib/IamporterError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | class ExtendableError extends Error { 5 | constructor(message, extra = {}) { 6 | super(message); 7 | 8 | this.name = this.constructor.name; 9 | this.status = extra.status; 10 | this.data = extra.data; 11 | this.raw = extra.raw; 12 | 13 | if (typeof Error.captureStackTrace === 'function') 14 | Error.captureStackTrace(this, this.constructor); 15 | else 16 | this.stack = (new Error(message)).stack; 17 | } 18 | } 19 | 20 | class IamporterError extends ExtendableError {} 21 | 22 | module.exports = IamporterError; 23 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | ## Customize the test machine 2 | machine: 3 | node: 4 | version: 6.1.0 5 | 6 | timezone: 7 | Asia/Seoul 8 | 9 | # Add some environment variables 10 | environment: 11 | YARN_VERSION: 0.19.1 12 | PATH: "${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin:${HOME}/.yarn/bin:${PATH}" 13 | 14 | ## Customize dependencies 15 | dependencies: 16 | cache_directories: 17 | - ~/.yarn 18 | - ~/.cache/yarn 19 | pre: 20 | - | 21 | if [[ ! -e ~/.yarn/bin/yarn || $(yarn --version) != "${YARN_VERSION}" ]]; then 22 | echo "Download and install Yarn." 23 | curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version $YARN_VERSION 24 | else 25 | echo "The correct version of Yarn is already installed." 26 | fi 27 | override: 28 | - yarn 29 | 30 | ## Customize test commands 31 | test: 32 | override: 33 | - yarn run test:coverage 34 | - yarn run lint 35 | post: 36 | - ./node_modules/.bin/coveralls < ./coverage/lcov.info 37 | - mv coverage $CIRCLE_ARTIFACTS/coverage 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Byungjin Park 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/ 2 | 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directory 31 | node_modules 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Tern configuration 40 | .tern-project 41 | .tern-port 42 | 43 | # Optional eslint cache 44 | .eslintcache 45 | 46 | # Output of 'npm pack' 47 | *.tgz 48 | 49 | # Yarn Integrity file 50 | .yarn-integrity 51 | 52 | # Yarn Lock file 53 | yarn.lock 54 | 55 | ### OSX ### 56 | *.DS_Store 57 | .AppleDouble 58 | .LSOverride 59 | 60 | # Icon must end with two \r 61 | Icon 62 | 63 | # Thumbnails 64 | ._* 65 | -------------------------------------------------------------------------------- /test/auth.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | const { Iamporter, IamporterError } = require('../'); 5 | 6 | describe('Auth', function () { 7 | describe('GET /users/getToken - Iamporter.getToken()', function () { 8 | let iamporter; 9 | 10 | beforeEach(function () { 11 | iamporter = new Iamporter(); 12 | }); 13 | 14 | it('should success to get a new API token with a default API Key and Secret', function () { 15 | return expect(iamporter.getToken()).to.be.fulfilled 16 | .then((res) => { 17 | expect(res.data, 'data').to.have.all.keys([ 18 | 'access_token', 'now', 'expired_at' 19 | ]); 20 | }); 21 | }); 22 | 23 | it('should fail to get a new API token with invalid API Key and Secret', function () { 24 | return expect(iamporter.getToken('InvalidAPIKey', 'InvalidSecret')).to.eventually 25 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 26 | }); 27 | 28 | it('should fail to get a new API token without API Key and Secret', function () { 29 | return expect(iamporter.getToken(null, null)).to.eventually 30 | .rejectedWith(IamporterError, 'imp_key, imp_secret 파라메터가 누락되었습니다.'); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iamporter", 3 | "version": "1.4.2", 4 | "description": "Node.js REST API client for I'mport", 5 | "license": "MIT", 6 | "author": "Claud D. Park (http://www.posquit0.com)", 7 | "contributors": [], 8 | "homepage": "https://github.com/posquit0/node-iamporter#readme", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/posquit0/node-iamporter.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/posquit0/node-iamporter/issues" 15 | }, 16 | "keywords": [ 17 | "iamport", 18 | "i'mport", 19 | "importer", 20 | "rest", 21 | "api", 22 | "client", 23 | "creditcard", 24 | "pay", 25 | "payment", 26 | "payments", 27 | "pg", 28 | "nodejs" 29 | ], 30 | "engines": { 31 | "node": ">=4" 32 | }, 33 | "files": [ 34 | "lib", 35 | "*.js" 36 | ], 37 | "main": "index.js", 38 | "scripts": { 39 | "test": "mocha test", 40 | "test:watch": "npm test -- --watch", 41 | "test:coverage": "istanbul cover _mocha --report lcovonly -- test", 42 | "lint": "eslint ./" 43 | }, 44 | "dependencies": { 45 | "axios": "^1.6.0", 46 | "debug": "^3.0.1" 47 | }, 48 | "devDependencies": { 49 | "chai": "^4.1.2", 50 | "chai-as-promised": "^7.1.1", 51 | "co-mocha": "^1.2.0", 52 | "coveralls": "^3.0.0", 53 | "eslint": "^5.0.0", 54 | "istanbul": "^0.4.5", 55 | "lodash": "^4.17.4", 56 | "mocha": "^5.0.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/certification.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { expect } = require('chai'); 4 | const { Iamporter, IamporterError } = require('../'); 5 | 6 | describe('Certification', function () { 7 | let iamporter; 8 | 9 | beforeEach(function () { 10 | iamporter = new Iamporter(); 11 | }); 12 | 13 | describe('GET /certifications/{imp_uid} - Iamporter.getCertification()', function () { 14 | it('should success to view a SMS certification when uid is valid'); 15 | 16 | it('should fail when given API token is invalid', function () { 17 | iamporter.token = 'invalid-token'; 18 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 19 | 20 | return expect(iamporter.getCertification()).to.eventually 21 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 22 | }); 23 | 24 | it('should fail to view a SMS certification with non-existent uid', function () { 25 | return expect(iamporter.getCertification('iamporter-test-imp-uid')).to.be.fulfilled 26 | .then((res) => { 27 | expect(res.message, 'res.message').to.equal('인증결과가 존재하지 않습니다.'); 28 | expect(res.data, 'res.data').to.be.null; 29 | }); 30 | }); 31 | }); 32 | 33 | describe('DELETE /certifications/{imp_uid} - Iamporter.deleteCertification()', function () { 34 | it('should success to delete a SMS certification when uid is valid'); 35 | 36 | it('should fail when given API token is invalid', function () { 37 | iamporter.token = 'invalid-token'; 38 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 39 | 40 | return expect(iamporter.deleteCertification()).to.eventually 41 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 42 | }); 43 | 44 | it('should fail to delete a non-existent SMS certification', function () { 45 | return expect(iamporter.deleteCertification('iamporter-test-imp-uid')).to.be.fulfilled 46 | .then((res) => { 47 | expect(res.message, 'res.message').to.equal('인증결과가 존재하지 않습니다.'); 48 | expect(res.data, 'res.data').to.be.null; 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/vbank.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { expect } = require('chai'); 5 | const { Iamporter, IamporterError } = require('../'); 6 | 7 | describe('Vbank', function () { 8 | let iamporter; 9 | 10 | beforeEach(function () { 11 | iamporter = new Iamporter(); 12 | }); 13 | 14 | describe('POST /vbanks - Iamporter.createVbank()', function () { 15 | it('should success to issue a virtual account when parameters are valid'); 16 | 17 | it('should fail when given API token is invalid', function () { 18 | iamporter.token = 'invalid-token'; 19 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 20 | 21 | const data = { 22 | 'merchant_uid': 'iamporter-test-merchant-uid', 23 | 'amount': 5000, 24 | 'vbank_code': '03', 25 | 'vbank_due': Math.floor(Date.now() / 1000) + 5000, 26 | 'vbank_holder': 'PLAT Corp' 27 | }; 28 | 29 | return expect(iamporter.createVbank(data)).to.eventually 30 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 31 | }); 32 | 33 | it('should fail when some parameters are omitted', function* () { 34 | const data = { 35 | 'merchant_uid': 'iamporter-test-merchant-uid', 36 | 'amount': 5000, 37 | 'vbank_code': '03', 38 | 'vbank_due': Math.floor(Date.now() / 1000) + 5000, 39 | 'vbank_holder': 'PLAT Corp' 40 | }; 41 | 42 | for (let key of Object.keys(data)) { 43 | const omitted = _.omit(data, key); 44 | yield expect(iamporter.createVbank(omitted)).to.eventually 45 | .rejectedWith(IamporterError, /파라미터 누락:/); 46 | } 47 | }); 48 | 49 | it('should fail to issue a virtual account without any contract with banks', function () { 50 | const data = { 51 | 'merchant_uid': 'iamporter-test-merchant-uid', 52 | 'amount': 5000, 53 | 'vbank_code': '03', 54 | 'vbank_due': Math.floor(Date.now() / 1000) + 5000, 55 | 'vbank_holder': 'PLAT Corp' 56 | }; 57 | 58 | return expect(iamporter.createVbank(data)).to.eventually 59 | .rejectedWith(IamporterError, /계약이 필요합니다./); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | es6: true 4 | 5 | extends: 6 | - "eslint:recommended" 7 | 8 | rules: 9 | # Possible Errors 10 | no-console: 11 | - error 12 | - 13 | allow: 14 | - log 15 | - warn 16 | - error 17 | no-debugger: error 18 | no-dupe-args: error 19 | no-dupe-keys: error 20 | no-duplicate-case: error 21 | no-empty: error 22 | no-ex-assign: error 23 | no-extra-boolean-cast: error 24 | no-extra-semi: error 25 | no-sparse-arrays: error 26 | no-template-curly-in-string: warn 27 | no-unexpected-multiline: error 28 | no-unreachable: error 29 | no-unsafe-negation: error 30 | use-isnan: error 31 | # valid-jsdoc: error 32 | valid-typeof: error 33 | 34 | # Best Practices 35 | curly: 36 | - error 37 | - multi-or-nest 38 | - consistent 39 | eqeqeq: 40 | - error 41 | - always 42 | - 43 | null: ignore 44 | 45 | # Strict Mode 46 | strict: 47 | - error 48 | - safe 49 | 50 | # Variables 51 | # no-shadow: 52 | # - error 53 | # - 54 | # builtinGlobals: true 55 | # hoist: all 56 | # allow: 57 | # - resolve 58 | # - reject 59 | # - done 60 | # - callback 61 | # - cb 62 | # - next 63 | no-undef-init: error 64 | no-undef: error 65 | no-undefined: error 66 | no-unused-vars: 67 | - error 68 | - 69 | vars: all 70 | args: after-used 71 | no-use-before-define: error 72 | 73 | # Node.js and CommonJS 74 | global-require: error 75 | no-new-require: error 76 | no-path-concat: error 77 | 78 | # Stylistic Issues 79 | array-bracket-spacing: 80 | - error 81 | - never 82 | block-spacing: error 83 | brace-style: 84 | - error 85 | - 1tbs 86 | - 87 | allowSingleLine: true 88 | camelcase: error 89 | comma-style: 90 | - error 91 | - last 92 | comma-spacing: 93 | - error 94 | - 95 | before: false 96 | after: true 97 | indent: 98 | - error 99 | - 2 100 | - 101 | SwitchCase: 0 102 | MemberExpression: 1 103 | FunctionDeclaration: 104 | parameters: 2 105 | body: 1 106 | linebreak-style: 107 | - error 108 | - unix 109 | quotes: 110 | - error 111 | - single 112 | semi: 113 | - error 114 | - always 115 | semi-spacing: 116 | - error 117 | - 118 | before: false 119 | after: true 120 | space-before-blocks: 121 | - error 122 | - always 123 | space-before-function-paren: 124 | - error 125 | - 126 | anonymous: always 127 | named: never 128 | asyncArrow: always 129 | space-in-parens: 130 | - error 131 | - never 132 | space-unary-ops: 133 | - error 134 | - 135 | words: true 136 | nonwords: false 137 | spaced-comment: 138 | - error 139 | - always 140 | - 141 | exceptions: 142 | - "-" 143 | - "+" 144 | - "*" 145 | 146 | # ECMAScript 6 147 | -------------------------------------------------------------------------------- /test/subscription.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { expect } = require('chai'); 5 | const { Iamporter, IamporterError } = require('../'); 6 | 7 | describe('Subscription', function () { 8 | let iamporter; 9 | 10 | beforeEach(function () { 11 | iamporter = new Iamporter(); 12 | }); 13 | 14 | describe('POST /subscribe/customers/{customer_uid} - Iamporter.createSubscription()', function () { 15 | it('should success to create a billing key with a private credit card'); 16 | 17 | it('should fail when given credit card is invalid', function () { 18 | const data = { 19 | 'customer_uid': 'iamporter-test-customer-uid', 20 | 'card_number': '1234-1234-1234-1234', 21 | 'expiry': '2020-02', 22 | 'birth': '880220' 23 | }; 24 | 25 | return expect(iamporter.createSubscription(data)).to.eventually 26 | .rejectedWith(IamporterError, /유효하지않은 카드번호/); 27 | }); 28 | 29 | it('should fail when given API token is invalid', function () { 30 | iamporter.token = 'invalid-token'; 31 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 32 | 33 | const data = { 34 | 'customer_uid': 'iamporter-test-customer-uid', 35 | 'card_number': '1234-1234-1234-1234', 36 | 'expiry': '2020-02', 37 | 'birth': '920220' 38 | }; 39 | 40 | return expect(iamporter.createSubscription(data)).to.eventually 41 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 42 | }); 43 | 44 | it('should fail when some parameters are omitted', function* () { 45 | const data = { 46 | 'customer_uid': 'iamporter-test-customer-uid', 47 | 'card_number': '1234-1234-1234-1234', 48 | 'expiry': '2020-02', 49 | 'birth': '920220' 50 | }; 51 | 52 | for (let key of Object.keys(data)) { 53 | const omitted = _.omit(data, key); 54 | yield expect(iamporter.createSubscription(omitted)).to.eventually 55 | .rejectedWith(IamporterError, /파라미터 누락:/); 56 | } 57 | }); 58 | }); 59 | 60 | describe('GET /subscribe/customers/{customer_uid} - Iamporter.getSubscription()', function () { 61 | it('should success to view a subscription information when billing key is valid'); 62 | 63 | it('should fail when given API token is invalid', function () { 64 | iamporter.token = 'invalid-token'; 65 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 66 | 67 | return expect(iamporter.getSubscription()).to.eventually 68 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 69 | }); 70 | 71 | it('should fail to view a subscription information with non-existent billing key', function () { 72 | return expect(iamporter.getSubscription('iamporter-test-uid')).to.eventually 73 | .rejectedWith(IamporterError, /등록된 정보를 찾을 수 없습니다./); 74 | }); 75 | }); 76 | 77 | describe('DELETE /subscribe/customers/{customer_uid} - Iamporter.deleteSubscription()', function () { 78 | it('should success to delete a subscription information when billing key is valid'); 79 | 80 | it('should fail when given API token is invalid', function () { 81 | iamporter.token = 'invalid-token'; 82 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 83 | 84 | return expect(iamporter.deleteSubscription()).to.eventually 85 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 86 | }); 87 | 88 | it('should fail to delete a non-existent billing key', function () { 89 | return expect(iamporter.deleteSubscription('iamporter-test-uid')).to.eventually 90 | .rejectedWith(IamporterError, /등록된 정보를 찾을 수 없습니다./); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Iamporter.js 4 | 5 |
6 | Iamporter 7 |

8 | 9 |

10 | A REST API client for I'mport 11 |

12 | 13 |
14 | 15 | CircleCI 16 | 17 | 18 | Coverage Status 19 | 20 | 21 | npm version 22 | 23 | 24 | npm 25 | 26 | 27 | npm 28 | 29 | 30 | MIT Licence 31 | 32 | 33 | Open Source Love 34 | 35 |
36 | 37 |
38 | 39 | **Iamporter**는 [아임포트](http://iamport.kr/)에서 제공하는 REST API를 쉽게 활용하기 위하여 작성된 Node.js 클라이언트 입니다. 40 | 41 | - 아임포트는 국내 PG사와의간편한 연동을 제공하는 서비스입니다. 42 | - 이용 중 발생한 문제에 대하여 책임지지 않습니다. 43 | - 최초 작성은 자동차 렌트 플랫폼 [CARPLAT](https://www.carplat.co.kr)에서 사용하기 위하여 작성되었습니다. 44 | 45 | ## Features 46 | 47 | - Written in ES6 Syntax 48 | - [Promise](http://www.html5rocks.com/ko/tutorials/es6/promises/) Support 49 | - Exception Handling with a custom error class `IamporterError` 50 | 51 | 52 | ## Installation 53 | 54 | ```bash 55 | $ npm install iamporter 56 | ``` 57 | 58 | 59 | ## Specification 60 | 61 | - 모든 API 요청은 Promise를 반환합니다. 62 | 63 | ### Fulfilled 64 | 65 | - API 요청이 성공적으로 수행된 경우 다음과 같은 형식의 데이터를 반환합니다. 66 | 67 | ```js 68 | { 69 | "status": 200, // HTTP STATUS CODE 70 | "message": "", // 아임포트 API 응답 메시지 혹은 Iamporter 정의 메시지 71 | "data": {}, // 아임포트 API 응답 데이터 72 |  "raw": {} // 아임포트 API RAW DATA 73 | } 74 | ``` 75 | 76 | ### Rejected 77 | 78 | - API 요청을 성공적으로 수행하지 못한 경우 항상 `IamporterError` 에러를 반환합니다. 79 | 80 | ```js 81 | iamporter.paySubscription(...) 82 | .catch((err) => { 83 | console.log(err.status); // HTTP STATUS CODE 84 | console.log(err.message); // 아임포트 API 응답 메시지 혹은 Iamporter 정의 메시지 85 | console.log(err.data); // 아임포트 API 응답 데이터 86 | console.log(err.raw); // 아임포트 API RAW DATA 87 | }); 88 | ``` 89 | 90 | 91 | ## Usage 92 | 93 | ### Import & Create an Instance 94 | 95 | - `iamporter` 패키지는 `Iamporter`와 `IamporterError` 두 클래스를 제공합니다. 96 | 97 | ```node 98 | const { Iamporter, IamporterError } = require('iamporter'); 99 | 100 | // For Testing (테스트용 API KEY와 SECRET 기본 설정) 101 | const iamporter = new Iamporter(); 102 | 103 | // For Production 104 | const iamporter = new Iamporter({ 105 | apiKey: 'YOUR_API_KEY', 106 | secret: 'YOUR_SECRET' 107 | }); 108 | ``` 109 | 110 | ### API Token 111 | 112 | - `iamporter`는 API 요청 전에 API 토큰의 유효성을 확인 후 자동 발급/갱신하므로 직접 토큰 API를 호출할 필요가 없습니다. 113 | 114 | ```node 115 | // 인스턴스 생성 시 설정한 API KEY와 SECRET 116 | iamporter.getToken() 117 | .then(...) 118 | 119 | // 토큰 생성 시 사용될 API KEY와 SECRET 직접 지정 120 | iamporter.getToken('API_KEY', 'SECRET') 121 | .then(...) 122 | ``` 123 | 124 | ### Subscription 125 | 126 | - 정기 구독(Subscription)형 서비스 등에 이용할 수 있는 빌링키를 관리합니다. 127 | 128 | ```node 129 | // 빌링키 생성 130 | iamporter.createSubscription({ 131 | 'customer_uid': 'test_uid', 132 | 'card_number': '1234-1234-1234-1234', 133 | 'expiry': '2021-11', 134 | 'birth': '620201', 135 | 'pwd_2digit': '99' 136 | }).then(result => { 137 | console.log(result); 138 | }).catch(err => { 139 | if (err instanceof IamporterError) 140 | // Handle the exception 141 | }); 142 | 143 | // 빌링키 조회 144 | iamporter.getSubscription('test_uid') 145 | .then(...) 146 | 147 | // 빌링키 삭제 148 | iamporter.deleteSubscription('test_uid') 149 | .then(...) 150 | 151 | // 비인증 결제 (빌링키 이용) 152 | iamporter.paySubscription({ 153 | 'customer_uid': 'test_uid', 154 | 'merchant_uid': 'test_billing_key', 155 | 'amount': 50000 156 | }).then(result => { 157 | console.log(result); 158 | }).catch(err => { 159 | if (err instanceof IamporterError) 160 | // Handle the exception 161 | }); 162 | ``` 163 | 164 | ### Onetime Payment 165 | 166 | - 빌링키를 생성하지 않아도 신용카드 정보만으로 간편 결제를 할 수 있습니다. 167 | 168 | ```node 169 | // Onetime 비인증 결제 170 | iamporter.payOnetime({ 171 | 'merchant_uid': 'merchant_1448280088556', 172 | 'amount': 5000, 173 | 'card_number': '1234-1234-1234-1234', 174 | 'expiry': '2021-12', 175 | 'birth': '590912', 176 | 'pwd_2digit': '11' 177 | }).then(result => { 178 | console.log(result); 179 | }).catch(err => { 180 | if (err instanceof IamporterError) 181 | // Handle the exception 182 | }); 183 | 184 | // 해외카드 비인증 결제 185 | iamporter.payForeign({ 186 | 'merchant_uid': 'merchant_1448280088556', 187 | 'amount': 5000, 188 | 'card_number': '1234-1234-1234-1234', 189 | 'expiry': '2021-12', 190 | }).then(result => { 191 | console.log(result); 192 | }).catch(err => { 193 | if (err instanceof IamporterError) 194 | // Handle the exception 195 | }); 196 | ``` 197 | 198 | ### Cancel the Payment 199 | 200 | - 아임포트 고유 아이디 혹은 상점 고유 아이디로 결제 취소가 가능합니다. 201 | - 부분 결제 취소 또한 지원됩니다. 202 | 203 | ```node 204 | // 아임포트 고유 아이디로 결제 취소 205 | iamporter.cancelByImpUid('imp_448280090638') 206 | .then(...) 207 | 208 | // 상점 고유 아이디로 결제 취소 209 | iamporter.cancelByMerchantUid('merchant_1448280088556') 210 | .then(...) 211 | 212 | // 상점 고유 아이디로 부분 결제 취소 213 | iamporter.cancelByMerchantUid('merchant_1448280088556', { 214 | 'amount': 2500, 215 | 'reason': '예약 변경' 216 | }).then(...) 217 | 218 | // 결제 취소 후 계좌 환불 219 | iamporter.cancel({ 220 | 'imp_uid': 'imp_448280090638', 221 | 'reason': '제품 상태 불량', 222 | 'refund_holder': '홍길동', 223 | 'refund_bank': '03', 224 | 'refund_account': '056-076923-01-017' 225 | ).then(...) 226 | ``` 227 | 228 | ### Find the Payments 229 | 230 | - 아임포트에서는 아임포트 고유 아이디(ImpUid)와 상점 고유 아이디(MerchantUid)로 결제정보 조회가 가능합니다. 231 | 232 | ```node 233 | // 아임포트 고유 아이디로 결제정보 조회 234 | iamporter.findByImpUid('imp_448280090638') 235 | .then(...) 236 | 237 | // 상점 고유 아이디로 결제정보 조회 238 | iamporter.findByMerchantUid('merchant_1448280088556') 239 | .then(...) 240 | 241 | // 상점 고유 아이디로 결제정보 목록 조회 242 | iamporter.findAllByMerchantUid('merchant_1448280088556') 243 | .then(...) 244 | 245 | // 결제 상태로 결제정보 목록 조회(status: ['all', 'ready', 'paid', 'cancelled', 'failed']) 246 | iamporter.findAllByStatus('paid') 247 | .then(...) 248 | ``` 249 | 250 | ### Prepared Payment 251 | 252 | - 아임포트에서는 결제 건에 대한 사전 정보 등록 및 검증을 할 수 있습니다. 253 | 254 | ```node 255 | // 결제 예정금액 사전 등록 256 | iamporter.createPreparedPayment({ 257 | 'merchant_uid': 'merchant_1448280088556', 258 | 'amount', '128900' 259 | }).then(...) 260 | 261 | // 결제 예정금액 조회 262 | iamporter.getPreparedPayment('merchant_1448280088556') 263 | .then(...) 264 | ``` 265 | 266 | ### Certifications 267 | 268 | - 아임포트에서는 SMS 본인인증 결과를 조회/삭제할 수 있습니다. 269 | 270 | ```node 271 | // 아임포트 고유 아이디로 SMS 본인인증 결과 조회 272 | iamporter.getCertification('imp_448280090638') 273 | .then(...) 274 | 275 | // 아임포트 고유 아이디로 SMS 본인인증 결과 삭제 276 | iamporter.deleteCertification('imp_448280090638') 277 | .then(...) 278 | ``` 279 | 280 | ### VBanks 281 | 282 | - 아임포트에서는 PG 결제화면 없이 API 만으로 가상계좌 발급이 가능합니다. 283 | 284 | ```node 285 | // 가상계좌 발급 286 | iamporter.createVbank({ 287 | 'merchant_uid': 'merchant_1448280088556', 288 | 'amount': '128900', 289 | 'vbank_code': '03', 290 | 'vbank_due': 1485697047, 291 | 'vbank_holder': 'PLAT Corp' 292 | }).then(...) 293 | ``` 294 | 295 | 296 | ## Links 297 | 298 | - [**아임포트**](http://www.iamport.kr/) 299 | - [**아임포트 API**](https://api.iamport.kr/) 300 | - [**아임포트 관리자**](https://admin.iamport.kr/) 301 | - [**아임포트 매뉴얼**](http://www.iamport.kr/manual/) 302 | 303 | 304 | ## Contact 305 | 306 | If you have any questions, feel free to join me at [`#posquit0` on Freenode](irc://irc.freenode.net/posquit0) and ask away. Click [here](https://kiwiirc.com/client/irc.freenode.net/posquit0) to connect. 307 | 308 | 309 | ## License 310 | 311 | - [MIT](https://github.com/posquit0/node-iamporter/blob/master/LICENSE) 312 | -------------------------------------------------------------------------------- /test/payment.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const { expect } = require('chai'); 5 | const { Iamporter, IamporterError } = require('../'); 6 | 7 | describe('Payment', function () { 8 | let iamporter; 9 | 10 | beforeEach(function () { 11 | iamporter = new Iamporter(); 12 | }); 13 | 14 | describe('GET /payments/{imp_uid} - Iamporter.findByImpUid()', function () { 15 | it('should success to find a payment information when uid is valid'); 16 | 17 | it('should fail when given API token is invalid', function () { 18 | iamporter.token = 'invalid-token'; 19 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 20 | 21 | return expect(iamporter.findByImpUid('iamporter-test-imp-uid')).to.eventually 22 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 23 | }); 24 | 25 | it('should fail to find a payment information with non-existent uid', function () { 26 | return expect(iamporter.findByImpUid('iamporter-test-imp-uid')).to.be.fulfilled 27 | .then((res) => { 28 | expect(res.message, 'res.message').to.equal('존재하지 않는 결제정보입니다.'); 29 | expect(res.data, 'res.data').to.be.null; 30 | }); 31 | }); 32 | }); 33 | 34 | describe('GET /payments/find/{merchant_uid} - Iamporter.findByMerchantUid()', function () { 35 | it('should success to find a payment information when uid is valid'); 36 | 37 | it('should fail when given API token is invalid', function () { 38 | iamporter.token = 'invalid-token'; 39 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 40 | 41 | return expect(iamporter.findByMerchantUid('iamporter-test-merchant-uid')).to.eventually 42 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 43 | }); 44 | 45 | it('should fail to find a payment information with non-existent uid', function () { 46 | return expect(iamporter.findByMerchantUid('iamporter-test-merchant-uid')).to.be.fulfilled 47 | .then((res) => { 48 | expect(res.message, 'res.message').to.equal('존재하지 않는 결제정보입니다.'); 49 | expect(res.data, 'res.data').to.be.null; 50 | }); 51 | }); 52 | }); 53 | 54 | describe('GET /payments/findAll/{merchant_uid}/{payment_status} - Iamporter.findAllByMerchantUid()', function () { 55 | it('should success to find a list of payment informations when uid is valid'); 56 | 57 | it('should fail when given API token is invalid', function () { 58 | iamporter.token = 'invalid-token'; 59 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 60 | 61 | return expect(iamporter.findAllByMerchantUid('iamporter-test-merchant-uid')).to.eventually 62 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 63 | }); 64 | 65 | it('should fail to find a list of payment informations with non-existent uid', function () { 66 | return expect(iamporter.findAllByMerchantUid('iamporter-test-merchant-uid')).to.be.fulfilled 67 | .then((res) => { 68 | expect(res.message, 'res.message').to.equal('존재하지 않는 결제정보입니다.'); 69 | expect(res.data, 'res.data').to.be.null; 70 | }); 71 | }); 72 | }); 73 | 74 | describe('GET /payments/status/{payment_status} - Iamporter.findAllByStatus()', function () { 75 | it('should success to find a list of payment informations when status is valid'); 76 | 77 | it('should fail when given API token is invalid', function () { 78 | iamporter.token = 'invalid-token'; 79 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 80 | 81 | return expect(iamporter.findAllByStatus()).to.eventually 82 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 83 | }); 84 | 85 | it('should fail to find a list of payment informations with invalid status', function () { 86 | return expect(iamporter.findAllByStatus('iamporter-test-status')).to.eventually 87 | .rejectedWith(IamporterError, '지원되지 않는 상태값입니다.'); 88 | }); 89 | }); 90 | 91 | describe('POST /payments/prepare - Iamporter.createPreparedPayment()', function () { 92 | it('should success to create a prepared payment'); 93 | 94 | it('should fail when given API token is invalid', function () { 95 | iamporter.token = 'invalid-token'; 96 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 97 | 98 | const data = { 99 | 'merchant_uid': 'iamporter-test-merchant-uid', 100 | 'amount': 500 101 | }; 102 | 103 | return expect(iamporter.createPreparedPayment(data)).to.eventually 104 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 105 | }); 106 | 107 | it('should fail when some parameters are omitted', function* () { 108 | const data = { 109 | 'merchant_uid': 'iamporter-test-merchant-uid', 110 | 'amount': 500 111 | }; 112 | 113 | for (let key of Object.keys(data)) { 114 | const omitted = _.omit(data, key); 115 | yield expect(iamporter.createPreparedPayment(omitted)).to.eventually 116 | .rejectedWith(IamporterError, /파라미터 누락:/); 117 | } 118 | }); 119 | }); 120 | 121 | describe('GET /payments/prepare - Iamporter.getPreparedPayment()', function () { 122 | it('should success to view a prepared payment information when uid is valid'); 123 | 124 | it('should fail when given API token is invalid', function () { 125 | iamporter.token = 'invalid-token'; 126 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 127 | 128 | return expect(iamporter.getPreparedPayment()).to.eventually 129 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 130 | }); 131 | 132 | it('should fail to view a prepared payment information with invalid uid', function () { 133 | return expect(iamporter.getPreparedPayment('iamporter-test-merchant-uid')).to.be.fulfilled 134 | .then((res) => { 135 | expect(res.message, 'res.message').to.equal('사전등록된 결제정보가 존재하지 않습니다.'); 136 | expect(res.data, 'res.data').to.be.null; 137 | }); 138 | }); 139 | }); 140 | 141 | describe('POST /subscribe/payments/onetime - Iamporter.payOnetime()', function () { 142 | it('should success to pay onetime with a credit-card'); 143 | 144 | it('should fail when given API token is invalid', function () { 145 | iamporter.token = 'invalid-token'; 146 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 147 | 148 | const data = { 149 | 'merchant_uid': 'iamporter-test-merchant-uid', 150 | 'amount': 5000, 151 | 'card_number': '1234-1234-1234-1234', 152 | 'expiry': '2020-02', 153 | 'birth': '920220' 154 | }; 155 | 156 | return expect(iamporter.payOnetime(data)).to.eventually 157 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 158 | }); 159 | 160 | it('should fail when some parameters are omitted', function* () { 161 | const data = { 162 | 'merchant_uid': 'iamporter-test-merchant-uid', 163 | 'amount': 5000, 164 | 'card_number': '1234-1234-1234-1234', 165 | 'expiry': '2020-02', 166 | 'birth': '920220' 167 | }; 168 | 169 | for (let key of Object.keys(data)) { 170 | const omitted = _.omit(data, key); 171 | yield expect(iamporter.payOnetime(omitted)).to.eventually 172 | .rejectedWith(IamporterError, /파라미터 누락:/); 173 | } 174 | }); 175 | 176 | it('should fail to pay with invalid credit-card information', function () { 177 | const data = { 178 | 'merchant_uid': 'iamporter-test-merchant-uid', 179 | 'amount': 5000, 180 | 'card_number': '1234-1234-1234-1234', 181 | 'expiry': '2020-02', 182 | 'birth': '920220' 183 | }; 184 | 185 | return expect(iamporter.payOnetime(data)).to.eventually 186 | .rejectedWith(IamporterError, /유효하지않은 카드번호/); 187 | }); 188 | }); 189 | 190 | describe('POST /subscribe/payments/again - Iamporter.paySubscription()', function () { 191 | it('should success to pay with a billing key'); 192 | 193 | it('should fail when given API token is invalid', function () { 194 | iamporter.token = 'invalid-token'; 195 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 196 | 197 | const data = { 198 | 'customer_uid': 'iamporter-test-customer-uid', 199 | 'merchant_uid': 'iamporter-test-merchant-uid', 200 | 'amount': 5000 201 | }; 202 | 203 | return expect(iamporter.paySubscription(data)).to.eventually 204 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 205 | }); 206 | 207 | it('should fail when some parameters are omitted', function* () { 208 | const data = { 209 | 'customer_uid': 'iamporter-test-customer-uid', 210 | 'merchant_uid': 'iamporter-test-merchant-uid', 211 | 'amount': 5000 212 | }; 213 | 214 | for (let key of Object.keys(data)) { 215 | const omitted = _.omit(data, key); 216 | yield expect(iamporter.paySubscription(omitted)).to.eventually 217 | .rejectedWith(IamporterError, /파라미터 누락:/); 218 | } 219 | }); 220 | 221 | it('should fail to pay with invalid billing key', function () { 222 | const data = { 223 | 'customer_uid': 'iamporter-test-customer-uid4', 224 | 'merchant_uid': 'iamporter-test-merchant-uid3', 225 | 'amount': 5000 226 | }; 227 | 228 | return expect(iamporter.paySubscription(data)).to.eventually 229 | .rejectedWith(IamporterError, /등록되지 않은 구매자입니다./); 230 | }); 231 | }); 232 | 233 | describe('POST /subscribe/payments/foreign - Iamporter.payForeign()', function () { 234 | it('should success to pay with a foreign credit-card'); 235 | 236 | it('should fail when given API token is invalid', function () { 237 | iamporter.token = 'invalid-token'; 238 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 239 | 240 | const data = { 241 | 'merchant_uid': 'iamporter-test-merchant-uid', 242 | 'amount': 5000, 243 | 'card_number': '1234-1234-1234-1234', 244 | 'expiry': '2020-02' 245 | }; 246 | 247 | return expect(iamporter.payForeign(data)).to.eventually 248 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 249 | }); 250 | 251 | it('should fail when some parameters are omitted', function* () { 252 | const data = { 253 | 'merchant_uid': 'iamporter-test-merchant-uid', 254 | 'amount': 5000, 255 | 'card_number': '1234-1234-1234-1234', 256 | 'expiry': '2020-02' 257 | }; 258 | 259 | for (let key of Object.keys(data)) { 260 | const omitted = _.omit(data, key); 261 | yield expect(iamporter.payForeign(omitted)).to.eventually 262 | .rejectedWith(IamporterError, /파라미터 누락:/); 263 | } 264 | }); 265 | }); 266 | 267 | describe('POST /payments/cancel - Iamporter.cancelByImpUid()', function () { 268 | it('should success to cancel a payment'); 269 | 270 | it('should fail when given API token is invalid', function () { 271 | iamporter.token = 'invalid-token'; 272 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 273 | 274 | return expect(iamporter.cancelByImpUid('iamporter-test-imp-uid')).to.eventually 275 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 276 | }); 277 | 278 | it('should fail to cancel a payment with non-existent uid', function () { 279 | return expect(iamporter.cancelByImpUid('iamporter-test-imp-uid')).to.eventually 280 | .rejectedWith(IamporterError, /취소할 결제건이 존재하지 않습니다./); 281 | }); 282 | }); 283 | 284 | describe('POST /payments/cancel - Iamporter.cancelByMerchantUid()', function () { 285 | it('should success to cancel a payment'); 286 | 287 | it('should fail when given API token is invalid', function () { 288 | iamporter.token = 'invalid-token'; 289 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 290 | 291 | return expect(iamporter.cancelByMerchantUid('iamporter-test-merchant-uid')).to.eventually 292 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 293 | }); 294 | 295 | it('should fail to cancel a payment with non-existent uid', function () { 296 | return expect(iamporter.cancelByMerchantUid('iamporter-test-merchant-uid')).to.eventually 297 | .rejectedWith(IamporterError, /취소할 결제건이 존재하지 않습니다./); 298 | }); 299 | }); 300 | 301 | describe('POST /payments/cancel - Iamporter.cancel()', function () { 302 | it('should success to cancel a payment'); 303 | 304 | it('should fail when given API token is invalid', function () { 305 | iamporter.token = 'invalid-token'; 306 | iamporter.expireAt = Math.floor(Date.now() / 1000) + 5000; 307 | 308 | return expect(iamporter.cancel()).to.eventually 309 | .rejectedWith(IamporterError, '아임포트 API 인증에 실패하였습니다.'); 310 | }); 311 | 312 | it('should fail to cancel a payment without any uid', function () { 313 | return expect(iamporter.cancel()).to.eventually 314 | .rejectedWith(IamporterError, /지정해주셔야합니다./); 315 | }); 316 | }); 317 | }); 318 | -------------------------------------------------------------------------------- /lib/Iamporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const axios = require('axios'); 4 | const debug = require('debug')('iamporter'); 5 | const IamporterError = require('./IamporterError'); 6 | 7 | 8 | const HTTP_OK = 200; 9 | const HTTP_BAD_REQUEST = 400; 10 | const HTTP_UNAUTHORIZED = 401; 11 | const HTTP_NOT_FOUND = 404; 12 | 13 | function resSerializer(res = {}) { 14 | return { 15 | status: res.status, 16 | message: res.data.message, 17 | data: res.data.response, 18 | raw: res.data 19 | }; 20 | } 21 | 22 | /** 23 | * Class representing a client for I'mport API 24 | */ 25 | class Iamporter { 26 | 27 | /** 28 | * Create an instance of Iamporter 29 | * 30 | * @param {string} [apiKey] 31 | * @param {string} [secret] 32 | * @param {string} [host] 33 | */ 34 | constructor({ 35 | apiKey = 'imp_apiKey', 36 | secret = 'ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f', 37 | host = 'https://api.iamport.kr', 38 | serializer = resSerializer 39 | } = {}) { 40 | 41 | this.apiKey = apiKey; 42 | this.secret = secret; 43 | this.host = host; 44 | this.client = axios.create({ 45 | baseURL: this.host, 46 | responseType: 'json', 47 | timeout: 1000 * 30 48 | }); 49 | this.resSerializer = serializer; 50 | 51 | debug('Create an instance of Iamporter'); 52 | } 53 | 54 | _request(spec) { 55 | spec.headers = { 56 | 'User-Agent': 'Iamporter.js' 57 | }; 58 | 59 | if (!this.isExpiredToken()) 60 | spec.headers['Authorization'] = this.token; 61 | 62 | debug(`${spec.method} ${spec.url} %j`, spec.data); 63 | 64 | return new Promise((resolve, reject) => { 65 | this.client.request(spec) 66 | .then(res => { 67 | const { status, data } = res; 68 | const output = this.resSerializer(res); 69 | debug(`${status} %j`, data); 70 | 71 | if (data.code !== 0) 72 | return reject(new IamporterError(data.message, output)); 73 | else 74 | return resolve(output); 75 | }) 76 | .catch(err => { 77 | if (!err.response) 78 | return reject(new IamporterError('예기치 못한 오류가 발생하였습니다.', {})); 79 | 80 | const { status, data } = err.response; 81 | const output = this.resSerializer(err.response); 82 | debug(`${status} %j`, data); 83 | 84 | switch (status) { 85 | case HTTP_OK: 86 | return reject(new IamporterError(data.message, output)); 87 | case HTTP_BAD_REQUEST: 88 | return reject(new IamporterError(data.message, output)); 89 | case HTTP_UNAUTHORIZED: 90 | return reject(new IamporterError('아임포트 API 인증에 실패하였습니다.', output)); 91 | case HTTP_NOT_FOUND: 92 | return resolve(output); 93 | default: 94 | return reject(new IamporterError('예기치 못한 오류가 발생하였습니다.', output)); 95 | } 96 | }); 97 | }); 98 | } 99 | 100 | _updateToken() { 101 | if (this.isExpiredToken()) { 102 | return this.getToken() 103 | .then(res => { 104 | this.token = res.data['access_token']; 105 | this.expireAt = res.data['expired_at']; 106 | debug(`Update API token: ${this.token}(expireAt: ${this.expireAt})`); 107 | return this.token; 108 | }); 109 | } else { 110 | return Promise.resolve(this.token); 111 | } 112 | } 113 | 114 | _validatePayment(amount, res) { 115 | if (res.data.status !== 'paid' || res.data.amount !== amount) 116 | throw new IamporterError(res.data['fail_reason']); 117 | 118 | return res; 119 | } 120 | 121 | isExpiredToken() { 122 | return !this.expireAt || this.expireAt <= Math.floor(Date.now() / 1000); 123 | } 124 | 125 | /** 126 | * API 토큰 요청 127 | * GET - https://api.iamport.kr/users/getToken 128 | * @see {@link https://api.iamport.kr/#!/authenticate/getToken} 129 | * 130 | * @param {string} [apiKey=this.apiKey] 131 | * @param {string} [secret=this.secret] 132 | * @returns {Promise} result 133 | */ 134 | getToken(apiKey = this.apiKey, secret = this.secret) { 135 | const spec = { 136 | method: 'POST', 137 | url: '/users/getToken', 138 | data: { 139 | 'imp_key': apiKey, 140 | 'imp_secret': secret 141 | } 142 | }; 143 | 144 | return this._request(spec); 145 | } 146 | 147 | /** 148 | * SMS 본인인증 정보 조회 149 | * GET - https://api.iamport.kr/certifications/{imp_uid} 150 | * @see {@link https://api.iamport.kr/#!/certifications/getCertification} 151 | * 152 | * @param {string} impUid 153 | * @returns {Promise} result 154 | */ 155 | getCertification(impUid) { 156 | const spec = { 157 | method: 'GET', 158 | url: `/certifications/${impUid}` 159 | }; 160 | 161 | return this._updateToken() 162 | .then(() => this._request(spec)); 163 | } 164 | 165 | /** 166 | * SMS 본인인증 정보 삭제 167 | * DELETE - https://api.iamport.kr/certifications/{imp_uid} 168 | * @see {@link https://api.iamport.kr/#!/certifications/deleteCertification} 169 | * 170 | * @returns {Promise} result 171 | */ 172 | deleteCertification(impUid) { 173 | const spec = { 174 | method: 'DELETE', 175 | url: `/certifications/${impUid}` 176 | }; 177 | 178 | return this._updateToken() 179 | .then(() => this._request(spec)); 180 | } 181 | 182 | /** 183 | * 에스크로 결제 배송정보 등록 184 | * POST - https://api.iamport.kr/escrows/logis/{imp_uid} 185 | * @see {@link https://api.iamport.kr/#!/escrow.logis/escrow_logis_save} 186 | * 187 | * @returns {Promise} result 188 | */ 189 | 190 | /** 191 | * 아임포트 고유 아이디로 결제 정보 조회 192 | * GET - https://api.iamport.kr/payments/{imp_uid} 193 | * @see {@link https://api.iamport.kr/#!/payments/getPaymentByImpUid} 194 | * 195 | * @param {string} impUid 196 | * @returns {Promise} result 197 | */ 198 | findByImpUid(impUid) { 199 | const spec = { 200 | method: 'GET', 201 | url: `/payments/${impUid}` 202 | }; 203 | 204 | return this._updateToken() 205 | .then(() => this._request(spec)); 206 | } 207 | 208 | /** 209 | * 상점 고유 아이디로 결제 정보 조회 210 | * GET - https://api.iamport.kr/payments/find/{merchant_uid} 211 | * @see {@link https://api.iamport.kr/#!/payments/getPaymentByMerchantUid} 212 | * 213 | * @param {string} merchantUid 214 | * @returns {Promise} result 215 | */ 216 | findByMerchantUid(merchantUid) { 217 | const spec = { 218 | method: 'GET', 219 | url: `/payments/find/${merchantUid}` 220 | }; 221 | 222 | return this._updateToken() 223 | .then(() => this._request(spec)); 224 | } 225 | 226 | /** 227 | * 상점 고유 아이디로 결제 정보 목록 조회 228 | * GET - https://api.iamport.kr/payments/findAll/{merchant_uid}/{payment_status} 229 | * @see {@link https://api.iamport.kr/#!/payments/getAllPaymentsByMerchantUid} 230 | * 231 | * @param {string} merchantUid 232 | * @param {Object} [extra] 233 | * @param {string} [extra.status] 234 | * @returns {Promise} result 235 | */ 236 | findAllByMerchantUid(merchantUid, extra = { 237 | status: '' 238 | }) { 239 | 240 | const spec = { 241 | method: 'GET', 242 | url: `/payments/findAll/${merchantUid}/${extra.status}` 243 | }; 244 | 245 | return this._updateToken() 246 | .then(() => this._request(spec)); 247 | } 248 | 249 | /** 250 | * 결제 상태로 결제 정보 목록 조회 251 | * GET - https://api.iamport.kr/payments/status/{payment_status} 252 | * @see {@link https://api.iamport.kr/#!/payments/getPaymentsByStatus} 253 | * 254 | * @param {string} [status=all] 255 | * @param {Object} [extra] 256 | * @param {} extra.page 257 | * @param {} extra.from 258 | * @param {} extra.to 259 | * @returns {Promise} result 260 | */ 261 | findAllByStatus(status = 'all', extra = {}) { 262 | const spec = { 263 | method: 'GET', 264 | url: `/payments/status/${status}`, 265 | params: extra 266 | }; 267 | 268 | return this._updateToken() 269 | .then(() => this._request(spec)); 270 | } 271 | 272 | /** 273 | * 결제취소 274 | * POST - https://api.iamport.kr/payments/cancel 275 | * @see {@link https://api.iamport.kr/#!/payments/cancelPayment} 276 | * 277 | * @param {Object} [data={}] 278 | * @returns {Promise} result 279 | */ 280 | cancel(data = {}) { 281 | const spec = { 282 | method: 'POST', 283 | url: '/payments/cancel', 284 | data: data 285 | }; 286 | 287 | return this._updateToken() 288 | .then(() => this._request(spec)); 289 | } 290 | 291 | /** 292 | * 아임포트 고유 아이디로 결제취소 293 | * POST - https://api.iamport.kr/payments/cancel 294 | * @see {@link https://api.iamport.kr/#!/payments/cancelPayment} 295 | * 296 | * @param {string} impUid 297 | * @param {Object} [extra={}] 298 | * @returns {Promise} result 299 | */ 300 | cancelByImpUid(impUid, extra = {}) { 301 | const data = Object.assign(extra, { 'imp_uid': impUid }); 302 | return this.cancel(data); 303 | } 304 | 305 | /** 306 | * 상점 고유 아이디로 결제취소 307 | * POST - https://api.iamport.kr/payments/cancel 308 | * @see {@link https://api.iamport.kr/#!/payments/cancelPayment} 309 | * 310 | * @param {string} merchantUid 311 | * @param {Object} [extra={}] 312 | * @returns {Promise} result 313 | */ 314 | cancelByMerchantUid(merchantUid, extra = {}) { 315 | const data = Object.assign(extra, { 'merchant_uid': merchantUid }); 316 | return this.cancel(data); 317 | } 318 | 319 | /** 320 | * 결제예정금액 사전등록 321 | * POST - https://api.iamport.kr/payments/prepare 322 | * @see {@link https://api.iamport.kr/#!/payments.validation/preparePayment} 323 | * 324 | * @param {Object} data 325 | * @returns {Promise} result 326 | */ 327 | createPreparedPayment(data = {}) { 328 | const requiredParams = ['merchant_uid', 'amount']; 329 | 330 | if (!requiredParams.every(param => data.hasOwnProperty(param))) 331 | return Promise.reject(new IamporterError(`파라미터 누락: ${requiredParams}`)); 332 | 333 | const spec = { 334 | method: 'POST', 335 | url: '/payments/prepare', 336 | data: data 337 | }; 338 | 339 | return this._updateToken() 340 | .then(() => this._request(spec)); 341 | } 342 | 343 | /** 344 | * 사전등록된 결제정보 조회 345 | * GET - https://api.iamport.kr/payments/prepare/{merchant_uid} 346 | * @see {@link https://api.iamport.kr/#!/payments.validation/getPaymentPrepareByMerchantUid} 347 | * 348 | * @param {string} merchantUid 349 | * @returns {Promise} result 350 | */ 351 | getPreparedPayment(merchantUid) { 352 | const spec = { 353 | method: 'GET', 354 | url: `/payments/prepare/${merchantUid}` 355 | }; 356 | 357 | return this._updateToken() 358 | .then(() => this._request(spec)); 359 | } 360 | 361 | /** 362 | * 비인증 신용카드 결제요청 363 | * POST - https://api.iamport.kr/subscribe/payments/onetime 364 | * @see {@link https://api.iamport.kr/#!/subscribe/onetime} 365 | * 366 | * @param {Object} data 367 | * @returns {Promise} result 368 | */ 369 | payOnetime(data = {}) { 370 | const requiredParams = [ 371 | 'merchant_uid', 'amount', 'card_number', 'expiry', 372 | 'birth' 373 | ]; 374 | 375 | if (!requiredParams.every(param => data.hasOwnProperty(param))) 376 | return Promise.reject(new IamporterError(`파라미터 누락: ${requiredParams}`)); 377 | 378 | const spec = { 379 | method: 'POST', 380 | url: '/subscribe/payments/onetime', 381 | data: data 382 | }; 383 | 384 | return this._updateToken() 385 | .then(() => this._request(spec)) 386 | .then((res) => this._validatePayment(data.amount, res)); 387 | } 388 | 389 | /** 390 | * 비인증 빌링키 결제요청 391 | * POST - https://api.iamport.kr/subscribe/payments/again 392 | * @see {@link https://api.iamport.kr/#!/subscribe/again} 393 | * 394 | * @param {Object} data 395 | * @returns {Promise} result 396 | */ 397 | paySubscription(data = {}) { 398 | const requiredParams = [ 399 | 'customer_uid', 'merchant_uid', 'amount' 400 | ]; 401 | 402 | if (!requiredParams.every(param => data.hasOwnProperty(param))) 403 | return Promise.reject(new IamporterError(`파라미터 누락: ${requiredParams}`)); 404 | 405 | const spec = { 406 | method: 'POST', 407 | url: '/subscribe/payments/again', 408 | data: data 409 | }; 410 | 411 | return this._updateToken() 412 | .then(() => this._request(spec)) 413 | .then((res) => this._validatePayment(data.amount, res)); 414 | } 415 | 416 | /** 417 | * 해외카드 결제요청 418 | * POST - https://api.iamport.kr/subscribe/payments/foreign 419 | * 420 | * @param {Object} data 421 | * @returns {Promise} result 422 | */ 423 | payForeign(data = {}) { 424 | const requiredParams = [ 425 | 'merchant_uid', 'amount', 'card_number', 'expiry' 426 | ]; 427 | 428 | if (!requiredParams.every(param => data.hasOwnProperty(param))) 429 | return Promise.reject(new IamporterError(`파라미터 누락: ${requiredParams}`)); 430 | 431 | const spec = { 432 | method: 'POST', 433 | url: '/subscribe/payments/foreign', 434 | data: data 435 | }; 436 | 437 | return this._updateToken() 438 | .then(() => this._request(spec)) 439 | .then((res) => this._validatePayment(data.amount, res)); 440 | } 441 | 442 | /** 443 | * POST - https://api.iamport.kr/subscribe/payments/schedule 444 | * @see {@link https://api.iamport.kr/#!/subscribe/schedule} 445 | * 446 | * @returns {Promise} result 447 | */ 448 | 449 | /** 450 | * POST - https://api.iamport.kr/subscribe/payments/unschedule 451 | * @see {@link https://api.iamport.kr/#!/subscribe/unschedule} 452 | * 453 | * @returns {Promise} result 454 | */ 455 | 456 | /** 457 | * 구매자 빌링키 발급 458 | * POST - https://api.iamport.kr/subscribe/customers/{customer_uid} 459 | * @see {@link https://api.iamport.kr/#!/subscribe.customer/customer_save} 460 | * 461 | * @param {Object} data 462 | * @returns {Promise} result 463 | */ 464 | createSubscription(data = {}) { 465 | const requiredParams = [ 466 | 'customer_uid', 'card_number', 'expiry', 'birth', 467 | ]; 468 | 469 | if (!requiredParams.every(param => data.hasOwnProperty(param))) 470 | return Promise.reject(new IamporterError(`파라미터 누락: ${requiredParams}`)); 471 | 472 | const spec = { 473 | method: 'POST', 474 | url: `/subscribe/customers/${data['customer_uid']}`, 475 | data: data 476 | }; 477 | 478 | return this._updateToken() 479 | .then(() => this._request(spec)); 480 | } 481 | 482 | /** 483 | * 구매자 빌링키 조회 484 | * GET - https://api.iamport.kr/subscribe/customers/{customer_uid} 485 | * @see {@link https://api.iamport.kr/#!/subscribe.customer/customer_view} 486 | * 487 | * @param {string} customerUid 488 | * @returns {Promise} result 489 | */ 490 | getSubscription(customerUid) { 491 | const spec = { 492 | method: 'GET', 493 | url: `/subscribe/customers/${customerUid}` 494 | }; 495 | 496 | return this._updateToken() 497 | .then(() => this._request(spec)); 498 | } 499 | 500 | /** 501 | * 구매자 빌링키 삭제 502 | * DELETE - https://api.iamport.kr/subscribe/customers/{customer_uid} 503 | * @see {@link https://api.iamport.kr/#!/subscribe.customer/customer_delete} 504 | * 505 | * @param {string} customerUid 506 | * @returns {Promise} result 507 | */ 508 | deleteSubscription(customerUid) { 509 | const spec = { 510 | method: 'DELETE', 511 | url: `/subscribe/customers/${customerUid}` 512 | }; 513 | 514 | return this._updateToken() 515 | .then(() => this._request(spec)); 516 | } 517 | 518 | /** 519 | * 가상계좌 발급 520 | * POST - https://api.iamport.kr/vbanks 521 | * @see {@link https://api.iamport.kr/#!/vbanks/createVbank} 522 | * 523 | * @param {Object} data 524 | * @returns {Promise} result 525 | */ 526 | createVbank(data = {}) { 527 | const requiredParams = [ 528 | 'merchant_uid', 'amount', 'vbank_code', 'vbank_due', 'vbank_holder' 529 | ]; 530 | 531 | if (!requiredParams.every(param => data.hasOwnProperty(param))) 532 | return Promise.reject(new IamporterError(`파라미터 누락: ${requiredParams}`)); 533 | 534 | const spec = { 535 | method: 'POST', 536 | url: '/vbanks', 537 | data: data 538 | }; 539 | 540 | return this._updateToken() 541 | .then(() => this._request(spec)); 542 | } 543 | } 544 | 545 | module.exports = Iamporter; 546 | --------------------------------------------------------------------------------