From b5a7f48a8313ba558451d479c3319cc24bcabec7 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Mon, 13 Nov 2023 21:12:46 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81=20?= =?UTF-8?q?=EC=8B=A4=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 웹팩 빌드 환경 구성 - DynamicModule 대신 StaticModule 사용 - 탭 2칸으로 변경 --- .github/dependabot.yml | 16 +- .gitignore | 1 + .prettierignore | 2 +- .vscode/settings.json | 2 +- packages/fake-iamport-server/nestia.config.ts | 2 +- packages/fake-iamport-server/package.json | 17 +- .../src/FakeIamportBackend.ts | 90 +- .../src/FakeIamportConfiguration.ts | 148 +- .../src/FakeIamportModule.ts | 26 +- .../src/api/IamportConnector.ts | 105 +- .../src/api/functional/index.ts | 4 +- .../src/api/structures/IIamportCardPayment.ts | 42 +- .../api/structures/IIamportCertification.ts | 282 +- .../src/api/structures/IIamportPayment.ts | 361 +- .../api/structures/IIamportPaymentCancel.ts | 120 +- .../src/api/structures/IIamportReceipt.ts | 200 +- .../src/api/structures/IIamportResponse.ts | 28 +- .../api/structures/IIamportSubscription.ts | 310 +- .../api/structures/IIamportTransferPayment.ts | 18 +- .../src/api/structures/IIamportUser.ts | 58 +- .../api/structures/IIamportVBankPayment.ts | 192 +- .../src/api/typings/Atomic.ts | 2 +- .../src/api/typings/Writable.ts | 4 +- .../FakeIamportCertificationsController.ts | 300 +- .../FakeIamportInternalController.ts | 116 +- .../FakeIamportPaymentsController.ts | 101 +- .../FakeIamportReceiptsController.ts | 173 +- .../controllers/FakeIamportUsersController.ts | 50 +- .../FakeIamportVbanksController.ts | 204 +- ...FakeIamportSubscribeCustomersController.ts | 200 +- .../FakeIamportSubscribePaymentsController.ts | 363 +- .../src/decorators/FakeIamportUserAuth.ts | 51 + .../src/executable/server.ts | 64 +- .../providers/FakeIamportPaymentProvider.ts | 127 +- .../providers/FakeIamportResponseProvider.ts | 16 +- .../src/providers/FakeIamportStorage.ts | 28 +- .../FakeIamportSubscriptionProvider.ts | 47 +- .../src/providers/FakeIamportUserAuth.ts | 37 - .../src/utils/AdvancedRandomGenerator.ts | 22 +- .../fake-iamport-server/src/utils/DateUtil.ts | 242 +- .../src/utils/ErrorUtil.ts | 10 +- .../fake-iamport-server/src/utils/Terminal.ts | 26 +- .../src/utils/VolatileMap.ts | 114 +- .../test/features/test_fake_card_payment.ts | 119 +- .../features/test_fake_card_payment_cancel.ts | 99 +- .../test_fake_card_payment_cancel_over.ts | 48 +- .../test_fake_card_payment_cancel_partial.ts | 85 +- .../test/features/test_fake_certification.ts | 100 +- .../test/features/test_fake_receipt.ts | 50 +- .../test_fake_subscription_payment_again.ts | 141 +- .../test_fake_subscription_payment_cancel.ts | 99 +- ...t_fake_subscription_payment_cancel_over.ts | 48 +- ...ake_subscription_payment_cancel_partial.ts | 85 +- .../test_fake_subscription_payment_onetime.ts | 161 +- .../test/features/test_fake_vbank_payment.ts | 192 +- .../test_fake_vbank_payment_cancel.ts | 109 +- .../test_fake_vbank_payment_cancel_over.ts | 50 +- .../test_fake_vbank_payment_cancel_partial.ts | 95 +- ...ake_vbank_payment_cancel_without_refund.ts | 34 +- packages/fake-iamport-server/test/index.ts | 66 +- .../fake-iamport-server/test/tsconfig.json | 5 +- .../nestia.config.ts | 2 +- .../fake-toss-payments-server/package.json | 15 +- .../src/FakeTossBackend.ts | 90 +- .../src/FakeTossConfiguration.ts | 113 +- .../src/FakeTossModule.ts | 20 +- .../src/api/structures/ITossBilling.ts | 252 +- .../src/api/structures/ITossCardPayment.ts | 392 +- .../src/api/structures/ITossCashReceipt.ts | 206 +- .../structures/ITossGiftCertificatePayment.ts | 32 +- .../api/structures/ITossMobilePhonePayment.ts | 40 +- .../src/api/structures/ITossPayment.ts | 374 +- .../src/api/structures/ITossPaymentCancel.ts | 132 +- .../src/api/structures/ITossPaymentWebhook.ts | 64 +- .../api/structures/ITossTransferPayment.ts | 32 +- .../structures/ITossVirtualAccountPayment.ts | 230 +- .../controllers/FakeTossBillingController.ts | 252 +- .../FakeTossCashReceiptsController.ts | 172 +- .../controllers/FakeTossInternalController.ts | 127 +- .../controllers/FakeTossPaymentsController.ts | 349 +- .../FakeTossVirtualAccountsController.ts | 130 +- .../src/decorators/FakeTossUserAuth.ts | 38 + .../src/executable/server.ts | 64 +- .../src/providers/FakeTossPaymentProvider.ts | 42 +- .../src/providers/FakeTossStorage.ts | 22 +- .../src/providers/FakeTossUserAuth.ts | 25 - .../src/providers/FakeTossWebhookProvider.ts | 28 +- .../src/utils/DateUtil.ts | 242 +- .../src/utils/ErrorUtil.ts | 77 +- .../src/utils/Terminal.ts | 26 +- .../src/utils/VolatileMap.ts | 102 +- .../internal/validate_fake_payment_cancel.ts | 117 +- .../validate_fake_payment_cancel_over.ts | 64 +- .../validate_fake_payment_cancel_partial.ts | 101 +- .../features/test_fake_billing_payment.ts | 64 +- .../test_fake_billing_payment_cancel.ts | 4 +- .../test_fake_billing_payment_cancel_over.ts | 2 +- ...est_fake_billing_payment_cancel_partial.ts | 8 +- .../test/features/test_fake_card_payment.ts | 138 +- .../features/test_fake_card_payment_cancel.ts | 4 +- .../test_fake_card_payment_cancel_over.ts | 2 +- .../test_fake_card_payment_cancel_partial.ts | 2 +- .../test/features/test_fake_cash_receipt.ts | 40 +- .../features/test_fake_storage_capacity.ts | 123 +- .../test_fake_storage_expiration_time.ts | 65 +- .../test_fake_virtual_account_payment.ts | 136 +- ...est_fake_virtual_account_payment_cancel.ts | 5 +- ...ake_virtual_account_payment_cancel_over.ts | 8 +- ..._virtual_account_payment_cancel_partial.ts | 8 +- .../fake-toss-payments-server/test/index.ts | 54 +- .../test/internal/AdvancedRandomGenerator.ts | 22 +- .../test/internal/TestConnection.ts | 28 +- .../test/tsconfig.json | 5 +- packages/iamport-server-api/package.json | 6 +- packages/iamport-server-api/swagger.json | 3990 ++++++++--------- packages/payment-api/package.json | 10 +- packages/payment-api/swagger.json | 2 +- packages/payment-backend/docs/ERD.md | 6 +- packages/payment-backend/nestia.config.ts | 14 +- packages/payment-backend/package.json | 34 +- packages/payment-backend/src/PaymentAsset.ts | 18 +- .../payment-backend/src/PaymentBackend.ts | 185 +- .../src/PaymentConfiguration.ts | 154 +- packages/payment-backend/src/PaymentGlobal.ts | 180 +- packages/payment-backend/src/PaymentModule.ts | 28 +- .../payment-backend/src/PaymentSetupWizard.ts | 34 +- .../payment-backend/src/PaymentUpdator.ts | 159 +- .../api/functional/monitors/health/index.ts | 2 +- .../functional/monitors/performance/index.ts | 2 +- .../api/functional/monitors/system/index.ts | 2 +- .../src/api/structures/common/IEntity.ts | 12 +- .../api/structures/monitors/IPerformance.ts | 6 +- .../src/api/structures/monitors/ISystem.ts | 124 +- .../payments/IPaymentCancelHistory.ts | 134 +- .../structures/payments/IPaymentHistory.ts | 230 +- .../payments/IPaymentReservation.ts | 148 +- .../api/structures/payments/IPaymentSource.ts | 62 +- .../api/structures/payments/IPaymentVendor.ts | 76 +- .../structures/payments/IPaymentWebhook.ts | 54 +- .../payments/IPaymentWebhookHistory.ts | 54 +- .../controllers/monitors/HealthController.ts | 7 - .../monitors/MonitorHealthController.ts | 7 + .../monitors/MonitorPerformanceController.ts | 16 + .../monitors/MonitorSystemController.ts | 19 + .../monitors/PerformanceController.ts | 16 - .../controllers/monitors/SystemController.ts | 19 - .../payments/PaymentHistoriesController.ts | 126 +- .../payments/PaymentInternalController.ts | 18 +- .../payments/PaymentReservationsController.ts | 88 +- .../payments/PaymentWebhooksController.ts | 90 +- .../payment-backend/src/executable/master.ts | 6 +- .../payment-backend/src/executable/monitor.ts | 36 +- .../payment-backend/src/executable/schema.ts | 116 +- .../payment-backend/src/executable/server.ts | 97 +- .../payment-backend/src/executable/update.ts | 76 +- .../src/executable/updator-master.ts | 8 +- .../src/executable/updator-slave.ts | 6 +- .../payment-backend/src/modules/express.ts | 3 - .../payment-backend/src/modules/nestjs.ts | 3 - .../src/providers/monitors/SystemProvider.ts | 66 +- .../providers/payments/FakePaymentStorage.ts | 2 +- .../payments/PaymentCancelHistoryProvider.ts | 163 +- .../payments/PaymentHistoryProvider.ts | 339 +- .../payments/PaymentReservationProvider.ts | 184 +- .../payments/PaymentWebhookProvider.ts | 187 +- .../src/schedulers/Scheduler.ts | 30 - .../schedule_something_in_every_day.ts | 9 - .../schedule_something_in_every_hour.ts | 9 - .../schedule_something_in_every_minutes.ts | 3 - .../src/services/iamport/IamportAsset.ts | 54 +- .../services/iamport/IamportPaymentService.ts | 134 +- .../src/services/toss/TossAsset.ts | 32 +- .../toss/TossPaymentBillingService.ts | 58 +- .../services/toss/TossPaymentCardService.ts | 18 +- .../src/services/toss/TossPaymentService.ts | 154 +- .../toss/TossPaymentVirtualAccountService.ts | 18 +- .../src/utils/ArgumentParser.ts | 132 +- .../payment-backend/src/utils/BcryptUtil.ts | 12 +- .../payment-backend/src/utils/DateUtil.ts | 184 +- .../payment-backend/src/utils/ErrorUtil.ts | 79 +- .../payment-backend/src/utils/Terminal.ts | 26 +- .../payment-backend/src/utils/TokenManager.ts | 141 +- .../iamport/test_api_iamport_card_payment.ts | 202 +- .../test_api_iamport_card_payment_cancel.ts | 12 +- ...api_iamport_card_payment_cancel_partial.ts | 12 +- .../test_api_iamport_subscription_payment.ts | 204 +- .../iamport/test_api_iamport_vbank_payment.ts | 200 +- .../test_api_iamport_vbank_payment_cancel.ts | 22 +- ...pi_iamport_vbank_payment_cancel_partial.ts | 22 +- .../internal/validate_payment_cancel.ts | 84 +- .../validate_payment_cancel_partial.ts | 80 +- .../monitors/test_api_monitor_health_check.ts | 4 +- .../monitors/test_api_monitor_system.ts | 10 +- .../toss/test_api_toss_card_payment.ts | 260 +- .../toss/test_api_toss_card_payment_cancel.ts | 10 +- ...st_api_toss_card_payment_cancel_partial.ts | 10 +- .../test_api_toss_subscription_payment.ts | 218 +- .../toss/test_api_toss_vbank_payment.ts | 209 +- .../test_api_toss_vbank_payment_cancel.ts | 22 +- ...t_api_toss_vbank_payment_cancel_partial.ts | 22 +- packages/payment-backend/test/index.ts | 213 +- .../payment-backend/test/manual/password.ts | 8 +- packages/payment-backend/test/tsconfig.json | 15 +- packages/payment-backend/tsconfig.json | 1 - packages/payment-backend/webpack.config.js | 79 + .../toss-payments-server-api/package.json | 6 +- .../toss-payments-server-api/swagger.json | 16 +- prettier.config.js | 7 +- 208 files changed, 10473 insertions(+), 10589 deletions(-) create mode 100644 packages/fake-iamport-server/src/decorators/FakeIamportUserAuth.ts delete mode 100644 packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts create mode 100644 packages/fake-toss-payments-server/src/decorators/FakeTossUserAuth.ts delete mode 100644 packages/fake-toss-payments-server/src/providers/FakeTossUserAuth.ts delete mode 100644 packages/payment-backend/src/controllers/monitors/HealthController.ts create mode 100644 packages/payment-backend/src/controllers/monitors/MonitorHealthController.ts create mode 100644 packages/payment-backend/src/controllers/monitors/MonitorPerformanceController.ts create mode 100644 packages/payment-backend/src/controllers/monitors/MonitorSystemController.ts delete mode 100644 packages/payment-backend/src/controllers/monitors/PerformanceController.ts delete mode 100644 packages/payment-backend/src/controllers/monitors/SystemController.ts delete mode 100644 packages/payment-backend/src/modules/express.ts delete mode 100644 packages/payment-backend/src/modules/nestjs.ts delete mode 100644 packages/payment-backend/src/schedulers/Scheduler.ts delete mode 100644 packages/payment-backend/src/schedulers/features/schedule_something_in_every_day.ts delete mode 100644 packages/payment-backend/src/schedulers/features/schedule_something_in_every_hour.ts delete mode 100644 packages/payment-backend/src/schedulers/features/schedule_something_in_every_minutes.ts create mode 100644 packages/payment-backend/webpack.config.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ec87577..d59908b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,9 +6,8 @@ updates: interval: 'daily' open-pull-requests-limit: 25 allow: - - dependency-name: "@nestia/core" - - dependency-name: "@nestia/e2e" - - dependency-name: "@nestia/sdk" + - dependency-name: "@nestia/*" + - dependency-name: "@nestjs/*" - dependency-name: "nestia" - dependency-name: "tstl" - dependency-name: "typia" @@ -18,9 +17,8 @@ updates: interval: 'daily' open-pull-requests-limit: 25 allow: - - dependency-name: "@nestia/core" - - dependency-name: "@nestia/e2e" - - dependency-name: "@nestia/sdk" + - dependency-name: "@nestia/*" + - dependency-name: "@nestjs/*" - dependency-name: "nestia" - dependency-name: "tstl" - dependency-name: "typia" @@ -30,10 +28,8 @@ updates: interval: 'daily' open-pull-requests-limit: 25 allow: - - dependency-name: "@nestia/core" - - dependency-name: "@nestia/e2e" - - dependency-name: "@nestia/sdk" - - dependency-name: "nestia" + - dependency-name: "@nestia/*" + - dependency-name: "@nestjs/*" - dependency-name: "tstl" - dependency-name: "typia" diff --git a/.gitignore b/.gitignore index 688a56c..c70043f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ bin/ +dist/ lib/ migrations/ node_modules/ diff --git a/.prettierignore b/.prettierignore index 7783ae8..93a6b6f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,4 +9,4 @@ packages/*/src/api/utils packages/*/src/api/*.ts README.md -packages/*/tsconfig.json \ No newline at end of file +tsconfig.json \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index c9887a4..fc8d545 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "editor.tabSize": 4, + "editor.tabSize": 2, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, diff --git a/packages/fake-iamport-server/nestia.config.ts b/packages/fake-iamport-server/nestia.config.ts index 076611a..14f4c02 100644 --- a/packages/fake-iamport-server/nestia.config.ts +++ b/packages/fake-iamport-server/nestia.config.ts @@ -4,7 +4,7 @@ import { NestFactory } from "@nestjs/core"; import { FakeIamportModule } from "./src/FakeIamportModule"; const NESTIA_CONFIG: INestiaConfig = { - input: async () => NestFactory.create(await FakeIamportModule()), + input: () => NestFactory.create(FakeIamportModule), output: "src/api", simulate: true, distribute: "../iamport-server-api", diff --git a/packages/fake-iamport-server/package.json b/packages/fake-iamport-server/package.json index 4e7fbc4..6360526 100644 --- a/packages/fake-iamport-server/package.json +++ b/packages/fake-iamport-server/package.json @@ -1,6 +1,6 @@ { "name": "fake-iamport-server", - "version": "5.0.6", + "version": "5.1.0", "description": "Fake iamport server for testing", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -15,7 +15,6 @@ "dev": "npm run build:test -- --watch", "eslint": "eslint src && eslint --config .eslintrc.test.cjs test", "eslint:fix": "eslint --fix src && eslint --fix --config .eslintrc.test.cjs test", - "prettier": "prettier src --write && prettier test --write", "------------------------------------------------": "", "package:api": "npm run build:swagger && npm run build:api && cd packages/api && npm publish", "package:latest": "npm run build && npm run test && npm publish", @@ -39,8 +38,7 @@ }, "homepage": "https://github.com/samchon/fake-iamport-server", "devDependencies": { - "@nestia/sdk": "^2.3.4", - "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@nestia/sdk": "^2.3.9", "@types/atob": "^2.1.2", "@types/btoa": "^1.2.3", "@types/cli": "^0.11.19", @@ -52,7 +50,6 @@ "copyfiles": "^2.4.1", "nestia": "^5.0.1", "pm2": "^4.5.6", - "prettier": "^2.6.2", "rimraf": "^3.0.2", "sloc": "^0.2.1", "ts-node": "^10.9.1", @@ -60,14 +57,18 @@ "typescript": "^5.2.2" }, "dependencies": { - "@nestia/core": "^2.3.4", + "@nestia/core": "^2.3.9", "@nestia/e2e": "^0.3.6", - "@nestia/fetcher": "^2.3.4", + "@nestia/fetcher": "^2.3.9", + "@nestjs/common": "^10.2.8", + "@nestjs/core": "^10.2.8", + "@nestjs/platform-fastify": "^10.2.8", + "fastify": "^4.24.3", "serialize-error": "^4.1.0", "source-map-support": "^0.5.19", "tstl": "^2.5.13", "typescript-transform-paths": "^3.4.6", - "typia": "^5.2.4", + "typia": "^5.2.6", "uuid": "^9.0.0" }, "keywords": [ diff --git a/packages/fake-iamport-server/src/FakeIamportBackend.ts b/packages/fake-iamport-server/src/FakeIamportBackend.ts index eb0e89b..e540e3c 100644 --- a/packages/fake-iamport-server/src/FakeIamportBackend.ts +++ b/packages/fake-iamport-server/src/FakeIamportBackend.ts @@ -1,7 +1,7 @@ import { NestFactory } from "@nestjs/core"; import { - FastifyAdapter, - NestFastifyApplication, + FastifyAdapter, + NestFastifyApplication, } from "@nestjs/platform-fastify"; import { FakeIamportConfiguration } from "./FakeIamportConfiguration"; @@ -13,47 +13,47 @@ import { FakeIamportModule } from "./FakeIamportModule"; * @author Samchon */ export class FakeIamportBackend { - private application_?: NestFastifyApplication; - - /** - * 서버 개설. - */ - public async open(): Promise { - //---- - // OPEN THE BACKEND SERVER - //---- - // MOUNT CONTROLLERS - this.application_ = await NestFactory.create( - await FakeIamportModule(), - new FastifyAdapter(), - { logger: false }, - ); - - // DO OPEN - this.application_.enableCors(); - await this.application_.listen(FakeIamportConfiguration.API_PORT); - - //---- - // POST-PROCESSES - //---- - // INFORM TO THE PM2 - if (process.send) process.send("ready"); - - // WHEN KILL COMMAND COMES - process.on("SIGINT", async () => { - await this.close(); - process.exit(0); - }); - } - - /** - * 서버 폐쇄. - */ - public async close(): Promise { - if (this.application_ === undefined) return; - - // DO CLOSE - await this.application_.close(); - delete this.application_; - } + private application_?: NestFastifyApplication; + + /** + * 서버 개설. + */ + public async open(): Promise { + //---- + // OPEN THE BACKEND SERVER + //---- + // MOUNT CONTROLLERS + this.application_ = await NestFactory.create( + FakeIamportModule, + new FastifyAdapter(), + { logger: false }, + ); + + // DO OPEN + this.application_.enableCors(); + await this.application_.listen(FakeIamportConfiguration.API_PORT); + + //---- + // POST-PROCESSES + //---- + // INFORM TO THE PM2 + if (process.send) process.send("ready"); + + // WHEN KILL COMMAND COMES + process.on("SIGINT", async () => { + await this.close(); + process.exit(0); + }); + } + + /** + * 서버 폐쇄. + */ + public async close(): Promise { + if (this.application_ === undefined) return; + + // DO CLOSE + await this.application_.close(); + delete this.application_; + } } diff --git a/packages/fake-iamport-server/src/FakeIamportConfiguration.ts b/packages/fake-iamport-server/src/FakeIamportConfiguration.ts index 1f48681..c9fdbca 100644 --- a/packages/fake-iamport-server/src/FakeIamportConfiguration.ts +++ b/packages/fake-iamport-server/src/FakeIamportConfiguration.ts @@ -1,12 +1,16 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; +import { + ConflictException, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException, +} from "@nestjs/common"; import { DomainError } from "tstl/exception/DomainError"; import { InvalidArgument } from "tstl/exception/InvalidArgument"; import { OutOfRange } from "tstl/exception/OutOfRange"; /* eslint-disable */ - -const EXTENSION = __filename.substr(-2); +const EXTENSION = __filename.substring(__filename.length - 2); if (EXTENSION === "js") require("source-map-support").install(); /** @@ -15,100 +19,100 @@ if (EXTENSION === "js") require("source-map-support").install(); * @author Samchon */ export namespace FakeIamportConfiguration { - /** - * @internal - */ - export const ASSETS = __dirname + "/../assets"; + /** + * @internal + */ + export const ASSETS = __dirname + "/../assets"; - /** - * 유저 토큰의 유효 시간. - */ - export const USER_EXPIRATION_TIME: number = -3 * 60 * 1000; + /** + * 유저 토큰의 유효 시간. + */ + export const USER_EXPIRATION_TIME: number = -3 * 60 * 1000; - /** - * 임시 저장소의 레코드 만료 기한. - */ - export const STORAGE_EXPIRATION: IExpiration = { - time: 3 * 60 * 1000, - capacity: 1000, - }; + /** + * 임시 저장소의 레코드 만료 기한. + */ + export const STORAGE_EXPIRATION: IExpiration = { + time: 3 * 60 * 1000, + capacity: 1000, + }; - /** - * 서버가 사용할 포트 번호. - */ - export let API_PORT: number = 10851; + /** + * 서버가 사용할 포트 번호. + */ + export let API_PORT: number = 10851; + + /** + * Webhook 이벤트를 수신할 URL 주소. + */ + export let WEBHOOK_URL: string = `http://127.0.0.1:${API_PORT}/internal/webhook`; + /** + * 토큰 발행 전 인증 함수. + * + * 클라이언트가 전송한 api 및 secret key 값이 제대로 된 것인지 판별한다. + * + * @param accessor 인증 키 값 + */ + export let authorize: (accessor: IAccessor) => boolean = (accessor) => { + return ( + accessor.imp_key === "test_imp_key" && + accessor.imp_secret === "test_imp_secret" + ); + }; + + /** + * 아임포트에서 부여해 준 API 및 secret 키. + */ + export interface IAccessor { /** - * Webhook 이벤트를 수신할 URL 주소. + * API 키. */ - export let WEBHOOK_URL: string = `http://127.0.0.1:${API_PORT}/internal/webhook`; + imp_key: string; /** - * 토큰 발행 전 인증 함수. - * - * 클라이언트가 전송한 api 및 secret key 값이 제대로 된 것인지 판별한다. - * - * @param accessor 인증 키 값 + * Secret 키. */ - export let authorize: (accessor: IAccessor) => boolean = (accessor) => { - return ( - accessor.imp_key === "test_imp_key" && - accessor.imp_secret === "test_imp_secret" - ); - }; + imp_secret: string; + } + /** + * 임시 저장소의 레코드 만료 기한. + */ + export interface IExpiration { /** - * 아임포트에서 부여해 준 API 및 secret 키. + * 만료 시간. */ - export interface IAccessor { - /** - * API 키. - */ - imp_key: string; - - /** - * Secret 키. - */ - imp_secret: string; - } + time: number; /** - * 임시 저장소의 레코드 만료 기한. + * 최대 수용량. */ - export interface IExpiration { - /** - * 만료 시간. - */ - time: number; - - /** - * 최대 수용량. - */ - capacity: number; - } + capacity: number; + } } // CUSTOM EXCEPTIION CONVERSION core.ExceptionManager.insert( - OutOfRange, - (exp) => new nest.NotFoundException(exp.message), + OutOfRange, + (exp) => new NotFoundException(exp.message), ); core.ExceptionManager.insert( - InvalidArgument, - (exp) => new nest.ConflictException(exp.message), + InvalidArgument, + (exp) => new ConflictException(exp.message), ); core.ExceptionManager.insert( - DomainError, - (exp) => new nest.UnprocessableEntityException(exp.message), + DomainError, + (exp) => new UnprocessableEntityException(exp.message), ); // TRACE EXACT SERVER INTERNAL ERROR core.ExceptionManager.insert( - Error, - (exp) => - new nest.InternalServerErrorException({ - message: exp.message, - name: exp.name, - stack: exp.stack, - }), + Error, + (exp) => + new InternalServerErrorException({ + message: exp.message, + name: exp.name, + stack: exp.stack, + }), ); diff --git a/packages/fake-iamport-server/src/FakeIamportModule.ts b/packages/fake-iamport-server/src/FakeIamportModule.ts index e6b4dbc..5336064 100644 --- a/packages/fake-iamport-server/src/FakeIamportModule.ts +++ b/packages/fake-iamport-server/src/FakeIamportModule.ts @@ -1,4 +1,24 @@ -import { DynamicModule } from "@nestia/core"; +import { Module } from "@nestjs/common"; -export const FakeIamportModule = () => - DynamicModule.mount(__dirname + "/controllers"); +import { FakeIamportCertificationsController } from "./controllers/FakeIamportCertificationsController"; +import { FakeIamportInternalController } from "./controllers/FakeIamportInternalController"; +import { FakeIamportPaymentsController } from "./controllers/FakeIamportPaymentsController"; +import { FakeIamportReceiptsController } from "./controllers/FakeIamportReceiptsController"; +import { FakeIamportUsersController } from "./controllers/FakeIamportUsersController"; +import { FakeIamportVbanksController } from "./controllers/FakeIamportVbanksController"; +import { FakeIamportSubscribeCustomersController } from "./controllers/subscribe/FakeIamportSubscribeCustomersController"; +import { FakeIampotSubscribePaymentsController } from "./controllers/subscribe/FakeIamportSubscribePaymentsController"; + +@Module({ + controllers: [ + FakeIamportSubscribeCustomersController, + FakeIampotSubscribePaymentsController, + FakeIamportCertificationsController, + FakeIamportInternalController, + FakeIamportPaymentsController, + FakeIamportReceiptsController, + FakeIamportUsersController, + FakeIamportVbanksController, + ], +}) +export class FakeIamportModule {} diff --git a/packages/fake-iamport-server/src/api/IamportConnector.ts b/packages/fake-iamport-server/src/api/IamportConnector.ts index 1b45151..3899592 100644 --- a/packages/fake-iamport-server/src/api/IamportConnector.ts +++ b/packages/fake-iamport-server/src/api/IamportConnector.ts @@ -24,61 +24,60 @@ import { IIamportUser } from "./structures/IIamportUser"; * @author Samchon */ export class IamportConnector { - private readonly mutex_: SharedMutex; - private token_: IIamportUser | null; + private readonly mutex_: SharedMutex; + private token_: IIamportUser | null; - /** - * Initializer Constructor - * - * @param host 아임포트 서버의 host 주소 - * @param accessor 아임포트에서 발급해 준 API 및 secret 키 - * @param surplus 만료 일시로부터의 여분 시간, 기본값은 15,000 ms - */ - public constructor( - public readonly host: string, - public readonly accessor: IIamportUser.IAccessor, - public readonly surplus: number = 15_000, - ) { - this.mutex_ = new SharedMutex(); - this.token_ = null; - } + /** + * Initializer Constructor + * + * @param host 아임포트 서버의 host 주소 + * @param accessor 아임포트에서 발급해 준 API 및 secret 키 + * @param surplus 만료 일시로부터의 여분 시간, 기본값은 15,000 ms + */ + public constructor( + public readonly host: string, + public readonly accessor: IIamportUser.IAccessor, + public readonly surplus: number = 15_000, + ) { + this.mutex_ = new SharedMutex(); + this.token_ = null; + } - /** - * 커넥션 정보 구성하기. - * - * 아임포트 API 를 호출하기 위한 {@link IConnection} 정보를 구성하여 리턴한다. 이 커넥션 - * 정보에는 아임포트의 유저 인증 토큰이 함께하는데, 만일 해당 유저 인증 토큰의 만료 일시가 - * 도래했다면, 이를 새로운 것으로 자동 갱신해준다. - * - * @returns 커넥션 정보 with 인증 토큰 - */ - public async get(): Promise { - return { - host: this.host, - headers: { - Authorization: await this.getToken(), - }, - }; - } + /** + * 커넥션 정보 구성하기. + * + * 아임포트 API 를 호출하기 위한 {@link IConnection} 정보를 구성하여 리턴한다. 이 커넥션 + * 정보에는 아임포트의 유저 인증 토큰이 함께하는데, 만일 해당 유저 인증 토큰의 만료 일시가 + * 도래했다면, 이를 새로운 것으로 자동 갱신해준다. + * + * @returns 커넥션 정보 with 인증 토큰 + */ + public async get(): Promise { + return { + host: this.host, + headers: { + Authorization: await this.getToken(), + }, + }; + } - private async getToken(): Promise { - if ( - this.token_ === null || - Date.now() >= this.token_.expired_at * 1000 - this.surplus - ) { - const locked: boolean = await UniqueLock.try_lock( - this.mutex_, - async () => { - const output: IIamportResponse = - await users.getToken( - { host: this.host }, - this.accessor, - ); - this.token_ = output.response; - }, - ); - if (locked === false) await SharedLock.lock(this.mutex_, () => {}); - } - return this.token_!.access_token; + private async getToken(): Promise { + if ( + this.token_ === null || + Date.now() >= this.token_.expired_at * 1000 - this.surplus + ) { + const locked: boolean = await UniqueLock.try_lock( + this.mutex_, + async () => { + const output: IIamportResponse = await users.getToken( + { host: this.host }, + this.accessor, + ); + this.token_ = output.response; + }, + ); + if (locked === false) await SharedLock.lock(this.mutex_, () => {}); } + return this.token_!.access_token; + } } diff --git a/packages/fake-iamport-server/src/api/functional/index.ts b/packages/fake-iamport-server/src/api/functional/index.ts index a7a4411..486bfbb 100644 --- a/packages/fake-iamport-server/src/api/functional/index.ts +++ b/packages/fake-iamport-server/src/api/functional/index.ts @@ -4,10 +4,10 @@ * @nestia Generated by Nestia - https://github.com/samchon/nestia */ //================================================================ +export * as subscribe from "./subscribe"; export * as certifications from "./certifications"; export * as internal from "./internal"; export * as payments from "./payments"; export * as receipts from "./receipts"; export * as users from "./users"; -export * as vbanks from "./vbanks"; -export * as subscribe from "./subscribe"; \ No newline at end of file +export * as vbanks from "./vbanks"; \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts index 697c7e9..b53b7c1 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts @@ -8,29 +8,29 @@ import { IIamportPayment } from "./IIamportPayment"; * @author Samchon */ export interface IIamportCardPayment - extends IIamportPayment.IBase<"card" | "samsung"> { - /** - * 카드 식별자 코드. - */ - card_code: string; + extends IIamportPayment.IBase<"card" | "samsung"> { + /** + * 카드 식별자 코드. + */ + card_code: string; - /** - * 카드 이름. - */ - card_name: string; + /** + * 카드 이름. + */ + card_name: string; - /** - * 카드 번호. - */ - card_number: string & tags.Pattern<"\\d{4}-\\d{4}-\\d{4}-\\d{4}">; + /** + * 카드 번호. + */ + card_number: string & tags.Pattern<"\\d{4}-\\d{4}-\\d{4}-\\d{4}">; - /** - * 할부 개월 수. - */ - card_quota: number & tags.Type<"uint32">; + /** + * 할부 개월 수. + */ + card_quota: number & tags.Type<"uint32">; - /** - * 카드사 승인번호. - */ - apply_num: string; + /** + * 카드사 승인번호. + */ + apply_num: string; } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts b/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts index 95755c2..884f39c 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts @@ -13,50 +13,144 @@ import { tags } from "typia"; * @author Samchon */ export interface IIamportCertification { + /** + * 아임포트가 발급해 준 식별자 번호. + */ + imp_uid: string; + + /** + * 서비스로부터의 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. + */ + merchant_uid: null | string; + + /** + * 본인인증대상자 성명. + */ + name: string; + + /** + * 성별. + */ + gender: string; + + /** + * 생년월일. + * + * 리눅스 타임이 쓰인다. + */ + birth: number; + + /** + * 생년월일, YYYYMMDD 형식. + */ + birthday: string & + tags.Pattern<"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; + + /** + * 외국인 여부. + */ + foreigner: boolean; + + /** + * 본인인증 대상자 핸드폰 번호. + */ + phone: string; + + /** + * 본인인증 대상자 통신사 코드. + */ + carrier: "SKT" | "KT" | "LGT"; + + /** + * OTP 인증 여부. + */ + certified: boolean; + + /** + * OTP 인증 일시. + * + * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + */ + certified_at: number; + + /** + * 뭔지 잘 모름, 용도 아시는 분? + */ + unique_key: string; + + /** + * 뭔지 잘 모름, 용도 아시는 분? + */ + unique_in_site: string; + + /** + * 뭔지 잘 모름, 용도 아시는 분? + */ + pg_tid: string; + + /** + * PG 제공자. + */ + pg_provider: string; + + /** + * 뭔지 잘 모름, 용도 아시는 분? + */ + origin: string; + + /** + * (테스트 전용) OTP 코드. + * + * 오직 `fake-iamport-server` 에서만 쓰이는 속성으로써, 본인인증을 시뮬레이션할 때, + * 어떠한 OTP 코드가 발급되었는 지를 확인하기 위하여 사용된다. 이를 이용하여 + * {@link functional.certifications.otp.confirm} 함수를 호출하면, 본인인증을 완료할 + * 수 있다. + */ + __otp?: string; +} +export namespace IIamportCertification { + /** + * 본인인증 정보의 접근자 구조체. + */ + export interface IAccessor { /** - * 아임포트가 발급해 준 식별자 번호. + * 본인인증정보의 식별자 키. */ imp_uid: string; + } - /** - * 서비스로부터의 식별자 키. - * - * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. - */ - merchant_uid: null | string; - + /** + * 본인 인증 입력 정보. + */ + export interface IStore { /** * 본인인증대상자 성명. */ name: string; /** - * 성별. + * 본인인증 대상자 핸드폰 번호. + * + * 핸드폰 번호에 "-" 값이 들어가던 아니던 상관 없음. + * + * 다만, 내부적으로는 "-" 값을 제거하여 처리한다. */ - gender: string; + phone: string; /** * 생년월일. * - * 리눅스 타임이 쓰인다. + * YYYYMMDD 형식. */ - birth: number; + birth: string & + tags.Pattern<"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; /** - * 생년월일, YYYYMMDD 형식. + * 주민등록 뒷부분 첫 자리. */ - birthday: string & - tags.Pattern<"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; - - /** - * 외국인 여부. - */ - foreigner: boolean; - - /** - * 본인인증 대상자 핸드폰 번호. - */ - phone: string; + gender_digit: string; /** * 본인인증 대상자 통신사 코드. @@ -64,135 +158,41 @@ export interface IIamportCertification { carrier: "SKT" | "KT" | "LGT"; /** - * OTP 인증 여부. + * 알뜰폰 여부. */ - certified: boolean; + is_mvno?: boolean; /** - * OTP 인증 일시. + * 가맹점 서비스 명칭 또는 domain URL. * - * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + * KISA 에서 대상자에게 발송하는 SMS에 안내될 서비스 명칭. */ - certified_at: number; + commpany?: string; /** - * 뭔지 잘 모름, 용도 아시는 분? - */ - unique_key: string; - - /** - * 뭔지 잘 모름, 용도 아시는 분? - */ - unique_in_site: string; - - /** - * 뭔지 잘 모름, 용도 아시는 분? - */ - pg_tid: string; - - /** - * PG 제공자. - */ - pg_provider: string; - - /** - * 뭔지 잘 모름, 용도 아시는 분? - */ - origin: string; - - /** - * (테스트 전용) OTP 코드. + * 귀사 서비스에서의 본인인증 식별자 키. * - * 오직 `fake-iamport-server` 에서만 쓰이는 속성으로써, 본인인증을 시뮬레이션할 때, - * 어떠한 OTP 코드가 발급되었는 지를 확인하기 위하여 사용된다. 이를 이용하여 - * {@link functional.certifications.otp.confirm} 함수를 호출하면, 본인인증을 완료할 - * 수 있다. - */ - __otp?: string; -} -export namespace IIamportCertification { - /** - * 본인인증 정보의 접근자 구조체. + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. */ - export interface IAccessor { - /** - * 본인인증정보의 식별자 키. - */ - imp_uid: string; - } + merchant_uid?: string; /** - * 본인 인증 입력 정보. + * PG 사 구분자. + * + * 다날 상점아이디를 2개 이상 동시에 사용하시려는 경우에 설정하면 된다. + * + * **danal.{상점아이디}** 형태로 지정. */ - export interface IStore { - /** - * 본인인증대상자 성명. - */ - name: string; - - /** - * 본인인증 대상자 핸드폰 번호. - * - * 핸드폰 번호에 "-" 값이 들어가던 아니던 상관 없음. - * - * 다만, 내부적으로는 "-" 값을 제거하여 처리한다. - */ - phone: string; - - /** - * 생년월일. - * - * YYYYMMDD 형식. - */ - birth: string & - tags.Pattern<"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; - - /** - * 주민등록 뒷부분 첫 자리. - */ - gender_digit: string; - - /** - * 본인인증 대상자 통신사 코드. - */ - carrier: "SKT" | "KT" | "LGT"; - - /** - * 알뜰폰 여부. - */ - is_mvno?: boolean; - - /** - * 가맹점 서비스 명칭 또는 domain URL. - * - * KISA 에서 대상자에게 발송하는 SMS에 안내될 서비스 명칭. - */ - commpany?: string; - - /** - * 귀사 서비스에서의 본인인증 식별자 키. - * - * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. - */ - merchant_uid?: string; - - /** - * PG 사 구분자. - * - * 다날 상점아이디를 2개 이상 동시에 사용하시려는 경우에 설정하면 된다. - * - * **danal.{상점아이디}** 형태로 지정. - */ - pg?: string; - } + pg?: string; + } + /** + * 본인인증 승인을 위한 입력 정보. + */ + export interface IConfirm { /** - * 본인인증 승인을 위한 입력 정보. + * SMS 로 전송된 본인인증 번호. */ - export interface IConfirm { - /** - * SMS 로 전송된 본인인증 번호. - */ - otp: string; - } + otp: string; + } } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts index c0799b9..1925214 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts @@ -20,192 +20,189 @@ import { IIamportVBankPayment } from "./IIamportVBankPayment"; * @author Samchon */ export type IIamportPayment = - | IIamportCardPayment - | IIamportTransferPayment - | IIamportVBankPayment - | IIamportPayment.IBase< - Exclude< - IIamportPayment.PayMethod, - "card" | "samsung" | "trans" | "vbank" - > - >; + | IIamportCardPayment + | IIamportTransferPayment + | IIamportVBankPayment + | IIamportPayment.IBase< + Exclude + >; export namespace IIamportPayment { + /** + * 결제 수단이 페이팔인 경우, 페이팔의 구매자 보호정책에 의해 결제 승인 시점에 + * Pending 상태를 만든 후, 내부 심사등을 통해 최종 결제 완료라고 변경함. + * + * `iamport` 의 기술적 이슈로 해당 상태를 status: failed 로 기록함. 추후 + * 페이팔에서 최종결제완료로 변경된 경우, `iamport` 에서 `paid` 로 변경 후, + * 해당건에 대한 웹훅 발송. `iamport` 를 사용하는 고객사에서는, failed 로 이미 + * 처리된 결제건에 대한 paid 상태의 웹훅을 받는 문제점이 생김. + * + * 이에, `iamport` 에서 제공하는 `/payment/{imp_uid}` 에 query-string 으로 + * `extension=true` 옵션을 추가해야 함 + * + * @issue https://github.com/samchon/fake-iamport-server/issues/13 + * @author Sangjin Han - https://github.com/ltnscp9028 + */ + export interface IQuery { /** - * 결제 수단이 페이팔인 경우, 페이팔의 구매자 보호정책에 의해 결제 승인 시점에 - * Pending 상태를 만든 후, 내부 심사등을 통해 최종 결제 완료라고 변경함. + * 페이팔의 경우, 이 값을 `true` 로 할 것. + */ + extension?: boolean; + } + + /** + * 웹훅 데이터. + */ + export interface IWebhook { + /** + * 결제 정보 {@link IIamportPayment} 의 식별자 키. + */ + imp_uid: string; + + /** + * 주문 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. + */ + merchant_uid: string; + + /** + * 현재 상태. + */ + status: Status; + } + + /** + * 결제 기본 (공통) 정보. + */ + export interface IBase { + // IDENTIFIER + pay_method: Method; + + /** + * 결제 정보 {@link IIamportPayment} 의 식별자 키. + */ + imp_uid: string; + + // ORDER INFO + /** + * 주문 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. + */ + merchant_uid: string; + + /** + * 주문명, 누락 가능. + */ + name: null | string; + + /** + * 결제 총액. + */ + amount: number; + + /** + * 결제 취소, 환불 총액. + */ + cancel_amount: number; + + /** + * 통화 단위. + */ + currency: IIamportPayment.Currency; + + /** + * 영수증 URL. + */ + receipt_url: string & tags.Format<"url">; + + /** + * 현금 영수증 발행 여부. + */ + cash_receipt_issue: boolean; + + // PAYMENT PRVIDER INFO + channel: string; + pg_provider: string; + emb_pg_provider: null | string; + pg_id: string; + pg_tid: string; + escrow: boolean; + + // BUYER + buyer_name: null | string; + buyer_email: null | (string & tags.Format<"email">); + buyer_tel: null | string; + buyer_addr: null | string; + buyer_postcode: null | string; + customer_uid: null | string; + customer_uid_usage: null | string; + custom_data: null | string; + user_agent: null | string; + + // PROPERTIES + /** + * 결제의 현재 (진행) 상태. + */ + status: IIamportPayment.Status; + + /** + * 결제 신청 일시. + * + * 리눅스 타임이 쓰임. + */ + started_at: number; + + /** + * 결제 (지불) 완료 일시. * - * `iamport` 의 기술적 이슈로 해당 상태를 status: failed 로 기록함. 추후 - * 페이팔에서 최종결제완료로 변경된 경우, `iamport` 에서 `paid` 로 변경 후, - * 해당건에 대한 웹훅 발송. `iamport` 를 사용하는 고객사에서는, failed 로 이미 - * 처리된 결제건에 대한 paid 상태의 웹훅을 받는 문제점이 생김. + * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + */ + paid_at: number; + + /** + * 결제 실패 일시. * - * 이에, `iamport` 에서 제공하는 `/payment/{imp_uid}` 에 query-string 으로 - * `extension=true` 옵션을 추가해야 함 + * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + */ + failed_at: number; + + /** + * 결제 취소 일시. * - * @issue https://github.com/samchon/fake-iamport-server/issues/13 - * @author Sangjin Han - https://github.com/ltnscp9028 - */ - export interface IQuery { - /** - * 페이팔의 경우, 이 값을 `true` 로 할 것. - */ - extension?: boolean; - } - - /** - * 웹훅 데이터. - */ - export interface IWebhook { - /** - * 결제 정보 {@link IIamportPayment} 의 식별자 키. - */ - imp_uid: string; - - /** - * 주문 식별자 키. - * - * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. - */ - merchant_uid: string; - - /** - * 현재 상태. - */ - status: Status; - } - - /** - * 결제 기본 (공통) 정보. - */ - export interface IBase { - // IDENTIFIER - pay_method: Method; - - /** - * 결제 정보 {@link IIamportPayment} 의 식별자 키. - */ - imp_uid: string; - - // ORDER INFO - /** - * 주문 식별자 키. - * - * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. - */ - merchant_uid: string; - - /** - * 주문명, 누락 가능. - */ - name: null | string; - - /** - * 결제 총액. - */ - amount: number; - - /** - * 결제 취소, 환불 총액. - */ - cancel_amount: number; - - /** - * 통화 단위. - */ - currency: IIamportPayment.Currency; - - /** - * 영수증 URL. - */ - receipt_url: string & tags.Format<"url">; - - /** - * 현금 영수증 발행 여부. - */ - cash_receipt_issue: boolean; - - // PAYMENT PRVIDER INFO - channel: string; - pg_provider: string; - emb_pg_provider: null | string; - pg_id: string; - pg_tid: string; - escrow: boolean; - - // BUYER - buyer_name: null | string; - buyer_email: null | (string & tags.Format<"email">); - buyer_tel: null | string; - buyer_addr: null | string; - buyer_postcode: null | string; - customer_uid: null | string; - customer_uid_usage: null | string; - custom_data: null | string; - user_agent: null | string; - - // PROPERTIES - /** - * 결제의 현재 (진행) 상태. - */ - status: IIamportPayment.Status; - - /** - * 결제 신청 일시. - * - * 리눅스 타임이 쓰임. - */ - started_at: number; - - /** - * 결제 (지불) 완료 일시. - * - * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. - */ - paid_at: number; - - /** - * 결제 실패 일시. - * - * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. - */ - failed_at: number; - - /** - * 결제 취소 일시. - * - * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. - */ - cancelled_at: number; - - // CANCELLATIONS - fail_reason: null | string; - cancel_reason: null | string; - cancel_history: IIamportPaymentCancel[]; - - /** - * @internal - */ - notice_url?: string & tags.Format<"url">; - } - - export type PayMethod = - | "card" - | "trans" - | "vbank" - | "phone" - | "samsung" - | "kpay" - | "kakaopay" - | "payco" - | "lpay" - | "ssgpay" - | "tosspay" - | "cultureland" - | "smartculture" - | "happymoney" - | "booknlife" - | "point"; - export type Status = "paid" | "ready" | "failed" | "cancelled"; - export type Currency = "KRW" | "USD" | "EUR" | "JPY"; + * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + */ + cancelled_at: number; + + // CANCELLATIONS + fail_reason: null | string; + cancel_reason: null | string; + cancel_history: IIamportPaymentCancel[]; + + /** + * @internal + */ + notice_url?: string & tags.Format<"url">; + } + + export type PayMethod = + | "card" + | "trans" + | "vbank" + | "phone" + | "samsung" + | "kpay" + | "kakaopay" + | "payco" + | "lpay" + | "ssgpay" + | "tosspay" + | "cultureland" + | "smartculture" + | "happymoney" + | "booknlife" + | "point"; + export type Status = "paid" | "ready" | "failed" | "cancelled"; + export type Currency = "KRW" | "USD" | "EUR" | "JPY"; } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts b/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts index 579ad62..0a7d28e 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts @@ -6,77 +6,77 @@ import { tags } from "typia"; * @author Samchon */ export interface IIamportPaymentCancel { - pg_id: string; - pg_tid: string; - amount: number; - cancelled_at: number; - reason: string; - receipt_url: string & tags.Format<"url">; + pg_id: string; + pg_tid: string; + amount: number; + cancelled_at: number; + reason: string; + receipt_url: string & tags.Format<"url">; } export namespace IIamportPaymentCancel { + /** + * 결제 취소 입력 정보. + */ + export interface IStore { /** - * 결제 취소 입력 정보. + * 결제 정보 {@link IIamportPayment} 의 식별자 키. */ - export interface IStore { - /** - * 결제 정보 {@link IIamportPayment} 의 식별자 키. - */ - imp_uid: string; + imp_uid: string; - /** - * 주문 식별자 키. - * - * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. - */ - merchant_uid: string; + /** + * 주문 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. + */ + merchant_uid: string; - /** - * 취소 금액, 부분 취소도 가능하다. - * - * 누락시 전액 취소. - */ - amount?: number; + /** + * 취소 금액, 부분 취소도 가능하다. + * + * 누락시 전액 취소. + */ + amount?: number; - /** - * 취소 트랜잭션 수행 전, 현재 시점의 취소 가능한 잔액. - * - * API요청자가 기록하고 있는 취소가능 잔액과 아임포트가 기록하고 있는 취소가능 잔액이 - * 일치하는지 사전에 검증하고, 검증에 실패하면 트랜잭션을 수행하지 않는다. - * - * `null` 인 경우에는 검증 프로세스를 생략. - */ - checksum: null | (number & tags.Minimum<0>); + /** + * 취소 트랜잭션 수행 전, 현재 시점의 취소 가능한 잔액. + * + * API요청자가 기록하고 있는 취소가능 잔액과 아임포트가 기록하고 있는 취소가능 잔액이 + * 일치하는지 사전에 검증하고, 검증에 실패하면 트랜잭션을 수행하지 않는다. + * + * `null` 인 경우에는 검증 프로세스를 생략. + */ + checksum: null | (number & tags.Minimum<0>); - /** - * 취소 사유. - */ - reason: string; + /** + * 취소 사유. + */ + reason: string; - /** - * 취소요청금액 중 면세금액. - * - * @default 0 - */ - tax_free?: number; + /** + * 취소요청금액 중 면세금액. + * + * @default 0 + */ + tax_free?: number; - /** - * 환불계좌 예금주. - */ - refund_holder?: string; + /** + * 환불계좌 예금주. + */ + refund_holder?: string; - /** - * 환불계좌 은행 코드. - */ - refund_bank?: string; + /** + * 환불계좌 은행 코드. + */ + refund_bank?: string; - /** - * 환불계좌 계좌번호. - */ - refund_account?: string; + /** + * 환불계좌 계좌번호. + */ + refund_account?: string; - /** - * 환불계좌 예금주 연락처 - */ - refund_tel?: string; - } + /** + * 환불계좌 예금주 연락처 + */ + refund_tel?: string; + } } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportReceipt.ts b/packages/fake-iamport-server/src/api/structures/IIamportReceipt.ts index cb4cb2a..22bdfca 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportReceipt.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportReceipt.ts @@ -6,130 +6,130 @@ import { tags } from "typia"; * @author Samchon */ export interface IIamportReceipt { + /** + * 귀속 결제의 {@link IIamportPayment.imp_uid}. + */ + imp_uid: string; + + /** + * 현금 영수증의 고유 식별자 ID. + */ + receipt_uid: string; + + /** + * 승인 번호. + */ + apply_num: string; + + /** + * 발행 타입 (대상). + */ + type: IIamportReceipt.Type; + + /** + * 결제 총액. + */ + amount: number; + + /** + * 부가세. + */ + vat: number; + + /** + * 현금영수증 조회 URL. + */ + receipt_url: string & tags.Format<"url">; + + /** + * 현금영수증 발행 시간. + */ + applied_at: number; + + /** + * 현금영수증 취소 시간. + * + * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + */ + cancelled_at: number; +} + +export namespace IIamportReceipt { + /** + * 현금영수증 발행대상 유형. + * + * - person: 주민등록번호 + * - business: 사업자등록번호 + * - phone: 휴대폰번호 + * - taxcard: 국세청현금영수증카드 + */ + export type IdentifierType = "person" | "business" | "phone" | "taxcard"; + + /** + * 현금영수증 발행 타입 (대상). + */ + export type Type = "person" | "company"; + + /** + * 현금영수증 입력 정보. + */ + export interface IStore { /** * 귀속 결제의 {@link IIamportPayment.imp_uid}. */ imp_uid: string; /** - * 현금 영수증의 고유 식별자 ID. - */ - receipt_uid: string; - - /** - * 승인 번호. - */ - apply_num: string; - - /** - * 발행 타입 (대상). - */ - type: IIamportReceipt.Type; - - /** - * 결제 총액. - */ - amount: number; - - /** - * 부가세. + * 현금영수증 발생대상 식별정보. + * + * - 국세청현금영수증카드 + * - 휴대폰번호 + * - 주민등록번호 + * - 사업자등록번호 */ - vat: number; + identifier: string; /** - * 현금영수증 조회 URL. + * 현금영수증 발행대상 유형. + * + * - person: 주민등록번호 + * - business: 사업자등록번호 + * - phone: 휴대폰번호 + * - taxcard: 국세청현금영수증카드 + * + * 일부 PG 사의 경우 이 항목이 없어 된다는데, 어지간하면 그냥 쓰기 바람. */ - receipt_url: string & tags.Format<"url">; + identifier_type?: IdentifierType; /** - * 현금영수증 발행 시간. + * 현금영수증 발행 타입 (대상). + * + * 누락시 person 이 사용됨. */ - applied_at: number; + type?: Type; /** - * 현금영수증 취소 시간. + * 구매자 이름. * - * 리눅스 타임이 쓰이며, `null` 대신 0 을 씀. + * 형금영수증 발행건 사후 추적을 위해 가급 입력하기 바람. */ - cancelled_at: number; -} + buyer_name?: string; -export namespace IIamportReceipt { /** - * 현금영수증 발행대상 유형. - * - * - person: 주민등록번호 - * - business: 사업자등록번호 - * - phone: 휴대폰번호 - * - taxcard: 국세청현금영수증카드 + * 구매자 이메일. */ - export type IdentifierType = "person" | "business" | "phone" | "taxcard"; + buyer_email?: string; /** - * 현금영수증 발행 타입 (대상). + * 구매자 전화번호. + * + * 현금영수증 발행건 사후 추적을 위해 가급 입력하기 바람. */ - export type Type = "person" | "company"; + buyer_tel?: string; /** - * 현금영수증 입력 정보. + * 면세 금액. */ - export interface IStore { - /** - * 귀속 결제의 {@link IIamportPayment.imp_uid}. - */ - imp_uid: string; - - /** - * 현금영수증 발생대상 식별정보. - * - * - 국세청현금영수증카드 - * - 휴대폰번호 - * - 주민등록번호 - * - 사업자등록번호 - */ - identifier: string; - - /** - * 현금영수증 발행대상 유형. - * - * - person: 주민등록번호 - * - business: 사업자등록번호 - * - phone: 휴대폰번호 - * - taxcard: 국세청현금영수증카드 - * - * 일부 PG 사의 경우 이 항목이 없어 된다는데, 어지간하면 그냥 쓰기 바람. - */ - identifier_type?: IdentifierType; - - /** - * 현금영수증 발행 타입 (대상). - * - * 누락시 person 이 사용됨. - */ - type?: Type; - - /** - * 구매자 이름. - * - * 형금영수증 발행건 사후 추적을 위해 가급 입력하기 바람. - */ - buyer_name?: string; - - /** - * 구매자 이메일. - */ - buyer_email?: string; - - /** - * 구매자 전화번호. - * - * 현금영수증 발행건 사후 추적을 위해 가급 입력하기 바람. - */ - buyer_tel?: string; - - /** - * 면세 금액. - */ - tax_free?: number; - } + tax_free?: number; + } } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts b/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts index ae1c39b..5c117af 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts @@ -4,20 +4,20 @@ * @author Samchon */ export interface IIamportResponse { - /** - * 에러 코드. - * - * 값이 0 이면 오류가 없다는 뜻. - */ - code: number; + /** + * 에러 코드. + * + * 값이 0 이면 오류가 없다는 뜻. + */ + code: number; - /** - * 성공 또는 오류 메시지. - */ - message: string; + /** + * 성공 또는 오류 메시지. + */ + message: string; - /** - * 응답 데이터, 사실상 본문. - */ - response: T; + /** + * 응답 데이터, 사실상 본문. + */ + response: T; } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts b/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts index 238ea3c..7c44870 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts @@ -8,162 +8,162 @@ import { IIamportPayment } from "./IIamportPayment"; * @author Samchon */ export interface IIamportSubscription extends IIamportSubscription.IAccessor { - pg_provider: string; - pg_id: string; - card_name: string; - card_code: string; - card_number: string; - card_type: string; - customer_name: null | string; - customer_tel: null | string; - customer_email: null | string; - customer_addr: null | string; - customer_postcode: null | string; - inserted: number; - updated: number; + pg_provider: string; + pg_id: string; + card_name: string; + card_code: string; + card_number: string; + card_type: string; + customer_name: null | string; + customer_tel: null | string; + customer_email: null | string; + customer_addr: null | string; + customer_postcode: null | string; + inserted: number; + updated: number; } export namespace IIamportSubscription { + /** + * {@link IIamportSubscription} 의 접근자 정보. + */ + export interface IAccessor { /** - * {@link IIamportSubscription} 의 접근자 정보. - */ - export interface IAccessor { - /** - * 고객 식별자 키. - * - * 아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - * - * 다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함. - */ - customer_uid: string; - } - - /** - * 간편 결제 카드 입력 정보. - */ - export interface IStore extends IAccessor { - /** - * 카드 번호. - * - * 형식: XXXX-XXXX-XXXX-XXXX - */ - card_number: string & tags.Pattern<"\\d{4}-\\d{4}-\\d{4}-\\d{4}">; - - /** - * 카드 유효기간. - * - * 형식: YYYY-MM - */ - expiry: string & tags.Pattern<"^([0-9]{4})-(0[1-9]|1[012])$">; - - /** - * 생년월일 YYMMDD 또는 사업자등록번호 10자리. - */ - birth: string & - tags.Pattern<"^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$">; - - /** - * 카드 비밀번호 앞 두 자리. - */ - pwd_2digit?: string & tags.Pattern<"\\d{2}">; - - /** - * 카드 인증번호 (카드 뒷면 3 자리). - */ - cvc?: string & tags.Pattern<"\\d{2}">; - - customer_name?: string; - customer_tel?: string; - customer_email?: string & tags.Format<"email">; - customr_addr?: string; - customer_postcode?: string; - } - - /** - * 결제 신청 입력 정보. - */ - export interface IOnetime - extends Omit, - Omit { - /** - * 고객 식별자 키. - * - * 아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - * - * 다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함. - * - * 이를 생략시 단순 결제로만 그치며, 카드 정보가 간편 결제용으로 등록되지 아니함. - */ - customer_uid?: string; - } - - /** - * 간편 결제 카드로 결제 신청 입력 정보. - */ - export interface IAgain extends IAccessor { - /** - * 주문 식별자 키. - * - * 아임포트가 아닌 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - merchant_uid: string; - - /** - * 결제 총액. - */ - amount: number; - - /** - * 주문 이름. - */ - name: string; - - /** - * 통화 정보. - */ - currency?: IIamportPayment.Currency; - - /** - * 면세 공급가액. - * - * 기본값은 0 로써, 알아서 amount 의 1/11 로써 부가세 처리됨. - */ - tax_free?: number; - - /** - * 할부 개월 수. - * - * 일시불은 0. - */ - card_quota?: number; - - buyer_name?: string; - buyer_email?: string & tags.Format<"email">; - buyer_tel?: string; - buyer_addr?: string; - buyer_postcode?: string; - - /** - * 카드할부처리할 때, 할부이자가 발생하는 경우 (카드사 무이자 프로모션 제외). - * - * 부과되는 할부이자를 고객대신 가맹점이 지불하고자 PG사와 계약된 경우(현재, 나이스페이먼츠만 지원됨) - */ - interest_free_by_merchant?: boolean; - - /** - * 승인요청시 카드사 포인트 차감하며 결제승인처리할지 flag. - * - * PG사 영업담당자와 계약 당시 사전 협의 필요(현재, 나이스페이먼츠만 지원됨) - */ - use_card_point?: boolean; - - /** - * 임의 정보를 기재할 수 있다. - */ - custom_data?: string; - - /** - * 결제 성공시 통지될 Notification, 웹훅 URL. - */ - notice_url?: string & tags.Format<"url">; - } + * 고객 식별자 키. + * + * 아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + * + * 다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함. + */ + customer_uid: string; + } + + /** + * 간편 결제 카드 입력 정보. + */ + export interface IStore extends IAccessor { + /** + * 카드 번호. + * + * 형식: XXXX-XXXX-XXXX-XXXX + */ + card_number: string & tags.Pattern<"\\d{4}-\\d{4}-\\d{4}-\\d{4}">; + + /** + * 카드 유효기간. + * + * 형식: YYYY-MM + */ + expiry: string & tags.Pattern<"^([0-9]{4})-(0[1-9]|1[012])$">; + + /** + * 생년월일 YYMMDD 또는 사업자등록번호 10자리. + */ + birth: string & + tags.Pattern<"^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$">; + + /** + * 카드 비밀번호 앞 두 자리. + */ + pwd_2digit?: string & tags.Pattern<"\\d{2}">; + + /** + * 카드 인증번호 (카드 뒷면 3 자리). + */ + cvc?: string & tags.Pattern<"\\d{2}">; + + customer_name?: string; + customer_tel?: string; + customer_email?: string & tags.Format<"email">; + customr_addr?: string; + customer_postcode?: string; + } + + /** + * 결제 신청 입력 정보. + */ + export interface IOnetime + extends Omit, + Omit { + /** + * 고객 식별자 키. + * + * 아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + * + * 다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함. + * + * 이를 생략시 단순 결제로만 그치며, 카드 정보가 간편 결제용으로 등록되지 아니함. + */ + customer_uid?: string; + } + + /** + * 간편 결제 카드로 결제 신청 입력 정보. + */ + export interface IAgain extends IAccessor { + /** + * 주문 식별자 키. + * + * 아임포트가 아닌 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + */ + merchant_uid: string; + + /** + * 결제 총액. + */ + amount: number; + + /** + * 주문 이름. + */ + name: string; + + /** + * 통화 정보. + */ + currency?: IIamportPayment.Currency; + + /** + * 면세 공급가액. + * + * 기본값은 0 로써, 알아서 amount 의 1/11 로써 부가세 처리됨. + */ + tax_free?: number; + + /** + * 할부 개월 수. + * + * 일시불은 0. + */ + card_quota?: number; + + buyer_name?: string; + buyer_email?: string & tags.Format<"email">; + buyer_tel?: string; + buyer_addr?: string; + buyer_postcode?: string; + + /** + * 카드할부처리할 때, 할부이자가 발생하는 경우 (카드사 무이자 프로모션 제외). + * + * 부과되는 할부이자를 고객대신 가맹점이 지불하고자 PG사와 계약된 경우(현재, 나이스페이먼츠만 지원됨) + */ + interest_free_by_merchant?: boolean; + + /** + * 승인요청시 카드사 포인트 차감하며 결제승인처리할지 flag. + * + * PG사 영업담당자와 계약 당시 사전 협의 필요(현재, 나이스페이먼츠만 지원됨) + */ + use_card_point?: boolean; + + /** + * 임의 정보를 기재할 수 있다. + */ + custom_data?: string; + + /** + * 결제 성공시 통지될 Notification, 웹훅 URL. + */ + notice_url?: string & tags.Format<"url">; + } } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts index 06a58ed..a46d417 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts @@ -6,14 +6,14 @@ import { IIamportPayment } from "./IIamportPayment"; * @author Samchon */ export interface IIamportTransferPayment - extends IIamportPayment.IBase<"trans"> { - /** - * 은행 식별자 코드. - */ - bank_code: string; + extends IIamportPayment.IBase<"trans"> { + /** + * 은행 식별자 코드. + */ + bank_code: string; - /** - * 은행 이름. - */ - bank_name: string; + /** + * 은행 이름. + */ + bank_name: string; } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportUser.ts b/packages/fake-iamport-server/src/api/structures/IIamportUser.ts index f9bf1c4..1c810f8 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportUser.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportUser.ts @@ -15,40 +15,40 @@ * @author Samchon */ export interface IIamportUser { - /** - * 토큰 발행 시간. - */ - now: number; + /** + * 토큰 발행 시간. + */ + now: number; - /** - * 토큰 만료 시간. - * - * 리눅스 타임이 기준이며, 이를 JS 에서 사용하려거든, 아래와 같이 변환해야 한다. - * - * ```typescript - * new Date(user.expired_at * 1_000); - * ``` - */ - expired_at: number; + /** + * 토큰 만료 시간. + * + * 리눅스 타임이 기준이며, 이를 JS 에서 사용하려거든, 아래와 같이 변환해야 한다. + * + * ```typescript + * new Date(user.expired_at * 1_000); + * ``` + */ + expired_at: number; - /** - * 유저 인증 토큰. - */ - access_token: string; + /** + * 유저 인증 토큰. + */ + access_token: string; } export namespace IIamportUser { + /** + * 아임포트에서 부여해 준 API 및 secret 키. + */ + export interface IAccessor { /** - * 아임포트에서 부여해 준 API 및 secret 키. + * API 키. */ - export interface IAccessor { - /** - * API 키. - */ - imp_key: string; + imp_key: string; - /** - * Secret 키. - */ - imp_secret: string; - } + /** + * Secret 키. + */ + imp_secret: string; + } } diff --git a/packages/fake-iamport-server/src/api/structures/IIamportVBankPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportVBankPayment.ts index cee0269..494b00b 100644 --- a/packages/fake-iamport-server/src/api/structures/IIamportVBankPayment.ts +++ b/packages/fake-iamport-server/src/api/structures/IIamportVBankPayment.ts @@ -6,123 +6,123 @@ import { IIamportPayment } from "./IIamportPayment"; * @author Samchon */ export interface IIamportVBankPayment extends IIamportPayment.IBase<"vbank"> { + /** + * 가상 계좌 식별자 코드. + */ + vbank_code: string; + + /** + * 가상 게좌 이름 + */ + vbank_name: string; + + /** + * 가상 계좌 번호 + */ + vbank_num: string; + + /** + * 가상 계좌 예금주. + */ + vbank_holder: string; + + /** + * 가상 계좌 입금 만료 기한. + */ + vbank_date: number; + + /** + * 가상 계좌 개설 일시. + */ + vbank_issued_at: number; +} +export namespace IIamportVBankPayment { + /** + * 가상 계좌 결제 입력 정보. + * + * 가상 계좌를 임의 생성할 수 있다. + * + * 단, 일부 PG 사 혹은 `fake-iamport-server` 만 가능. + * + * - 세틀뱅크 + * - 나이스페이먼츠 + * - KG이니시스 + */ + export interface IStore { /** - * 가상 계좌 식별자 코드. + * 주문 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. */ - vbank_code: string; + merchant_uid: string; + + /** + * 총액. + */ + amount: number; /** - * 가상 게좌 이름 + * 가상계좌 은행 코드. */ - vbank_name: string; + vbank_code: string; /** - * 가상 계좌 번호 + * 가상계좌 입금기한, 유닉스 타임. */ - vbank_num: string; + vbank_due: number; /** - * 가상 계좌 예금주. + * 예금주. */ vbank_holder: string; + name?: string; + buyer_name?: string; + buyer_email?: string; + buyer_tel?: string; + buyer_addr?: string; + buyer_postcode?: string; + pg?: string; + /** - * 가상 계좌 입금 만료 기한. + * 가상 계좌 입금 정보를 수신할 URL. + * + * 누락시 기본 웹훅 URL 사용. */ - vbank_date: number; + notice_url?: string; /** - * 가상 계좌 개설 일시. + * 커스텀 데이터, 자유롭게 사용 가능. */ - vbank_issued_at: number; -} -export namespace IIamportVBankPayment { + custom_data?: string; + /** - * 가상 계좌 결제 입력 정보. - * - * 가상 계좌를 임의 생성할 수 있다. - * - * 단, 일부 PG 사 혹은 `fake-iamport-server` 만 가능. - * - * - 세틀뱅크 - * - 나이스페이먼츠 - * - KG이니시스 + * [이니시스 전용] 가맹점 콘솔에서 확인한 API 값. + */ + pg_api_key?: string; + } + + /** + * 가상 계좌 결제의 수정 입력 정보. + * + * 아직 입금되지 않은 가상계좌의 입금기한 또는 입금금액을 수정할 수 있다. + * + * 다만, 세틀뱅크 혹은 `fake-iamport-server` 만 가능. + */ + export interface IUpdate { + /** + * 대상 결제 기록의 {@link IIamportPayment.imp_uid}. */ - export interface IStore { - /** - * 주문 식별자 키. - * - * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. - */ - merchant_uid: string; - - /** - * 총액. - */ - amount: number; - - /** - * 가상계좌 은행 코드. - */ - vbank_code: string; - - /** - * 가상계좌 입금기한, 유닉스 타임. - */ - vbank_due: number; - - /** - * 예금주. - */ - vbank_holder: string; - - name?: string; - buyer_name?: string; - buyer_email?: string; - buyer_tel?: string; - buyer_addr?: string; - buyer_postcode?: string; - pg?: string; - - /** - * 가상 계좌 입금 정보를 수신할 URL. - * - * 누락시 기본 웹훅 URL 사용. - */ - notice_url?: string; - - /** - * 커스텀 데이터, 자유롭게 사용 가능. - */ - custom_data?: string; - - /** - * [이니시스 전용] 가맹점 콘솔에서 확인한 API 값. - */ - pg_api_key?: string; - } + imp_uid: string; /** - * 가상 계좌 결제의 수정 입력 정보. - * - * 아직 입금되지 않은 가상계좌의 입금기한 또는 입금금액을 수정할 수 있다. - * - * 다만, 세틀뱅크 혹은 `fake-iamport-server` 만 가능. + * 수정할 결제 금액. + */ + amount?: number; + + /** + * 수정할 가상계좌 입금 기한. */ - export interface IUpdate { - /** - * 대상 결제 기록의 {@link IIamportPayment.imp_uid}. - */ - imp_uid: string; - - /** - * 수정할 결제 금액. - */ - amount?: number; - - /** - * 수정할 가상계좌 입금 기한. - */ - vbank_due?: number; - } + vbank_due?: number; + } } diff --git a/packages/fake-iamport-server/src/api/typings/Atomic.ts b/packages/fake-iamport-server/src/api/typings/Atomic.ts index 61a6dee..4454c7a 100644 --- a/packages/fake-iamport-server/src/api/typings/Atomic.ts +++ b/packages/fake-iamport-server/src/api/typings/Atomic.ts @@ -10,5 +10,5 @@ * @author Samchon */ export type Atomic = { - [P in keyof Instance]: Instance[P] extends object ? never : Instance[P]; + [P in keyof Instance]: Instance[P] extends object ? never : Instance[P]; }; diff --git a/packages/fake-iamport-server/src/api/typings/Writable.ts b/packages/fake-iamport-server/src/api/typings/Writable.ts index 0ed6231..2b24a09 100644 --- a/packages/fake-iamport-server/src/api/typings/Writable.ts +++ b/packages/fake-iamport-server/src/api/typings/Writable.ts @@ -4,9 +4,9 @@ */ //================================================================ export type Writable = { - -readonly [P in keyof T]: T[P]; + -readonly [P in keyof T]: T[P]; }; export function Writable(elem: Readonly): Writable { - return elem; + return elem; } diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts index cfb7b14..9a44c17 100644 --- a/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts +++ b/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts @@ -1,163 +1,159 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { + Controller, + ForbiddenException, + UnprocessableEntityException, +} from "@nestjs/common"; import { IIamportCertification } from "iamport-server-api/lib/structures/IIamportCertification"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; import { randint } from "tstl"; import { v4 } from "uuid"; +import { FakeIamportUserAuth } from "../decorators/FakeIamportUserAuth"; import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; import { FakeIamportStorage } from "../providers/FakeIamportStorage"; -import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; -@nest.Controller("certifications") +@Controller("certifications") export class FakeIamportCertificationsController { - /** - * 본인인증 정보 열람하기. - * - * `certiciations.at` 은 본인인증 정보를 열람할 때 사용하는 API 함수이다. - * - * 다만 이 API 함수를 통하여 열람한 본인인증 정보 {@link IIamportCertification} 이 - * 곧 OTP 인증까지 마쳐 본인인증을 모두 마친 레코드라는 보장은 없다. 본인인증의 완결 - * 여부는 오직, {@link IIamportCertification.certified} 값을 직접 검사해봐야만 알 - * 수 있기 때문이다. - * - * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} - * @returns 본인인증 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Get(":imp_uid") - public at( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - const certification = FakeIamportStorage.certifications.get(imp_uid); - return FakeIamportResponseProvider.success(certification); - } - - /** - * 본인인증 요청하기. - * - * `certifications.otp.request` 는 아임포트 서버에 본인인증을 요청하는 API 함수이다. - * 이 API 를 호출하면 본인인증 대상자의 핸드폰으로 OTP 문자가 전송되며, 본인인증 - * 대상자가 {@link certifications.otp.confirm} 을 통하여 이 OTP 번호를 정확히 - * 입력함으로써, 본인인증이 완결된다. - * - * 또한 본인인증 대상자가 자신의 핸드폰으로 전송된 OTP 문자를 입력하기 전에도, - * 여전히해당 본인인증 내역은 {@link certifications.at} 함수를 통하여 조회할 수 있다. - * 다만, 이 때 리턴되는 {@link IIamportCertification} 에서 인증의 완결 여부를 - * 지칭하는 {@link IIamportCertification.certified} 값은 `false` 이다. - * - * @param input 본인인증 요청 정보 - * @returns 진행 중인 본인인증의 식별자 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post("otp/request") - public request( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: IIamportCertification.IStore, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - const birth: Date = new Date( - `${input.birth.substr(0, 4)}-${input.birth.substr( - 4, - 2, - )}-${input.birth.substr(6, 2)}`, - ); - const certication: IIamportCertification = { - imp_uid: v4(), - merchant_uid: input.merchant_uid || null, - - name: input.name, - gender: String(Number(input.gender_digit) % 2), - birth: birth.getTime() / 1_000, - birthday: input.birth, - foreigner: false, - phone: input.phone.split("-").join(""), - carrier: input.carrier, - - certified: false, - certified_at: 0, - - unique_key: v4(), - unique_in_site: v4(), - pg_tid: v4(), - pg_provider: "some-provider", - origin: "fake-iamport", - - __otp: randint(0, 9999).toString().padStart(4, "0"), - }; - FakeIamportStorage.certifications.set(certication.imp_uid, certication); - - return FakeIamportResponseProvider.success({ - imp_uid: certication.imp_uid, - }); - } - - /** - * 본인인증 시 발급된 OTP 코드 입력하기. - * - * `certifications.otp.confirm` 는 {@link certifications.otp.request} 를 통하여 - * 발급된 본인인증 건에 대하여, 본인인증 대상자의 휴대폰으로 전송된 OTP 번호를 - * 검증하고, 입력한 OTP 번호가 맞거든 해당 본인인증 건을 승인하여 완료 처리해주는 - * API 함수이다. - * - * 이처럼 본인인증을 완료하거든, 해당 본인인증 건 {@link IIamportCertification} 의 - * {@link IIamportCertification.certified} 값이 비로소 `true` 로 변경되어, - * 비로소 완결된다. - * - * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} - * @param input OTP 코드 - * @returns 인증 완료된 본인인증 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post("otp/confirm/:imp_uid") - public confirm( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - @core.TypedBody() input: IIamportCertification.IConfirm, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - const certification = FakeIamportStorage.certifications.get(imp_uid); - if (certification.certified === true) - throw new nest.UnprocessableEntityException("Already certified."); - else if (certification.__otp !== input.otp) - throw new nest.ForbiddenException("Wrong OTP value."); - - certification.certified = true; - certification.certified_at = Date.now() / 1_000; - return FakeIamportResponseProvider.success(certification); - } - - /** - * 본인인증 정보 삭제하기. - * - * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} - * @returns 삭제된 본인인증 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Delete(":imp_uid") - public erase( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - const certification = FakeIamportStorage.certifications.get(imp_uid); - FakeIamportStorage.certifications.erase(imp_uid); - - return FakeIamportResponseProvider.success(certification); - } + /** + * 본인인증 정보 열람하기. + * + * `certiciations.at` 은 본인인증 정보를 열람할 때 사용하는 API 함수이다. + * + * 다만 이 API 함수를 통하여 열람한 본인인증 정보 {@link IIamportCertification} 이 + * 곧 OTP 인증까지 마쳐 본인인증을 모두 마친 레코드라는 보장은 없다. 본인인증의 완결 + * 여부는 오직, {@link IIamportCertification.certified} 값을 직접 검사해봐야만 알 + * 수 있기 때문이다. + * + * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} + * @returns 본인인증 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Get(":imp_uid") + public at( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + ): IIamportResponse { + const certification = FakeIamportStorage.certifications.get(imp_uid); + return FakeIamportResponseProvider.success(certification); + } + + /** + * 본인인증 요청하기. + * + * `certifications.otp.request` 는 아임포트 서버에 본인인증을 요청하는 API 함수이다. + * 이 API 를 호출하면 본인인증 대상자의 핸드폰으로 OTP 문자가 전송되며, 본인인증 + * 대상자가 {@link certifications.otp.confirm} 을 통하여 이 OTP 번호를 정확히 + * 입력함으로써, 본인인증이 완결된다. + * + * 또한 본인인증 대상자가 자신의 핸드폰으로 전송된 OTP 문자를 입력하기 전에도, + * 여전히해당 본인인증 내역은 {@link certifications.at} 함수를 통하여 조회할 수 있다. + * 다만, 이 때 리턴되는 {@link IIamportCertification} 에서 인증의 완결 여부를 + * 지칭하는 {@link IIamportCertification.certified} 값은 `false` 이다. + * + * @param input 본인인증 요청 정보 + * @returns 진행 중인 본인인증의 식별자 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post("otp/request") + public request( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedBody() input: IIamportCertification.IStore, + ): IIamportResponse { + const birth: Date = new Date( + `${input.birth.substr(0, 4)}-${input.birth.substr( + 4, + 2, + )}-${input.birth.substr(6, 2)}`, + ); + const certication: IIamportCertification = { + imp_uid: v4(), + merchant_uid: input.merchant_uid || null, + + name: input.name, + gender: String(Number(input.gender_digit) % 2), + birth: birth.getTime() / 1_000, + birthday: input.birth, + foreigner: false, + phone: input.phone.split("-").join(""), + carrier: input.carrier, + + certified: false, + certified_at: 0, + + unique_key: v4(), + unique_in_site: v4(), + pg_tid: v4(), + pg_provider: "some-provider", + origin: "fake-iamport", + + __otp: randint(0, 9999).toString().padStart(4, "0"), + }; + FakeIamportStorage.certifications.set(certication.imp_uid, certication); + + return FakeIamportResponseProvider.success({ + imp_uid: certication.imp_uid, + }); + } + + /** + * 본인인증 시 발급된 OTP 코드 입력하기. + * + * `certifications.otp.confirm` 는 {@link certifications.otp.request} 를 통하여 + * 발급된 본인인증 건에 대하여, 본인인증 대상자의 휴대폰으로 전송된 OTP 번호를 + * 검증하고, 입력한 OTP 번호가 맞거든 해당 본인인증 건을 승인하여 완료 처리해주는 + * API 함수이다. + * + * 이처럼 본인인증을 완료하거든, 해당 본인인증 건 {@link IIamportCertification} 의 + * {@link IIamportCertification.certified} 값이 비로소 `true` 로 변경되어, + * 비로소 완결된다. + * + * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} + * @param input OTP 코드 + * @returns 인증 완료된 본인인증 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post("otp/confirm/:imp_uid") + public confirm( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + @core.TypedBody() input: IIamportCertification.IConfirm, + ): IIamportResponse { + const certification = FakeIamportStorage.certifications.get(imp_uid); + if (certification.certified === true) + throw new UnprocessableEntityException("Already certified."); + else if (certification.__otp !== input.otp) + throw new ForbiddenException("Wrong OTP value."); + + certification.certified = true; + certification.certified_at = Date.now() / 1_000; + return FakeIamportResponseProvider.success(certification); + } + + /** + * 본인인증 정보 삭제하기. + * + * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} + * @returns 삭제된 본인인증 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Delete(":imp_uid") + public erase( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + ): IIamportResponse { + const certification = FakeIamportStorage.certifications.get(imp_uid); + FakeIamportStorage.certifications.erase(imp_uid); + + return FakeIamportResponseProvider.success(certification); + } } diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts index c2ba3ed..660f582 100644 --- a/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts +++ b/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts @@ -1,72 +1,66 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller, UnprocessableEntityException } from "@nestjs/common"; import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; +import { FakeIamportUserAuth } from "../decorators/FakeIamportUserAuth"; import { FakeIamportPaymentProvider } from "../providers/FakeIamportPaymentProvider"; import { FakeIamportStorage } from "../providers/FakeIamportStorage"; -import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; -@nest.Controller("internal") +@Controller("internal") export class FakeIamportInternalController { - /** - * 웹훅 이벤트 더미 리스너. - * - * `internal.webhook` 은 실제 아임포트의 서버에는 존재하지 않는 API 로써, - * `fake-impoart-server` 의 {@link Configuration.WEBHOOK_URL} 에 아무런 URL 을 설정하지 - * 않으면, `fake-iamport-server` 로부터 발생하는 모든 종류의 웹훅 이벤트는 이 곳으로 전달되어 - * 무의미하게 사라진다. - * - * 따라서 `fake-iamport-server` 를 사용하여 아임포트 서버와의 연동을 미리 검증코자 할 때는, - * 반드시 {@link Configuration.WEBHOOK_URL} 를 설정하여 웹훅 이벤트가 귀하의 백엔드 서버로 - * 제대로 전달되도록 하자. - * - * @param input 웹훅 이벤트 정보 - * - * @author Samchon - */ - @core.TypedRoute.Post("webhook") - public webhook(@core.TypedBody() input: IIamportPayment.IWebhook): void { - input; // DO NOTHING - } + /** + * 웹훅 이벤트 더미 리스너. + * + * `internal.webhook` 은 실제 아임포트의 서버에는 존재하지 않는 API 로써, + * `fake-impoart-server` 의 {@link Configuration.WEBHOOK_URL} 에 아무런 URL 을 설정하지 + * 않으면, `fake-iamport-server` 로부터 발생하는 모든 종류의 웹훅 이벤트는 이 곳으로 전달되어 + * 무의미하게 사라진다. + * + * 따라서 `fake-iamport-server` 를 사용하여 아임포트 서버와의 연동을 미리 검증코자 할 때는, + * 반드시 {@link Configuration.WEBHOOK_URL} 를 설정하여 웹훅 이벤트가 귀하의 백엔드 서버로 + * 제대로 전달되도록 하자. + * + * @param input 웹훅 이벤트 정보 + * + * @author Samchon + */ + @core.TypedRoute.Post("webhook") + public webhook(@core.TypedBody() input: IIamportPayment.IWebhook): void { + input; // DO NOTHING + } - /** - * 가상 계좌에 입금하기. - * - * `internal.deposit` 은 실제 아임포트 결제 서버에는 존재하지 않는 API 로써, 가상 계좌 - * 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 상황을 시뮬레이션 할 수 있는 - * 함수이다. - * - * 즉, `internal.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 입금을 하고, 그에 따라 - * 아임포트 서버에서 webhook 이벤트가 발생, 이를 귀하의 백엔드 서버로 전송하는 일련의 상황을 - * 시뮬레이션하기 위하여 설계된 테스트 함수다. - * - * @param imp_uid 대상 결제의 {@link IIamportVBankPayment.imp_uid} - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Put("deposit/:imp_uid") - public deposit( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - ): void { - // AUTHORIZE - FakeIamportUserAuth.authorize(request); + /** + * 가상 계좌에 입금하기. + * + * `internal.deposit` 은 실제 아임포트 결제 서버에는 존재하지 않는 API 로써, 가상 계좌 + * 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 상황을 시뮬레이션 할 수 있는 + * 함수이다. + * + * 즉, `internal.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 입금을 하고, 그에 따라 + * 아임포트 서버에서 webhook 이벤트가 발생, 이를 귀하의 백엔드 서버로 전송하는 일련의 상황을 + * 시뮬레이션하기 위하여 설계된 테스트 함수다. + * + * @param imp_uid 대상 결제의 {@link IIamportVBankPayment.imp_uid} + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Put("deposit/:imp_uid") + public deposit( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + ): void { + // GET PAYMENT RECORD + const payment: IIamportPayment = FakeIamportStorage.payments.get(imp_uid); + if (payment.pay_method !== "vbank") + throw new UnprocessableEntityException("Not a virtual bank payment."); - // GET PAYMENT RECORD - const payment: IIamportPayment = - FakeIamportStorage.payments.get(imp_uid); - if (payment.pay_method !== "vbank") - throw new nest.UnprocessableEntityException( - "Not a virtual bank payment.", - ); + // MODIFY + payment.status = "paid"; + payment.paid_at = Date.now() / 1000; - // MODIFY - payment.status = "paid"; - payment.paid_at = Date.now() / 1000; - - // INFORM - FakeIamportPaymentProvider.webhook(payment).catch(() => {}); - } + // INFORM + FakeIamportPaymentProvider.webhook(payment).catch(() => {}); + } } diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportPaymentsController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportPaymentsController.ts index b312d0b..ff8ce13 100644 --- a/packages/fake-iamport-server/src/controllers/FakeIamportPaymentsController.ts +++ b/packages/fake-iamport-server/src/controllers/FakeIamportPaymentsController.ts @@ -1,65 +1,60 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller } from "@nestjs/common"; import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; import { IIamportPaymentCancel } from "../api/structures/IIamportPaymentCancel"; +import { FakeIamportUserAuth } from "../decorators/FakeIamportUserAuth"; import { FakeIamportPaymentProvider } from "../providers/FakeIamportPaymentProvider"; import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; import { FakeIamportStorage } from "../providers/FakeIamportStorage"; -import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; -@nest.Controller("payments") +@Controller("payments") export class FakeIamportPaymentsController { - /** - * 결제 기록 열람하기. - * - * 아임포트를 통하여 발생한 결제 기록을 열람한다. - * - * @param imp_uid 대상 결제 기록의 {@link IIamportPayment.imp_uid} - * @param query 결제 수단이 페이팔인 경우에 사용 - * @returns 결제 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Get(":imp_uid") - public at( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - @core.TypedQuery() query: IIamportPayment.IQuery, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); + /** + * 결제 기록 열람하기. + * + * 아임포트를 통하여 발생한 결제 기록을 열람한다. + * + * @param imp_uid 대상 결제 기록의 {@link IIamportPayment.imp_uid} + * @param query 결제 수단이 페이팔인 경우에 사용 + * @returns 결제 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Get(":imp_uid") + public at( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + @core.TypedQuery() query: IIamportPayment.IQuery, + ): IIamportResponse { + query; + const payment: IIamportPayment = FakeIamportStorage.payments.get(imp_uid); + return FakeIamportResponseProvider.success(payment); + } - query; - const payment: IIamportPayment = - FakeIamportStorage.payments.get(imp_uid); - return FakeIamportResponseProvider.success(payment); - } - - /** - * 결제 취소하기. - * - * 만약 가상 계좌를 통한 결제였다면, 반드시 환불 계좌 정보를 입력해줘야 한다. - * - * @param input 결제 취소 입력 정보 - * @returns 취소된 결제 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post("cancel") - public cancel( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: IIamportPaymentCancel.IStore, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - const payment: IIamportPayment = FakeIamportStorage.payments.get( - input.imp_uid, - ); - FakeIamportPaymentProvider.cancel(payment, input); - return FakeIamportResponseProvider.success(payment); - } + /** + * 결제 취소하기. + * + * 만약 가상 계좌를 통한 결제였다면, 반드시 환불 계좌 정보를 입력해줘야 한다. + * + * @param input 결제 취소 입력 정보 + * @returns 취소된 결제 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post("cancel") + public cancel( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedBody() input: IIamportPaymentCancel.IStore, + ): IIamportResponse { + const payment: IIamportPayment = FakeIamportStorage.payments.get( + input.imp_uid, + ); + FakeIamportPaymentProvider.cancel(payment, input); + return FakeIamportResponseProvider.success(payment); + } } diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts index c2625dc..016f3da 100644 --- a/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts +++ b/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts @@ -1,111 +1,100 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller, UnprocessableEntityException } from "@nestjs/common"; import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; import { IIamportReceipt } from "iamport-server-api/lib/structures/IIamportReceipt"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; import { v4 } from "uuid"; +import { FakeIamportUserAuth } from "../decorators/FakeIamportUserAuth"; import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; import { FakeIamportStorage } from "../providers/FakeIamportStorage"; -import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; -@nest.Controller("receipts/:imp_uid") +@Controller("receipts/:imp_uid") export class FakeIamportReceiptsController { - /** - * 현금 영수증 조회하기. - * - * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} - * @returns 현금 영수증 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Get() - public at( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); + /** + * 현금 영수증 조회하기. + * + * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} + * @returns 현금 영수증 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Get() + public at( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + ): IIamportResponse { + const receipt: IIamportReceipt = FakeIamportStorage.receipts.get(imp_uid); + return FakeIamportResponseProvider.success(receipt); + } - const receipt: IIamportReceipt = - FakeIamportStorage.receipts.get(imp_uid); - return FakeIamportResponseProvider.success(receipt); + /** + * 현금 영수증 발행하기. + * + * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} + * @param input 현금 영수증 입력 정보 + * @returns 현금 영수증 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post() + public store( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + @core.TypedBody() input: IIamportReceipt.IStore, + ): IIamportResponse { + const payment: IIamportPayment = FakeIamportStorage.payments.get(imp_uid); + if (!payment.paid_at) + throw new UnprocessableEntityException("Not paid yet."); + else if (FakeIamportStorage.receipts.has(imp_uid) === true) { + const oldbie: IIamportReceipt = FakeIamportStorage.receipts.get(imp_uid); + if (oldbie.cancelled_at === null) + throw new UnprocessableEntityException("Already issued."); } - /** - * 현금 영수증 발행하기. - * - * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} - * @param input 현금 영수증 입력 정보 - * @returns 현금 영수증 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post() - public store( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - @core.TypedBody() input: IIamportReceipt.IStore, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); + const receipt: IIamportReceipt = { + imp_uid, + receipt_uid: v4(), + apply_num: v4(), + type: input.type || "person", + amount: payment.amount, + vat: payment.amount * 0.1, + receipt_url: "https://github.com/samchon/fake-iamport-server", + applied_at: Date.now() / 1000, + cancelled_at: 0, + }; + FakeIamportStorage.receipts.set(imp_uid, receipt); + payment.cash_receipt_issue = true; - const payment: IIamportPayment = - FakeIamportStorage.payments.get(imp_uid); - if (!payment.paid_at) - throw new nest.UnprocessableEntityException("Not paid yet."); - else if (FakeIamportStorage.receipts.has(imp_uid) === true) { - const oldbie: IIamportReceipt = - FakeIamportStorage.receipts.get(imp_uid); - if (oldbie.cancelled_at === null) - throw new nest.UnprocessableEntityException("Already issued."); - } + return FakeIamportResponseProvider.success(receipt); + } - const receipt: IIamportReceipt = { - imp_uid, - receipt_uid: v4(), - apply_num: v4(), - type: input.type || "person", - amount: payment.amount, - vat: payment.amount * 0.1, - receipt_url: "https://github.com/samchon/fake-iamport-server", - applied_at: Date.now() / 1000, - cancelled_at: 0, - }; - FakeIamportStorage.receipts.set(imp_uid, receipt); - payment.cash_receipt_issue = true; + /** + * 현금 영수증 취소하기. + * + * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} + * @returns 취소된 현금 영수증 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Delete() + public erase( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("imp_uid") imp_uid: string, + ) { + const payment: IIamportPayment = FakeIamportStorage.payments.get(imp_uid); + const receipt: IIamportReceipt = FakeIamportStorage.receipts.get(imp_uid); - return FakeIamportResponseProvider.success(receipt); - } - - /** - * 현금 영수증 취소하기. - * - * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} - * @returns 취소된 현금 영수증 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Delete() - public erase( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("imp_uid") imp_uid: string, - ) { - FakeIamportUserAuth.authorize(request); - - const payment: IIamportPayment = - FakeIamportStorage.payments.get(imp_uid); - const receipt: IIamportReceipt = - FakeIamportStorage.receipts.get(imp_uid); + if (receipt.cancelled_at !== null) + throw new UnprocessableEntityException("Already cancelled."); - if (receipt.cancelled_at !== null) - throw new nest.UnprocessableEntityException("Already cancelled."); + payment.cash_receipt_issue = false; + receipt.cancelled_at = Date.now() / 1000; - payment.cash_receipt_issue = false; - receipt.cancelled_at = Date.now() / 1000; - - return FakeIamportResponseProvider.success(receipt); - } + return FakeIamportResponseProvider.success(receipt); + } } diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts index 2612ef8..565f9ad 100644 --- a/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts +++ b/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts @@ -1,33 +1,33 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; +import { Controller } from "@nestjs/common"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; +import { FakeIamportUserAuth } from "../decorators/FakeIamportUserAuth"; import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; -import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; -@nest.Controller("users") +@Controller("users") export class FakeIamportUsersController { - /** - * 유저 인증 토큰 발행하기. - * - * 아임포트에 가입하여 부여받은 API 및 secret 키를 토대로, 유저 인증 토큰을 발행한다. - * - * 단, 아임포트가 발급해주는 유저 인증 토큰에는 유효 시간 {@link IIamportUser.expired_at} - * 이 있어, 해당 시간이 지나거든 기 발급 토큰이 만료되어 더 이상 쓸 수 없게 된다. 때문에 - * 아임포트의 이러한 시간 제한에 구애받지 않고 자유로이 아임포트의 API 를 이용하고 싶다면, - * `iamport-server-api` 에서 제공해주는 {@link IamportConnector} 를 활용하도록 하자. - * - * @param input 아임포트의 API 및 secret 키 정보 - * @returns 유저 인증 토큰 정보 - * - * @author Samchon - */ - @core.TypedRoute.Post("getToken") - public getToken( - @core.TypedBody() input: IIamportUser.IAccessor, - ): IIamportResponse { - const user: IIamportUser = FakeIamportUserAuth.issue(input); - return FakeIamportResponseProvider.success(user); - } + /** + * 유저 인증 토큰 발행하기. + * + * 아임포트에 가입하여 부여받은 API 및 secret 키를 토대로, 유저 인증 토큰을 발행한다. + * + * 단, 아임포트가 발급해주는 유저 인증 토큰에는 유효 시간 {@link IIamportUser.expired_at} + * 이 있어, 해당 시간이 지나거든 기 발급 토큰이 만료되어 더 이상 쓸 수 없게 된다. 때문에 + * 아임포트의 이러한 시간 제한에 구애받지 않고 자유로이 아임포트의 API 를 이용하고 싶다면, + * `iamport-server-api` 에서 제공해주는 {@link IamportConnector} 를 활용하도록 하자. + * + * @param input 아임포트의 API 및 secret 키 정보 + * @returns 유저 인증 토큰 정보 + * + * @author Samchon + */ + @core.TypedRoute.Post("getToken") + public getToken( + @core.TypedBody() input: IIamportUser.IAccessor, + ): IIamportResponse { + const user: IIamportUser = FakeIamportUserAuth.issue(input); + return FakeIamportResponseProvider.success(user); + } } diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts index 99a7e19..5082250 100644 --- a/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts +++ b/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts @@ -1,129 +1,121 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller, UnprocessableEntityException } from "@nestjs/common"; import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; import { randint } from "tstl/algorithm/random"; import { v4 } from "uuid"; +import { FakeIamportUserAuth } from "../decorators/FakeIamportUserAuth"; import { FakeIamportPaymentProvider } from "../providers/FakeIamportPaymentProvider"; import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; import { FakeIamportStorage } from "../providers/FakeIamportStorage"; -import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; import { AdvancedRandomGenerator } from "../utils/AdvancedRandomGenerator"; -@nest.Controller("vbanks") +@Controller("vbanks") export class FakeIamportVbanksController { - /** - * 가상 계좌 발급하기. - * - * @param input 가상 계좌 입력 정보 - * @returns 가상 계좌 결제 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post() - public store( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: IIamportVBankPayment.IStore, - ): IIamportResponse { - // AUTHORIZE - FakeIamportUserAuth.authorize(request); + /** + * 가상 계좌 발급하기. + * + * @param input 가상 계좌 입력 정보 + * @returns 가상 계좌 결제 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post() + public store( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedBody() input: IIamportVBankPayment.IStore, + ): IIamportResponse { + // CONSTRUCTION + const pg_id: string = v4(); + const payment: IIamportVBankPayment = { + // VIRTUAL-BANK INFO + vbank_code: input.vbank_code, + vbank_name: AdvancedRandomGenerator.name(2) + "은행", + vbank_num: randint(100000000, 999999999).toString(), + vbank_holder: AdvancedRandomGenerator.name(), + vbank_date: input.vbank_due, + vbank_issued_at: Date.now(), - // CONSTRUCTION - const pg_id: string = v4(); - const payment: IIamportVBankPayment = { - // VIRTUAL-BANK INFO - vbank_code: input.vbank_code, - vbank_name: AdvancedRandomGenerator.name(2) + "은행", - vbank_num: randint(100000000, 999999999).toString(), - vbank_holder: AdvancedRandomGenerator.name(), - vbank_date: input.vbank_due, - vbank_issued_at: Date.now(), + // ORDER INFO + pay_method: "vbank", + currency: "KRW", + merchant_uid: input.merchant_uid, + imp_uid: v4(), + name: input.name || null, + amount: input.amount, + cancel_amount: 0, + receipt_url: "https://github.com/samchon/fake-iamport-server", + cash_receipt_issue: true, - // ORDER INFO - pay_method: "vbank", - currency: "KRW", - merchant_uid: input.merchant_uid, - imp_uid: v4(), - name: input.name || null, - amount: input.amount, - cancel_amount: 0, - receipt_url: "https://github.com/samchon/fake-iamport-server", - cash_receipt_issue: true, + // PAYMENT PROVIDER INFO + channel: Math.random() < 0.5 ? "pc" : "mobile", + pg_provider: "somewhere", + emb_pg_provider: null, + pg_id, + pg_tid: pg_id, + escrow: false, - // PAYMENT PROVIDER INFO - channel: Math.random() < 0.5 ? "pc" : "mobile", - pg_provider: "somewhere", - emb_pg_provider: null, - pg_id, - pg_tid: pg_id, - escrow: false, + // BUYER + buyer_name: input.buyer_name || null, + buyer_tel: input.buyer_tel || null, + buyer_email: input.buyer_email || null, + buyer_addr: input.buyer_addr || null, + buyer_postcode: input.buyer_postcode || null, + customer_uid: v4(), + customer_uid_usage: "issue", + custom_data: input.custom_data || null, + user_agent: "Test Automation", - // BUYER - buyer_name: input.buyer_name || null, - buyer_tel: input.buyer_tel || null, - buyer_email: input.buyer_email || null, - buyer_addr: input.buyer_addr || null, - buyer_postcode: input.buyer_postcode || null, - customer_uid: v4(), - customer_uid_usage: "issue", - custom_data: input.custom_data || null, - user_agent: "Test Automation", + // TIMESTAMPS + status: "ready", + started_at: Date.now() / 1000, + paid_at: 0, + failed_at: 0, + fail_reason: null, + cancelled_at: 0, + cancel_reason: null, + cancel_history: [], - // TIMESTAMPS - status: "ready", - started_at: Date.now() / 1000, - paid_at: 0, - failed_at: 0, - fail_reason: null, - cancelled_at: 0, - cancel_reason: null, - cancel_history: [], + // HIDDEN + notice_url: input.notice_url, + }; + FakeIamportPaymentProvider.store(payment); - // HIDDEN - notice_url: input.notice_url, - }; - FakeIamportPaymentProvider.store(payment); + // RETURNS + return FakeIamportResponseProvider.success(payment); + } - // RETURNS - return FakeIamportResponseProvider.success(payment); - } + /** + * 가상 계좌 편집하기. + * + * @param input 가상 계좌 편집 입력 정보 + * @returns 편집된 가상 계좌 결제 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Put() + public update( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedBody() input: IIamportVBankPayment.IUpdate, + ): IIamportResponse { + // GET PAYMENT RECORD + const payment: IIamportPayment = FakeIamportStorage.payments.get( + input.imp_uid, + ); + if (payment.pay_method !== "vbank") + throw new UnprocessableEntityException("Not a virtual bank payment."); - /** - * 가상 계좌 편집하기. - * - * @param input 가상 계좌 편집 입력 정보 - * @returns 편집된 가상 계좌 결제 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Put() - public update( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: IIamportVBankPayment.IUpdate, - ): IIamportResponse { - // AUTHORIZE - FakeIamportUserAuth.authorize(request); + // MODIFY + if (input.amount) payment.amount = input.amount; + if (input.vbank_due) payment.vbank_date = input.vbank_due; - // GET PAYMENT RECORD - const payment: IIamportPayment = FakeIamportStorage.payments.get( - input.imp_uid, - ); - if (payment.pay_method !== "vbank") - throw new nest.UnprocessableEntityException( - "Not a virtual bank payment.", - ); - - // MODIFY - if (input.amount) payment.amount = input.amount; - if (input.vbank_due) payment.vbank_date = input.vbank_due; - - // RETURNS WITH INFORM - FakeIamportPaymentProvider.webhook(payment).catch(() => {}); - return FakeIamportResponseProvider.success(payment); - } + // RETURNS WITH INFORM + FakeIamportPaymentProvider.webhook(payment).catch(() => {}); + return FakeIamportResponseProvider.success(payment); + } } diff --git a/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts index 702ad15..dc75e4d 100644 --- a/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts +++ b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts @@ -1,121 +1,111 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller } from "@nestjs/common"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; import { v4 } from "uuid"; +import { FakeIamportUserAuth } from "../../decorators/FakeIamportUserAuth"; import { FakeIamportResponseProvider } from "../../providers/FakeIamportResponseProvider"; import { FakeIamportStorage } from "../../providers/FakeIamportStorage"; -import { FakeIamportUserAuth } from "../../providers/FakeIamportUserAuth"; import { AdvancedRandomGenerator } from "../../utils/AdvancedRandomGenerator"; -@nest.Controller("subscribe/customers") +@Controller("subscribe/customers") export class FakeIamportSubscribeCustomersController { - /** - * 간편 결제 카드 정보 조회하기. - * - * `subscribe.customers.at` 은 고객이 {@link store} 나 혹은 아임포트가 제공하는 - * 간편 결제 카드 등록 창을 이용하여 저장한 간편 결제 카드 정보를 조회하는 API - * 함수이다. - * - * @param customer_uid 고객 (간편 결제 카드) 식별자 키 - * @returns 간편 결제 카드 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Get(":customer_uid") - public at( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("customer_uid") customer_uid: string, - ): IIamportResponse { - // AUTHORIZE - FakeIamportUserAuth.authorize(request); + /** + * 간편 결제 카드 정보 조회하기. + * + * `subscribe.customers.at` 은 고객이 {@link store} 나 혹은 아임포트가 제공하는 + * 간편 결제 카드 등록 창을 이용하여 저장한 간편 결제 카드 정보를 조회하는 API + * 함수이다. + * + * @param customer_uid 고객 (간편 결제 카드) 식별자 키 + * @returns 간편 결제 카드 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Get(":customer_uid") + public at( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("customer_uid") customer_uid: string, + ): IIamportResponse { + // GET SUBSCRIPTION RECORD + const subscription = FakeIamportStorage.subscriptions.get(customer_uid); - // GET SUBSCRIPTION RECORD - const subscription = FakeIamportStorage.subscriptions.get(customer_uid); + // RETURNS + return FakeIamportResponseProvider.success(subscription); + } - // RETURNS - return FakeIamportResponseProvider.success(subscription); - } + /** + * 간편 결제 카드 등록하기. + * + * `subscribe.customers.stoer` 는 고객이 자신의 카드를 서버에 등록해두고, 매번 결제가 + * 필요할 때마다 카드 정보를 반복 입력하는 일 없이, 간편하게 결제를 진행하고자 할 때 + * 사용하는 API 함수이다. + * + * 참고로 `subscribe.customers.store` 는 클라이언트 어플리케이션이 아임포트가 제공하는 + * 간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 + * 일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 상황을 시뮬레이션하기 위하여, + * 테스트 자동화 프로그램 수준에서 사용될 수는 있다. + * + * @param customer_uid 고객 (간편 결제 카드) 식별자 키 + * @param input 카드 입력 정보 + * @returns 간편 결제 카드 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post(":customer_uid") + public store( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("customer_uid") customer_uid: string, + @core.TypedBody() input: IIamportSubscription.IStore, + ): IIamportResponse { + // ENROLLMENT + const subscription: IIamportSubscription = { + customer_uid, + pg_provider: "pg-of-somewhere", + pg_id: v4(), + card_type: "card", + card_code: v4(), + card_name: AdvancedRandomGenerator.name(), + card_number: input.card_number, + customer_name: AdvancedRandomGenerator.name(), + customer_tel: AdvancedRandomGenerator.mobile(), + customer_addr: "address-of-somewhere", + customer_email: AdvancedRandomGenerator.alphabets(8) + "@samchon.org", + customer_postcode: "11122", + inserted: 1, + updated: 0, + }; + FakeIamportStorage.subscriptions.set(customer_uid, subscription); - /** - * 간편 결제 카드 등록하기. - * - * `subscribe.customers.stoer` 는 고객이 자신의 카드를 서버에 등록해두고, 매번 결제가 - * 필요할 때마다 카드 정보를 반복 입력하는 일 없이, 간편하게 결제를 진행하고자 할 때 - * 사용하는 API 함수이다. - * - * 참고로 `subscribe.customers.store` 는 클라이언트 어플리케이션이 아임포트가 제공하는 - * 간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 - * 일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 상황을 시뮬레이션하기 위하여, - * 테스트 자동화 프로그램 수준에서 사용될 수는 있다. - * - * @param customer_uid 고객 (간편 결제 카드) 식별자 키 - * @param input 카드 입력 정보 - * @returns 간편 결제 카드 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post(":customer_uid") - public store( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("customer_uid") customer_uid: string, - @core.TypedBody() input: IIamportSubscription.IStore, - ): IIamportResponse { - // AUTHORIZE - FakeIamportUserAuth.authorize(request); + // RETURNS + return FakeIamportResponseProvider.success(subscription); + } - // ENROLLMENT - const subscription: IIamportSubscription = { - customer_uid, - pg_provider: "pg-of-somewhere", - pg_id: v4(), - card_type: "card", - card_code: v4(), - card_name: AdvancedRandomGenerator.name(), - card_number: input.card_number, - customer_name: AdvancedRandomGenerator.name(), - customer_tel: AdvancedRandomGenerator.mobile(), - customer_addr: "address-of-somewhere", - customer_email: - AdvancedRandomGenerator.alphabets(8) + "@samchon.org", - customer_postcode: "11122", - inserted: 1, - updated: 0, - }; - FakeIamportStorage.subscriptions.set(customer_uid, subscription); + /** + * 간편 결제 카드 삭제하기. + * + * 간편 결제를 위하여 등록한 카드를 제거한다. + * + * @param customer_uid 고객 (간편 결제 카드) 식별자 키 + * @returns 삭제된 간편 결제 카드 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Delete(":customer_uid") + public erase( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedParam("customer_uid") customer_uid: string, + ): IIamportResponse { + // ERASE RECORD + const subscription = FakeIamportStorage.subscriptions.get(customer_uid); + FakeIamportStorage.subscriptions.erase(customer_uid); - // RETURNS - return FakeIamportResponseProvider.success(subscription); - } - - /** - * 간편 결제 카드 삭제하기. - * - * 간편 결제를 위하여 등록한 카드를 제거한다. - * - * @param customer_uid 고객 (간편 결제 카드) 식별자 키 - * @returns 삭제된 간편 결제 카드 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Delete(":customer_uid") - public erase( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("customer_uid") customer_uid: string, - ): IIamportResponse { - // AUTHORIZE - FakeIamportUserAuth.authorize(request); - - // ERASE RECORD - const subscription = FakeIamportStorage.subscriptions.get(customer_uid); - FakeIamportStorage.subscriptions.erase(customer_uid); - - // RETURNS - return FakeIamportResponseProvider.success(subscription); - } + // RETURNS + return FakeIamportResponseProvider.success(subscription); + } } diff --git a/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts index 22cf3f8..81e9dc8 100644 --- a/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts +++ b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts @@ -1,198 +1,193 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller } from "@nestjs/common"; import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; import { v4 } from "uuid"; +import { FakeIamportUserAuth } from "../../decorators/FakeIamportUserAuth"; import { FakeIamportPaymentProvider } from "../../providers/FakeIamportPaymentProvider"; import { FakeIamportResponseProvider } from "../../providers/FakeIamportResponseProvider"; import { FakeIamportStorage } from "../../providers/FakeIamportStorage"; import { FakeIamportSubscriptionProvider } from "../../providers/FakeIamportSubscriptionProvider"; -import { FakeIamportUserAuth } from "../../providers/FakeIamportUserAuth"; import { AdvancedRandomGenerator } from "../../utils/AdvancedRandomGenerator"; -@nest.Controller("subscribe/payments") +@Controller("subscribe/payments") export class FakeIampotSubscribePaymentsController { - /** - * 카드로 결제하기, 더불어 간편 결제용으로 등록 가능. - * - * `subscribe.payments.onetime` 은 카드를 매개로 한 결제를 하고자 할 때 호출하는 API - * 함수이다. 더하여 입력 값에 {@link IIamportSubscription.IOnetime.customer_uid} 를 - * 기입하는 경우, 결제에 사용한 카드를 그대로 간편 결제용 카드 - * {@link IIamportSubscription} 로 등록해버린다. - * - * 다만, 정히 간편 카드 등록과 결제를 동시에 하고 싶다면, - * `subscribe.payments.onetime` 에 {@link IIamportSubscription.IOnetime.customer_uid} - * 를 더하기보다, {@link subscribe.customers.store} 와 {@link subscribe.payments.again} - * 을 각각 호출하는 것을 권장한다. 그것이 예외적인 상황에 보다 안전하게 대처할 수 있기 - * 때문이다. - * - * 더하여 `subscribe.payments.onetime` 은 클라이언트 어플리케이션이 아임포트가 제공하는 - * 결제 창을 그대로 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 일은 - * 없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여, 테스트 - * 자동화 프로그램 수준에서 사용될 수는 있다. - * - * @param input 카드 결제 신청 정보 - * @returns 카드 결제 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post("onetime") - public onetime( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: IIamportSubscription.IOnetime, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - if (input.customer_uid) - FakeIamportSubscriptionProvider.store( - input.customer_uid, - input as IIamportSubscription.IStore, - ); - - const pg_id: string = v4(); - const payment: IIamportCardPayment = { - card_code: v4(), - card_name: AdvancedRandomGenerator.name(), - card_number: input.card_number, - card_quota: input.card_quota || 0, - apply_num: v4(), - - // ORDER INFO - pay_method: "card", - currency: input.currency || "KRW", - merchant_uid: input.merchant_uid, - imp_uid: v4(), - name: input.name, - amount: input.amount, - cancel_amount: 0, - receipt_url: "https://github.com/samchon/fake-iamport-server", - cash_receipt_issue: true, - - // PAYMENT PROVIDER INFO - channel: Math.random() < 0.5 ? "pc" : "mobile", - pg_provider: "somewhere", - emb_pg_provider: null, - pg_id, - pg_tid: pg_id, - escrow: false, - - // BUYER - buyer_name: input.buyer_name || null, - buyer_tel: input.buyer_tel || null, - buyer_email: input.buyer_email || null, - buyer_addr: input.buyer_addr || null, - buyer_postcode: input.buyer_postcode || null, - customer_uid: input.customer_uid || null, - customer_uid_usage: input.customer_uid ? "issue" : null, - custom_data: input.custom_data || null, - user_agent: "Test Automation", - - // TIMESTAMPS - status: "paid", - started_at: Date.now() / 1000, - paid_at: Date.now() / 1000, - failed_at: 0, - fail_reason: null, - cancelled_at: 0, - cancel_reason: null, - cancel_history: [], - - // HIDDEN - notice_url: input.notice_url, - }; - FakeIamportPaymentProvider.store(payment); - - return FakeIamportResponseProvider.success(payment); - } - - /** - * 간편 결제에 등록된 카드로 결제하기. - * - * `subscribe.payments.again` 은 고객이 간편 결제에 등록한 카드로 결제를 진행하고자 할 때 - * 호출하는 API 함수이다. 이는 간편하고 불편하고를 떠나, 본질적으로 카드 결제의 일환이기에, - * 리턴값은 일반적인 카드 결제 때와 동일한 {@link IIamportCardPayment} 이다. - * - * 그리고 `subscribe.payments.again` 은 결제 수단 중 유일하게, 클라이언트 어플리케이션이 - * 아임포트가 제공하는 결체 창을 사용할 수 없어, 오직 귀하의 백엔드 서버가 아임포트의 API - * 함수를 직접 호출해야하는 경우에 해당한다. 따라서 간편 결제에 관하여 아임포트 서버와 - * 연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 대한 별도의 - * 설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다. - * - * @param input 미리 등록한 카드를 이용한 결제 신청 정보 - * @returns 카드 결제 정보 - * - * @security bearer - * @author Samchon - */ - @core.TypedRoute.Post("again") - public again( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: IIamportSubscription.IAgain, - ): IIamportResponse { - FakeIamportUserAuth.authorize(request); - - const subscription: IIamportSubscription = - FakeIamportStorage.subscriptions.get(input.customer_uid); - - const pg_id: string = v4(); - const payment: IIamportCardPayment = { - card_code: subscription.card_code, - card_name: subscription.card_name, - card_number: subscription.card_number, - card_quota: input.card_quota || 0, - apply_num: v4(), - - // ORDER INFO - pay_method: "card", - currency: input.currency || "KRW", - merchant_uid: input.merchant_uid, - imp_uid: v4(), - name: input.name, - amount: input.amount, - cancel_amount: 0, - receipt_url: "https://github.com/samchon/fake-iamport-server", - cash_receipt_issue: true, - - // PAYMENT PROVIDER INFO - channel: Math.random() < 0.5 ? "pc" : "mobile", - pg_provider: "somewhere", - emb_pg_provider: null, - pg_id, - pg_tid: pg_id, - escrow: false, - - // BUYER - buyer_name: input.buyer_name || subscription.customer_name || null, - buyer_tel: input.buyer_tel || subscription.customer_tel || null, - buyer_email: - input.buyer_email || subscription.customer_email || null, - buyer_addr: input.buyer_addr || subscription.customer_addr || null, - buyer_postcode: - input.buyer_postcode || subscription.customer_postcode || null, - customer_uid: subscription.customer_uid, - customer_uid_usage: "issue", - custom_data: input.custom_data || null, - user_agent: "Test Automation", - - // TIMESTAMPS - status: "paid", - started_at: Date.now() / 1000, - paid_at: Date.now() / 1000, - failed_at: 0, - fail_reason: null, - cancelled_at: 0, - cancel_reason: null, - cancel_history: [], - - // HIDDEN - notice_url: input.notice_url, - }; - FakeIamportPaymentProvider.store(payment); - - return FakeIamportResponseProvider.success(payment); - } + /** + * 카드로 결제하기, 더불어 간편 결제용으로 등록 가능. + * + * `subscribe.payments.onetime` 은 카드를 매개로 한 결제를 하고자 할 때 호출하는 API + * 함수이다. 더하여 입력 값에 {@link IIamportSubscription.IOnetime.customer_uid} 를 + * 기입하는 경우, 결제에 사용한 카드를 그대로 간편 결제용 카드 + * {@link IIamportSubscription} 로 등록해버린다. + * + * 다만, 정히 간편 카드 등록과 결제를 동시에 하고 싶다면, + * `subscribe.payments.onetime` 에 {@link IIamportSubscription.IOnetime.customer_uid} + * 를 더하기보다, {@link subscribe.customers.store} 와 {@link subscribe.payments.again} + * 을 각각 호출하는 것을 권장한다. 그것이 예외적인 상황에 보다 안전하게 대처할 수 있기 + * 때문이다. + * + * 더하여 `subscribe.payments.onetime` 은 클라이언트 어플리케이션이 아임포트가 제공하는 + * 결제 창을 그대로 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 일은 + * 없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여, 테스트 + * 자동화 프로그램 수준에서 사용될 수는 있다. + * + * @param input 카드 결제 신청 정보 + * @returns 카드 결제 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post("onetime") + public onetime( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedBody() input: IIamportSubscription.IOnetime, + ): IIamportResponse { + if (input.customer_uid) + FakeIamportSubscriptionProvider.store( + input.customer_uid, + input as IIamportSubscription.IStore, + ); + + const pg_id: string = v4(); + const payment: IIamportCardPayment = { + card_code: v4(), + card_name: AdvancedRandomGenerator.name(), + card_number: input.card_number, + card_quota: input.card_quota || 0, + apply_num: v4(), + + // ORDER INFO + pay_method: "card", + currency: input.currency || "KRW", + merchant_uid: input.merchant_uid, + imp_uid: v4(), + name: input.name, + amount: input.amount, + cancel_amount: 0, + receipt_url: "https://github.com/samchon/fake-iamport-server", + cash_receipt_issue: true, + + // PAYMENT PROVIDER INFO + channel: Math.random() < 0.5 ? "pc" : "mobile", + pg_provider: "somewhere", + emb_pg_provider: null, + pg_id, + pg_tid: pg_id, + escrow: false, + + // BUYER + buyer_name: input.buyer_name || null, + buyer_tel: input.buyer_tel || null, + buyer_email: input.buyer_email || null, + buyer_addr: input.buyer_addr || null, + buyer_postcode: input.buyer_postcode || null, + customer_uid: input.customer_uid || null, + customer_uid_usage: input.customer_uid ? "issue" : null, + custom_data: input.custom_data || null, + user_agent: "Test Automation", + + // TIMESTAMPS + status: "paid", + started_at: Date.now() / 1000, + paid_at: Date.now() / 1000, + failed_at: 0, + fail_reason: null, + cancelled_at: 0, + cancel_reason: null, + cancel_history: [], + + // HIDDEN + notice_url: input.notice_url, + }; + FakeIamportPaymentProvider.store(payment); + + return FakeIamportResponseProvider.success(payment); + } + + /** + * 간편 결제에 등록된 카드로 결제하기. + * + * `subscribe.payments.again` 은 고객이 간편 결제에 등록한 카드로 결제를 진행하고자 할 때 + * 호출하는 API 함수이다. 이는 간편하고 불편하고를 떠나, 본질적으로 카드 결제의 일환이기에, + * 리턴값은 일반적인 카드 결제 때와 동일한 {@link IIamportCardPayment} 이다. + * + * 그리고 `subscribe.payments.again` 은 결제 수단 중 유일하게, 클라이언트 어플리케이션이 + * 아임포트가 제공하는 결체 창을 사용할 수 없어, 오직 귀하의 백엔드 서버가 아임포트의 API + * 함수를 직접 호출해야하는 경우에 해당한다. 따라서 간편 결제에 관하여 아임포트 서버와 + * 연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 대한 별도의 + * 설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다. + * + * @param input 미리 등록한 카드를 이용한 결제 신청 정보 + * @returns 카드 결제 정보 + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Post("again") + public again( + @FakeIamportUserAuth() _user: IIamportUser.IAccessor, + @core.TypedBody() input: IIamportSubscription.IAgain, + ): IIamportResponse { + const subscription: IIamportSubscription = + FakeIamportStorage.subscriptions.get(input.customer_uid); + + const pg_id: string = v4(); + const payment: IIamportCardPayment = { + card_code: subscription.card_code, + card_name: subscription.card_name, + card_number: subscription.card_number, + card_quota: input.card_quota || 0, + apply_num: v4(), + + // ORDER INFO + pay_method: "card", + currency: input.currency || "KRW", + merchant_uid: input.merchant_uid, + imp_uid: v4(), + name: input.name, + amount: input.amount, + cancel_amount: 0, + receipt_url: "https://github.com/samchon/fake-iamport-server", + cash_receipt_issue: true, + + // PAYMENT PROVIDER INFO + channel: Math.random() < 0.5 ? "pc" : "mobile", + pg_provider: "somewhere", + emb_pg_provider: null, + pg_id, + pg_tid: pg_id, + escrow: false, + + // BUYER + buyer_name: input.buyer_name || subscription.customer_name || null, + buyer_tel: input.buyer_tel || subscription.customer_tel || null, + buyer_email: input.buyer_email || subscription.customer_email || null, + buyer_addr: input.buyer_addr || subscription.customer_addr || null, + buyer_postcode: + input.buyer_postcode || subscription.customer_postcode || null, + customer_uid: subscription.customer_uid, + customer_uid_usage: "issue", + custom_data: input.custom_data || null, + user_agent: "Test Automation", + + // TIMESTAMPS + status: "paid", + started_at: Date.now() / 1000, + paid_at: Date.now() / 1000, + failed_at: 0, + fail_reason: null, + cancelled_at: 0, + cancel_reason: null, + cancel_history: [], + + // HIDDEN + notice_url: input.notice_url, + }; + FakeIamportPaymentProvider.store(payment); + + return FakeIamportResponseProvider.success(payment); + } } diff --git a/packages/fake-iamport-server/src/decorators/FakeIamportUserAuth.ts b/packages/fake-iamport-server/src/decorators/FakeIamportUserAuth.ts new file mode 100644 index 0000000..1efb2b7 --- /dev/null +++ b/packages/fake-iamport-server/src/decorators/FakeIamportUserAuth.ts @@ -0,0 +1,51 @@ +import { + ExecutionContext, + ForbiddenException, + createParamDecorator, +} from "@nestjs/common"; +import { FastifyRequest } from "fastify"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; +import { Singleton } from "tstl"; +import { v4 } from "uuid"; + +import { FakeIamportConfiguration } from "../FakeIamportConfiguration"; +import { FakeIamportStorage } from "../providers/FakeIamportStorage"; + +export function FakeIamportUserAuth() { + return singleton.get()(); +} +export namespace FakeIamportUserAuth { + export function issue(accessor: IIamportUser.IAccessor): IIamportUser { + if (FakeIamportConfiguration.authorize(accessor) === false) + throw new ForbiddenException("Wrong authorization key values."); + + const user: IIamportUser = { + now: Date.now() / 1_000, + expired_at: + (Date.now() + FakeIamportConfiguration.USER_EXPIRATION_TIME) / 1_000, + access_token: v4(), + }; + FakeIamportStorage.users.set(user.access_token, user); + + return user; + } + + export function authorize(request: FastifyRequest): IIamportUser { + const token: string | undefined = request.headers.authorization; + if (token === undefined) + throw new ForbiddenException("No authorization token exists."); + + const user: IIamportUser = FakeIamportStorage.users.get(token); + if (new Date(user.expired_at * 1_000).getTime() > Date.now()) + throw new ForbiddenException("The token has been expired."); + + return user; + } +} + +const singleton = new Singleton(() => + createParamDecorator(async (_0: any, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return FakeIamportUserAuth.authorize(request); + }), +); diff --git a/packages/fake-iamport-server/src/executable/server.ts b/packages/fake-iamport-server/src/executable/server.ts index 7112e92..16b2e96 100644 --- a/packages/fake-iamport-server/src/executable/server.ts +++ b/packages/fake-iamport-server/src/executable/server.ts @@ -9,51 +9,51 @@ const EXTENSION = __filename.substr(-2); if (EXTENSION === "js") require("source-map-support/register"); const directory = new Singleton(async () => { - await mkdir(`${__dirname}/../../assets`); - await mkdir(`${__dirname}/../../assets/logs`); - await mkdir(`${__dirname}/../../assets/logs/errors`); + await mkdir(`${__dirname}/../../assets`); + await mkdir(`${__dirname}/../../assets/logs`); + await mkdir(`${__dirname}/../../assets/logs/errors`); }); function cipher(val: number): string { - if (val < 10) return "0" + val; - else return String(val); + if (val < 10) return "0" + val; + else return String(val); } async function mkdir(path: string): Promise { - try { - await fs.promises.mkdir(path); - } catch {} + try { + await fs.promises.mkdir(path); + } catch {} } async function handle_error(exp: any): Promise { - try { - const date: Date = new Date(); - const fileName: string = `${date.getFullYear()}${cipher( - date.getMonth() + 1, - )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( - date.getMinutes(), - )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; - const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); - - await directory.get(); - await fs.promises.writeFile( - `${__dirname}/../../assets/logs/errors/${fileName}.log`, - content, - "utf8", - ); - } catch {} + try { + const date: Date = new Date(); + const fileName: string = `${date.getFullYear()}${cipher( + date.getMonth() + 1, + )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( + date.getMinutes(), + )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; + const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); + + await directory.get(); + await fs.promises.writeFile( + `${__dirname}/../../assets/logs/errors/${fileName}.log`, + content, + "utf8", + ); + } catch {} } async function main(): Promise { - // BACKEND SEVER LATER - const backend: FakeIamportBackend = new FakeIamportBackend(); - await backend.open(); + // BACKEND SEVER LATER + const backend: FakeIamportBackend = new FakeIamportBackend(); + await backend.open(); - // UNEXPECTED ERRORS - global.process.on("uncaughtException", handle_error); - global.process.on("unhandledRejection", handle_error); + // UNEXPECTED ERRORS + global.process.on("uncaughtException", handle_error); + global.process.on("unhandledRejection", handle_error); } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts b/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts index 9c46754..66d1613 100644 --- a/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts +++ b/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts @@ -7,77 +7,72 @@ import { FakeIamportConfiguration } from "../FakeIamportConfiguration"; import { FakeIamportStorage } from "./FakeIamportStorage"; export namespace FakeIamportPaymentProvider { - export function store(payment: IIamportPayment): void { - FakeIamportStorage.payments.set(payment.imp_uid, payment); - webhook(payment).catch(() => {}); - } + export function store(payment: IIamportPayment): void { + FakeIamportStorage.payments.set(payment.imp_uid, payment); + webhook(payment).catch(() => {}); + } - export function deposit(payment: IIamportVBankPayment): void { - payment.status = "paid"; - payment.paid_at = Date.now() / 1_000; - webhook(payment).catch(() => {}); - } + export function deposit(payment: IIamportVBankPayment): void { + payment.status = "paid"; + payment.paid_at = Date.now() / 1_000; + webhook(payment).catch(() => {}); + } - export function cancel( - payment: IIamportPayment, - input: IIamportPaymentCancel.IStore, - ): void { - // VALIDATION - if ( - payment.pay_method === "vbank" && - (!input.refund_holder || - !input.refund_bank || - !input.refund_account) - ) - throw new DomainError( - "가상계좌 취소는 계좌번호, 예금주, 은행명을 입력해야 합니다.", - ); - else if ( - typeof input.amount === "number" && - input.amount > payment.amount - payment.cancel_amount - ) - throw new DomainError( - "취소 금액은 결제 또는 잔여 금액보다 클 수 없습니다.", - ); - else if (!payment.cancel_amount) payment.cancel_amount = 0; + export function cancel( + payment: IIamportPayment, + input: IIamportPaymentCancel.IStore, + ): void { + // VALIDATION + if ( + payment.pay_method === "vbank" && + (!input.refund_holder || !input.refund_bank || !input.refund_account) + ) + throw new DomainError( + "가상계좌 취소는 계좌번호, 예금주, 은행명을 입력해야 합니다.", + ); + else if ( + typeof input.amount === "number" && + input.amount > payment.amount - payment.cancel_amount + ) + throw new DomainError( + "취소 금액은 결제 또는 잔여 금액보다 클 수 없습니다.", + ); + else if (!payment.cancel_amount) payment.cancel_amount = 0; - input.amount ??= payment.amount - payment.cancel_amount; + input.amount ??= payment.amount - payment.cancel_amount; - // ARCHIVE CANCEL HISTORY - payment.cancel_amount += input.amount; - payment.cancel_reason = input.reason; - payment.cancelled_at = Date.now() / 1_000; - payment.cancel_history.push({ - pg_id: payment.pg_id, - pg_tid: payment.pg_tid, - amount: input.amount, - cancelled_at: Date.now() / 1_000, - reason: input.reason, - receipt_url: payment.receipt_url, - }); + // ARCHIVE CANCEL HISTORY + payment.cancel_amount += input.amount; + payment.cancel_reason = input.reason; + payment.cancelled_at = Date.now() / 1_000; + payment.cancel_history.push({ + pg_id: payment.pg_id, + pg_tid: payment.pg_tid, + amount: input.amount, + cancelled_at: Date.now() / 1_000, + reason: input.reason, + receipt_url: payment.receipt_url, + }); - // INFORM THE EVENT - payment.status = "cancelled"; - webhook(payment).catch(() => {}); - } + // INFORM THE EVENT + payment.status = "cancelled"; + webhook(payment).catch(() => {}); + } - export async function webhook(payment: IIamportPayment): Promise { - const webhook: IIamportPayment.IWebhook = { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - status: payment.status, - }; - FakeIamportStorage.webhooks.set(webhook.imp_uid, webhook); + export async function webhook(payment: IIamportPayment): Promise { + const webhook: IIamportPayment.IWebhook = { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + status: payment.status, + }; + FakeIamportStorage.webhooks.set(webhook.imp_uid, webhook); - await fetch( - payment.notice_url || FakeIamportConfiguration.WEBHOOK_URL, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(webhook), - }, - ); - } + await fetch(payment.notice_url || FakeIamportConfiguration.WEBHOOK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(webhook), + }); + } } diff --git a/packages/fake-iamport-server/src/providers/FakeIamportResponseProvider.ts b/packages/fake-iamport-server/src/providers/FakeIamportResponseProvider.ts index 8bf0fa0..08d2ebd 100644 --- a/packages/fake-iamport-server/src/providers/FakeIamportResponseProvider.ts +++ b/packages/fake-iamport-server/src/providers/FakeIamportResponseProvider.ts @@ -1,13 +1,11 @@ import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; export namespace FakeIamportResponseProvider { - export function success( - response: T, - ): IIamportResponse { - return { - code: 0, - message: "success", - response, - }; - } + export function success(response: T): IIamportResponse { + return { + code: 0, + message: "success", + response, + }; + } } diff --git a/packages/fake-iamport-server/src/providers/FakeIamportStorage.ts b/packages/fake-iamport-server/src/providers/FakeIamportStorage.ts index 0a9f4b7..b17f805 100644 --- a/packages/fake-iamport-server/src/providers/FakeIamportStorage.ts +++ b/packages/fake-iamport-server/src/providers/FakeIamportStorage.ts @@ -8,17 +8,19 @@ import { FakeIamportConfiguration } from "../FakeIamportConfiguration"; import { VolatileMap } from "../utils/VolatileMap"; export namespace FakeIamportStorage { - export const certifications: VolatileMap = - new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); - export const payments: VolatileMap = - new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); - export const receipts: VolatileMap = - new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); - export const subscriptions: VolatileMap = - new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); - export const users: VolatileMap = new VolatileMap( - FakeIamportConfiguration.STORAGE_EXPIRATION, - ); - export const webhooks: VolatileMap = - new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); + export const certifications: VolatileMap = + new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); + export const payments: VolatileMap = new VolatileMap( + FakeIamportConfiguration.STORAGE_EXPIRATION, + ); + export const receipts: VolatileMap = new VolatileMap( + FakeIamportConfiguration.STORAGE_EXPIRATION, + ); + export const subscriptions: VolatileMap = + new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); + export const users: VolatileMap = new VolatileMap( + FakeIamportConfiguration.STORAGE_EXPIRATION, + ); + export const webhooks: VolatileMap = + new VolatileMap(FakeIamportConfiguration.STORAGE_EXPIRATION); } diff --git a/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts b/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts index d6e74c0..c9ece2a 100644 --- a/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts +++ b/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts @@ -5,29 +5,28 @@ import { AdvancedRandomGenerator } from "../utils/AdvancedRandomGenerator"; import { FakeIamportStorage } from "./FakeIamportStorage"; export namespace FakeIamportSubscriptionProvider { - export function store( - customer_uid: string, - input: IIamportSubscription.IStore, - ): IIamportSubscription { - const subscription: IIamportSubscription = { - customer_uid, - pg_provider: "pg-of-somewhere", - pg_id: v4(), - card_type: "card", - card_code: v4(), - card_name: AdvancedRandomGenerator.name(), - card_number: input.card_number, - customer_name: AdvancedRandomGenerator.name(), - customer_tel: AdvancedRandomGenerator.mobile(), - customer_addr: "address-of-somewhere", - customer_email: - AdvancedRandomGenerator.alphabets(8) + "@samchon.org", - customer_postcode: "11122", - inserted: 1, - updated: 0, - }; - FakeIamportStorage.subscriptions.set(customer_uid, subscription); + export function store( + customer_uid: string, + input: IIamportSubscription.IStore, + ): IIamportSubscription { + const subscription: IIamportSubscription = { + customer_uid, + pg_provider: "pg-of-somewhere", + pg_id: v4(), + card_type: "card", + card_code: v4(), + card_name: AdvancedRandomGenerator.name(), + card_number: input.card_number, + customer_name: AdvancedRandomGenerator.name(), + customer_tel: AdvancedRandomGenerator.mobile(), + customer_addr: "address-of-somewhere", + customer_email: AdvancedRandomGenerator.alphabets(8) + "@samchon.org", + customer_postcode: "11122", + inserted: 1, + updated: 0, + }; + FakeIamportStorage.subscriptions.set(customer_uid, subscription); - return subscription; - } + return subscription; + } } diff --git a/packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts b/packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts deleted file mode 100644 index 7779ca1..0000000 --- a/packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; -import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; -import { v4 } from "uuid"; - -import { FakeIamportConfiguration } from "../FakeIamportConfiguration"; -import { FakeIamportStorage } from "./FakeIamportStorage"; - -export namespace FakeIamportUserAuth { - export function issue(accessor: IIamportUser.IAccessor): IIamportUser { - if (FakeIamportConfiguration.authorize(accessor) === false) - throw new nest.ForbiddenException( - "Wrong authorization key values.", - ); - - const user: IIamportUser = { - now: Date.now() / 1_000, - expired_at: - (Date.now() + FakeIamportConfiguration.USER_EXPIRATION_TIME) / - 1_000, - access_token: v4(), - }; - FakeIamportStorage.users.set(user.access_token, user); - - return user; - } - - export function authorize(request: fastify.FastifyRequest): void { - const token: string | undefined = request.headers.authorization; - if (token === undefined) - throw new nest.ForbiddenException("No authorization token exists."); - - const user: IIamportUser = FakeIamportStorage.users.get(token); - if (new Date(user.expired_at * 1_000).getTime() > Date.now()) - throw new nest.ForbiddenException("The token has been expired."); - } -} diff --git a/packages/fake-iamport-server/src/utils/AdvancedRandomGenerator.ts b/packages/fake-iamport-server/src/utils/AdvancedRandomGenerator.ts index 1b25195..b289e9b 100644 --- a/packages/fake-iamport-server/src/utils/AdvancedRandomGenerator.ts +++ b/packages/fake-iamport-server/src/utils/AdvancedRandomGenerator.ts @@ -2,15 +2,15 @@ import { RandomGenerator } from "@nestia/e2e"; import { randint } from "tstl/algorithm/random"; export const AdvancedRandomGenerator = { - ...RandomGenerator, - name: (length: number = 3) => - Array(length) - .fill("") - .map(() => String.fromCharCode(randint(44031, 55203))) - .join(""), - cardNumber: () => - new Array(4) - .fill("") - .map(() => randint(0, 9999).toString().padStart(4, "0")) - .join("-"), + ...RandomGenerator, + name: (length: number = 3) => + Array(length) + .fill("") + .map(() => String.fromCharCode(randint(44031, 55203))) + .join(""), + cardNumber: () => + new Array(4) + .fill("") + .map(() => randint(0, 9999).toString().padStart(4, "0")) + .join("-"), }; diff --git a/packages/fake-iamport-server/src/utils/DateUtil.ts b/packages/fake-iamport-server/src/utils/DateUtil.ts index e9577e4..477344f 100644 --- a/packages/fake-iamport-server/src/utils/DateUtil.ts +++ b/packages/fake-iamport-server/src/utils/DateUtil.ts @@ -1,129 +1,123 @@ export namespace DateUtil { - export const SECOND = 1_000; - export const MINUTE = 60 * SECOND; - export const HOUR = 60 * MINUTE; - export const DAY = 24 * HOUR; - export const WEEK = 7 * DAY; - export const MONTH = 30 * DAY; - - export function to_string(date: Date, hms: boolean = false): string { - const ymd: string = [ - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - ] - .map((value) => _To_cipher_string(value)) - .join("-"); - if (hms === false) return ymd; - - return ( - `${ymd} ` + - [date.getHours(), date.getMinutes(), date.getSeconds()] - .map((value) => _To_cipher_string(value)) - .join(":") - ); - } - - export function to_uuid(date: Date = new Date()): string { - const elements: number[] = [ - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds(), - ]; - return ( - elements.map((value) => _To_cipher_string(value)).join("") + - "-" + - Math.random().toString().substring(4) - ); - } - - export interface IDifference { - year: number; - month: number; - date: number; - } - - export function diff(x: Date | string, y: Date | string): IDifference { - x = _To_date(x); - y = _To_date(y); - - // FIRST DIFFERENCES - const ret: IDifference = { - year: x.getFullYear() - y.getFullYear(), - month: x.getMonth() - y.getMonth(), - date: x.getDate() - y.getDate(), - }; - - //---- - // HANDLE NEGATIVE ELEMENTS - //---- - // DATE - if (ret.date < 0) { - const last: number = last_date(y.getFullYear(), y.getMonth()); - - --ret.month; - ret.date = x.getDate() + (last - y.getDate()); - } - - // MONTH - if (ret.month < 0) { - --ret.year; - ret.month = 12 + ret.month; - } - return ret; - } - - export function last_date(year: number, month: number): number { - // LEAP MONTH - if ( - month == 1 && - year % 4 == 0 && - !(year % 100 == 0 && year % 400 != 0) - ) - return 29; - else return LAST_DATES[month]; - } - - export function add_years(date: Date, value: number): Date { - date = new Date(date); - date.setFullYear(date.getFullYear() + value); - - return date; - } - - export function add_months(date: Date, value: number): Date { - date = new Date(date); - - const newYear: number = - date.getFullYear() + Math.floor((date.getMonth() + value) / 12); - const newMonth: number = (date.getMonth() + value) % 12; - const lastDate: number = last_date(newYear, newMonth - 1); - - if (lastDate < date.getDate()) date.setDate(lastDate); - - date.setMonth(value - 1); - return date; - } - - export function add_days(date: Date, value: number): Date { - date = new Date(); - date.setDate(date.getDate() + value); - - return date; + export const SECOND = 1_000; + export const MINUTE = 60 * SECOND; + export const HOUR = 60 * MINUTE; + export const DAY = 24 * HOUR; + export const WEEK = 7 * DAY; + export const MONTH = 30 * DAY; + + export function to_string(date: Date, hms: boolean = false): string { + const ymd: string = [ + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + ] + .map((value) => _To_cipher_string(value)) + .join("-"); + if (hms === false) return ymd; + + return ( + `${ymd} ` + + [date.getHours(), date.getMinutes(), date.getSeconds()] + .map((value) => _To_cipher_string(value)) + .join(":") + ); + } + + export function to_uuid(date: Date = new Date()): string { + const elements: number[] = [ + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + ]; + return ( + elements.map((value) => _To_cipher_string(value)).join("") + + "-" + + Math.random().toString().substring(4) + ); + } + + export interface IDifference { + year: number; + month: number; + date: number; + } + + export function diff(x: Date | string, y: Date | string): IDifference { + x = _To_date(x); + y = _To_date(y); + + // FIRST DIFFERENCES + const ret: IDifference = { + year: x.getFullYear() - y.getFullYear(), + month: x.getMonth() - y.getMonth(), + date: x.getDate() - y.getDate(), + }; + + //---- + // HANDLE NEGATIVE ELEMENTS + //---- + // DATE + if (ret.date < 0) { + const last: number = last_date(y.getFullYear(), y.getMonth()); + + --ret.month; + ret.date = x.getDate() + (last - y.getDate()); } - function _To_date(date: string | Date): Date { - if (date instanceof Date) return date; - else return new Date(date); + // MONTH + if (ret.month < 0) { + --ret.year; + ret.month = 12 + ret.month; } - function _To_cipher_string(val: number): string { - if (val < 10) return "0" + val; - else return String(val); - } - const LAST_DATES: number[] = [ - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - ]; + return ret; + } + + export function last_date(year: number, month: number): number { + // LEAP MONTH + if (month == 1 && year % 4 == 0 && !(year % 100 == 0 && year % 400 != 0)) + return 29; + else return LAST_DATES[month]; + } + + export function add_years(date: Date, value: number): Date { + date = new Date(date); + date.setFullYear(date.getFullYear() + value); + + return date; + } + + export function add_months(date: Date, value: number): Date { + date = new Date(date); + + const newYear: number = + date.getFullYear() + Math.floor((date.getMonth() + value) / 12); + const newMonth: number = (date.getMonth() + value) % 12; + const lastDate: number = last_date(newYear, newMonth - 1); + + if (lastDate < date.getDate()) date.setDate(lastDate); + + date.setMonth(value - 1); + return date; + } + + export function add_days(date: Date, value: number): Date { + date = new Date(); + date.setDate(date.getDate() + value); + + return date; + } + + function _To_date(date: string | Date): Date { + if (date instanceof Date) return date; + else return new Date(date); + } + function _To_cipher_string(val: number): string { + if (val < 10) return "0" + val; + else return String(val); + } + const LAST_DATES: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; } diff --git a/packages/fake-iamport-server/src/utils/ErrorUtil.ts b/packages/fake-iamport-server/src/utils/ErrorUtil.ts index d187213..c4ca025 100644 --- a/packages/fake-iamport-server/src/utils/ErrorUtil.ts +++ b/packages/fake-iamport-server/src/utils/ErrorUtil.ts @@ -1,9 +1,9 @@ import serializeError from "serialize-error"; export namespace ErrorUtil { - export function toJSON(err: any): object { - return err instanceof Object && err.toJSON instanceof Function - ? err.toJSON() - : serializeError(err); - } + export function toJSON(err: any): object { + return err instanceof Object && err.toJSON instanceof Function + ? err.toJSON() + : serializeError(err); + } } diff --git a/packages/fake-iamport-server/src/utils/Terminal.ts b/packages/fake-iamport-server/src/utils/Terminal.ts index c7174aa..c7fe808 100644 --- a/packages/fake-iamport-server/src/utils/Terminal.ts +++ b/packages/fake-iamport-server/src/utils/Terminal.ts @@ -2,17 +2,17 @@ import cp from "child_process"; import { Pair } from "tstl/utility/Pair"; export namespace Terminal { - export function execute( - ...commands: string[] - ): Promise> { - return new Promise((resolve, reject) => { - cp.exec( - commands.join(" && "), - (error: Error | null, stdout: string, stderr: string) => { - if (error) reject(error); - else resolve(new Pair(stdout, stderr)); - }, - ); - }); - } + export function execute( + ...commands: string[] + ): Promise> { + return new Promise((resolve, reject) => { + cp.exec( + commands.join(" && "), + (error: Error | null, stdout: string, stderr: string) => { + if (error) reject(error); + else resolve(new Pair(stdout, stderr)); + }, + ); + }); + } } diff --git a/packages/fake-iamport-server/src/utils/VolatileMap.ts b/packages/fake-iamport-server/src/utils/VolatileMap.ts index eb9244a..cea87d0 100644 --- a/packages/fake-iamport-server/src/utils/VolatileMap.ts +++ b/packages/fake-iamport-server/src/utils/VolatileMap.ts @@ -5,82 +5,82 @@ import { equal_to } from "tstl/functional/comparators"; import { hash } from "tstl/functional/hash"; export class VolatileMap { - private readonly dict_: HashMap; - private readonly timepoints_: TreeMap; + private readonly dict_: HashMap; + private readonly timepoints_: TreeMap; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- CONSTRUCTORS ----------------------------------------------------------- */ - public constructor( - public readonly expiration: VolatileMap.IExpiration, - hasher: (key: Key) => number = hash, - pred: (x: Key, y: Key) => boolean = equal_to, - ) { - this.dict_ = new HashMap(hasher, pred); - this.timepoints_ = new TreeMap(); - } + public constructor( + public readonly expiration: VolatileMap.IExpiration, + hasher: (key: Key) => number = hash, + pred: (x: Key, y: Key) => boolean = equal_to, + ) { + this.dict_ = new HashMap(hasher, pred); + this.timepoints_ = new TreeMap(); + } - public clear(): void { - this.dict_.clear(); - this.timepoints_.clear(); - } + public clear(): void { + this.dict_.clear(); + this.timepoints_.clear(); + } - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- ACCESSORS ----------------------------------------------------------- */ - public size(): number { - return this.dict_.size(); - } + public size(): number { + return this.dict_.size(); + } - public get(key: Key): T { - return this.dict_.get(key); - } + public get(key: Key): T { + return this.dict_.get(key); + } - public has(key: Key): boolean { - return this.dict_.has(key); - } + public has(key: Key): boolean { + return this.dict_.has(key); + } - public back(): T { - if (this.size() === 0) throw new OutOfRange("No element exists."); - return this.dict_.rbegin().second; - } + public back(): T { + if (this.size() === 0) throw new OutOfRange("No element exists."); + return this.dict_.rbegin().second; + } - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- ELEMENTS I/O ----------------------------------------------------------- */ - public set(key: Key, value: T): void { - this._Clean_up(); - - this.dict_.set(key, value); - this.timepoints_.set(Date.now(), key); - } + public set(key: Key, value: T): void { + this._Clean_up(); - private _Clean_up(): void { - const bound: number = Date.now() - this.expiration.time; - const last: TreeMap.Iterator = - this.timepoints_.upper_bound(bound); + this.dict_.set(key, value); + this.timepoints_.set(Date.now(), key); + } - for (let it = this.timepoints_.begin(); it.equals(last) === false; ) { - this.dict_.erase(it.second); - it = this.timepoints_.erase(it); - } - if (this.timepoints_.size() < this.expiration.capacity) return; + private _Clean_up(): void { + const bound: number = Date.now() - this.expiration.time; + const last: TreeMap.Iterator = + this.timepoints_.upper_bound(bound); - let left: number = this.timepoints_.size() - this.expiration.capacity; - while (left-- === 0) { - const it: TreeMap.Iterator = this.timepoints_.begin(); - this.dict_.erase(it.second); - this.timepoints_.erase(it); - } + for (let it = this.timepoints_.begin(); it.equals(last) === false; ) { + this.dict_.erase(it.second); + it = this.timepoints_.erase(it); } + if (this.timepoints_.size() < this.expiration.capacity) return; - public erase(key: Key): number { - return this.dict_.erase(key); + let left: number = this.timepoints_.size() - this.expiration.capacity; + while (left-- === 0) { + const it: TreeMap.Iterator = this.timepoints_.begin(); + this.dict_.erase(it.second); + this.timepoints_.erase(it); } + } + + public erase(key: Key): number { + return this.dict_.erase(key); + } } export namespace VolatileMap { - export interface IExpiration { - time: number; - capacity: number; - } + export interface IExpiration { + time: number; + capacity: number; + } } diff --git a/packages/fake-iamport-server/test/features/test_fake_card_payment.ts b/packages/fake-iamport-server/test/features/test_fake_card_payment.ts index 5cc7c58..29580c7 100644 --- a/packages/fake-iamport-server/test/features/test_fake_card_payment.ts +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment.ts @@ -10,69 +10,68 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; export async function test_fake_card_payment( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - /** - * 결제 요청 레코드 발행하기. - * - * 아임포트의 경우 {@link subscribe.payments.onetime} 을 이용하면, API 만을 - * 가지고도 카드 결제를 진행할 수 있다. 그리고 이 때, *input* 값에서 - * {@link IIamportSubscription.IOnetime.customer_uid} 를 빼 버리면, 해당 카드가 - * 간편 결제용으로 등록되지도 아니함. - * - * 반대로 *input* 값에 {@link IIamportSubscription.IOnetime.customer_uid} 값을 - * 채워넣으면, 결제가 완료됨과 동시에 해당 카드가간편 결제용으로 등록된다. - */ - const reply: IIamportResponse = - await imp.functional.subscribe.payments.onetime(await connector.get(), { - card_number: AdvancedRandomGenerator.cardNumber(), - expiry: "2028-12", - birth: "880311", + /** + * 결제 요청 레코드 발행하기. + * + * 아임포트의 경우 {@link subscribe.payments.onetime} 을 이용하면, API 만을 + * 가지고도 카드 결제를 진행할 수 있다. 그리고 이 때, *input* 값에서 + * {@link IIamportSubscription.IOnetime.customer_uid} 를 빼 버리면, 해당 카드가 + * 간편 결제용으로 등록되지도 아니함. + * + * 반대로 *input* 값에 {@link IIamportSubscription.IOnetime.customer_uid} 값을 + * 채워넣으면, 결제가 완료됨과 동시에 해당 카드가간편 결제용으로 등록된다. + */ + const reply: IIamportResponse = + await imp.functional.subscribe.payments.onetime(await connector.get(), { + card_number: AdvancedRandomGenerator.cardNumber(), + expiry: "2028-12", + birth: "880311", - merchant_uid: v4(), - amount: 25_000, - name: "Fake 주문", - }); - typia.assert(reply); + merchant_uid: v4(), + amount: 25_000, + name: "Fake 주문", + }); + typia.assert(reply); - /** - * 아임포트 서버로부터의 웹훅 데이터. - * - * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 - * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. - */ - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)( - reply.response.imp_uid, - ); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid)( - reply.response.merchant_uid, - ); - TestValidator.equals("webhook.status")(webhook.status)("paid"); - - /** - * 결제 내역 조회하기. - * - * 위에서 발행한 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 서버로부터 - * {@link payments.at} API 함수를 호출하여 재 조회해보면, 카드 결제가 무사히 - * 완료되었음을, 그리고 관련 결제 정보 {@link IIamportCardPayment} 정보가 완전하게 - * 구성되었음을 알 수 있다. - */ - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - webhook.imp_uid, - {}, - ); - typia.assert(reloaded); + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. + */ + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)( + reply.response.imp_uid, + ); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid)( + reply.response.merchant_uid, + ); + TestValidator.equals("webhook.status")(webhook.status)("paid"); - // 결제 방식 및 완료 여부 확인 - const payment: IIamportCardPayment = typia.assert( - reloaded.response, + /** + * 결제 내역 조회하기. + * + * 위에서 발행한 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 서버로부터 + * {@link payments.at} API 함수를 호출하여 재 조회해보면, 카드 결제가 무사히 + * 완료되었음을, 그리고 관련 결제 정보 {@link IIamportCardPayment} 정보가 완전하게 + * 구성되었음을 알 수 있다. + */ + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + webhook.imp_uid, + {}, ); - TestValidator.predicate("paid")( - () => payment.status === "paid" && payment.paid_at !== 0, - ); - return payment; + typia.assert(reloaded); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportCardPayment = typia.assert( + reloaded.response, + ); + TestValidator.predicate("paid")( + () => payment.status === "paid" && payment.paid_at !== 0, + ); + return payment; } diff --git a/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel.ts b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel.ts index 97467fa..22cc0a1 100644 --- a/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel.ts +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel.ts @@ -9,61 +9,58 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { test_fake_card_payment } from "./test_fake_card_payment"; export async function test_fake_card_payment_cancel( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 카드 결제하기 - const payment: IIamportCardPayment = await test_fake_card_payment( - connector, - ); + // 카드 결제하기 + const payment: IIamportCardPayment = await test_fake_card_payment(connector); - // 검증 로직 준비 - const validate = (cancelled: boolean) => (p: IIamportPayment) => { - TestValidator.equals("cancel_amount")(p.cancel_amount)( - cancelled ? payment.amount : 0, - ); - TestValidator.predicate("cancelled_at")( - () => !!p.cancelled_at === cancelled, - ); - TestValidator.predicate("cancel_history")( - cancelled - ? () => - p.cancel_history.length === 1 && - p.cancel_history[0].amount === payment.amount - : () => p.cancel_history.length === 0, - ); - }; - validate(false)(payment); + // 검증 로직 준비 + const validate = (cancelled: boolean) => (p: IIamportPayment) => { + TestValidator.equals("cancel_amount")(p.cancel_amount)( + cancelled ? payment.amount : 0, + ); + TestValidator.predicate("cancelled_at")( + () => !!p.cancelled_at === cancelled, + ); + TestValidator.predicate("cancel_history")( + cancelled + ? () => + p.cancel_history.length === 1 && + p.cancel_history[0].amount === payment.amount + : () => p.cancel_history.length === 0, + ); + }; + validate(false)(payment); - // 결제 취소하기 (전액) - const reply: IIamportResponse = - await imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount, - checksum: payment.amount, - reason: "테스트 결제 취소", - }); + // 결제 취소하기 (전액) + const reply: IIamportResponse = + await imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount, + checksum: payment.amount, + reason: "테스트 결제 취소", + }); - // 데이터 및 로직 검증 - typia.assert(reply); - typia.assert(reply.response); - validate(true)(reply.response); + // 데이터 및 로직 검증 + typia.assert(reply); + typia.assert(reply.response); + validate(true)(reply.response); - // 웹훅 검증 - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); - TestValidator.equals("webhook.status")(webhook.status)("cancelled"); + // 웹훅 검증 + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); + TestValidator.equals("webhook.status")(webhook.status)("cancelled"); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - typia.assert(reloaded.response); - validate(true)(reloaded.response); + // 결제 내역 재 조회하여 다시 검증 + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + typia.assert(reloaded.response); + validate(true)(reloaded.response); } diff --git a/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_over.ts b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_over.ts index 4ffcad1..53c4c15 100644 --- a/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_over.ts +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_over.ts @@ -8,32 +8,30 @@ import typia from "typia"; import { test_fake_card_payment } from "./test_fake_card_payment"; export async function test_fake_card_payment_cancel_over( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 카드 결제하기 - const payment: IIamportCardPayment = await test_fake_card_payment( - connector, - ); + // 카드 결제하기 + const payment: IIamportCardPayment = await test_fake_card_payment(connector); - // 결제 취소하기 (전액 + 100) - await TestValidator.error("over")(async () => - imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount + 100, - checksum: null, - reason: "테스트 결제 취소", - }), - ); + // 결제 취소하기 (전액 + 100) + await TestValidator.error("over")(async () => + imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount + 100, + checksum: null, + reason: "테스트 결제 취소", + }), + ); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - typia.assert(reloaded.response); - TestValidator.equals("cancel_amount")(reloaded.response.cancel_amount)(0); + // 결제 내역 재 조회하여 다시 검증 + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + typia.assert(reloaded.response); + TestValidator.equals("cancel_amount")(reloaded.response.cancel_amount)(0); } diff --git a/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_partial.ts b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_partial.ts index e3c0bd5..a2d014f 100644 --- a/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_partial.ts +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_partial.ts @@ -8,53 +8,48 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { test_fake_card_payment } from "./test_fake_card_payment"; export async function test_fake_card_payment_cancel_partial( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 카드 결제하기 - const payment: IIamportCardPayment = await test_fake_card_payment( - connector, - ); + // 카드 결제하기 + const payment: IIamportCardPayment = await test_fake_card_payment(connector); - // 검증 로직 준비 - const validate = - (count: number) => - async (p: IIamportPayment): Promise => { - // VALIDATE CANCELLED AMOUNT - const expected: number = (payment.amount / 5) * count; - TestValidator.equals("cancel_amount")(p.cancel_amount)(expected); - TestValidator.predicate("cancelled_at")( - () => !!p.cancelled_at === !!count, - ); - TestValidator.predicate("cancel_history")( - () => - p.cancel_history.length === count && - p.cancel_history - .map((h) => h.amount) - .reduce((a, b) => a + b, 0) === expected, - ); - if (count === 0) return; + // 검증 로직 준비 + const validate = + (count: number) => + async (p: IIamportPayment): Promise => { + // VALIDATE CANCELLED AMOUNT + const expected: number = (payment.amount / 5) * count; + TestValidator.equals("cancel_amount")(p.cancel_amount)(expected); + TestValidator.predicate("cancelled_at")( + () => !!p.cancelled_at === !!count, + ); + TestValidator.predicate("cancel_history")( + () => + p.cancel_history.length === count && + p.cancel_history.map((h) => h.amount).reduce((a, b) => a + b, 0) === + expected, + ); + if (count === 0) return; - // VALIDATE WEBHOOK - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)( - payment.imp_uid, - ); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); - TestValidator.equals("webhook.status")(webhook.status)("cancelled"); - }; - validate(0)(payment); + // VALIDATE WEBHOOK + const webhook: IIamportPayment.IWebhook = + FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); + TestValidator.equals("webhook.status")(webhook.status)("cancelled"); + }; + validate(0)(payment); - // 5 회에 걸쳐 분할 취소 하기 - await ArrayUtil.asyncRepeat(5)(async (i) => { - const reply: IIamportResponse = - await imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount / 5, - checksum: payment.amount - (payment.amount * i) / 5, - reason: `테스트 결제 취소 ${i}`, - }); - await validate(i + 1)(reply.response); - }); + // 5 회에 걸쳐 분할 취소 하기 + await ArrayUtil.asyncRepeat(5)(async (i) => { + const reply: IIamportResponse = + await imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount / 5, + checksum: payment.amount - (payment.amount * i) / 5, + reason: `테스트 결제 취소 ${i}`, + }); + await validate(i + 1)(reply.response); + }); } diff --git a/packages/fake-iamport-server/test/features/test_fake_certification.ts b/packages/fake-iamport-server/test/features/test_fake_certification.ts index cee19e0..e52b168 100644 --- a/packages/fake-iamport-server/test/features/test_fake_certification.ts +++ b/packages/fake-iamport-server/test/features/test_fake_certification.ts @@ -5,57 +5,57 @@ import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResp import typia from "typia"; export async function test_fake_certification( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - /** - * 본인인증 요청 시뮬레이션. - * - * 아임포트가 제공하는 본인인증 팝업창을 이용, 휴대폰 번호를 통한 본인인증을 진행한다. - */ - const accessor: IIamportResponse = - await imp.functional.certifications.otp.request(await connector.get(), { - name: "남정호", - phone: "01011112222", - birth: "19880311", - gender_digit: "1", - carrier: "LGT", - }); - typia.assert(accessor); + /** + * 본인인증 요청 시뮬레이션. + * + * 아임포트가 제공하는 본인인증 팝업창을 이용, 휴대폰 번호를 통한 본인인증을 진행한다. + */ + const accessor: IIamportResponse = + await imp.functional.certifications.otp.request(await connector.get(), { + name: "남정호", + phone: "01011112222", + birth: "19880311", + gender_digit: "1", + carrier: "LGT", + }); + typia.assert(accessor); - /** - * 본인인증 상세 레코드 조회. - * - * 고객의 휴대폰 번호로 본인인증을 위한 OTP 번호가 문자로 전송되었지만, 아직 고객은 - * 이를 아임포트 본인인증 창의 OTP 입력 폼에 기재하지 않았다. 이처럼 본인인증이 - * 완결되지 않은 건에 대해서도, 아임포트는 본인인증 내역을 조회할 수 있다. - * - * 하지만 이 경우 {@link IIamportCertification.certified} 값이 `false` 로 - * 명시되기에, 이를 검사하면, 해당 본인인증 건이 완결되지 않았음을 알 수 있다. - */ - const uncertified: IIamportResponse = - await imp.functional.certifications.at( - await connector.get(), - accessor.response.imp_uid, - ); - typia.assert(uncertified); - TestValidator.equals("not cerified")(uncertified.response.certified)(false); + /** + * 본인인증 상세 레코드 조회. + * + * 고객의 휴대폰 번호로 본인인증을 위한 OTP 번호가 문자로 전송되었지만, 아직 고객은 + * 이를 아임포트 본인인증 창의 OTP 입력 폼에 기재하지 않았다. 이처럼 본인인증이 + * 완결되지 않은 건에 대해서도, 아임포트는 본인인증 내역을 조회할 수 있다. + * + * 하지만 이 경우 {@link IIamportCertification.certified} 값이 `false` 로 + * 명시되기에, 이를 검사하면, 해당 본인인증 건이 완결되지 않았음을 알 수 있다. + */ + const uncertified: IIamportResponse = + await imp.functional.certifications.at( + await connector.get(), + accessor.response.imp_uid, + ); + typia.assert(uncertified); + TestValidator.equals("not cerified")(uncertified.response.certified)(false); - /** - * 본인인증 OTP 코드 입력 시뮬레이션. - * - * 고객이 아임포트 본인인증 창에 입력한 OTP 코드가 정확하다면, 해당 본인인증 건은 - * 완결되어 {@link IIamportCertification.certified} 값이 `true` 로 변한다. 이는 - * {@link functional.certifications.at} 메서드를 통하여 해당 건을 재 조회하여도 - * 동일한 바이다. - */ - const confirmed: IIamportResponse = - await imp.functional.certifications.otp.confirm( - await connector.get(), - accessor.response.imp_uid, - { - otp: uncertified.response.__otp!, - }, - ); - typia.assert(confirmed); - TestValidator.equals("cerified")(confirmed.response.certified)(true); + /** + * 본인인증 OTP 코드 입력 시뮬레이션. + * + * 고객이 아임포트 본인인증 창에 입력한 OTP 코드가 정확하다면, 해당 본인인증 건은 + * 완결되어 {@link IIamportCertification.certified} 값이 `true` 로 변한다. 이는 + * {@link functional.certifications.at} 메서드를 통하여 해당 건을 재 조회하여도 + * 동일한 바이다. + */ + const confirmed: IIamportResponse = + await imp.functional.certifications.otp.confirm( + await connector.get(), + accessor.response.imp_uid, + { + otp: uncertified.response.__otp!, + }, + ); + typia.assert(confirmed); + TestValidator.equals("cerified")(confirmed.response.certified)(true); } diff --git a/packages/fake-iamport-server/test/features/test_fake_receipt.ts b/packages/fake-iamport-server/test/features/test_fake_receipt.ts index 5c18de2..c8c1071 100644 --- a/packages/fake-iamport-server/test/features/test_fake_receipt.ts +++ b/packages/fake-iamport-server/test/features/test_fake_receipt.ts @@ -8,31 +8,31 @@ import typia from "typia"; import { test_fake_card_payment } from "./test_fake_card_payment"; export async function test_fake_receipt( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - const payment: IIamportPayment = await test_fake_card_payment(connector); - const output: IIamportResponse = - await imp.functional.receipts.store( - await connector.get(), - payment.imp_uid, - { - imp_uid: payment.imp_uid, - identifier: "8803111******", - identifier_type: "person", - buyer_name: "남정호", - buyer_tel: "010********", - }, - ); - typia.assert(output); - TestValidator.equals("imp_uid")(output.response.imp_uid)(payment.imp_uid); - TestValidator.equals("amount")(output.response.amount)(payment.amount); + const payment: IIamportPayment = await test_fake_card_payment(connector); + const output: IIamportResponse = + await imp.functional.receipts.store( + await connector.get(), + payment.imp_uid, + { + imp_uid: payment.imp_uid, + identifier: "8803111******", + identifier_type: "person", + buyer_name: "남정호", + buyer_tel: "010********", + }, + ); + typia.assert(output); + TestValidator.equals("imp_uid")(output.response.imp_uid)(payment.imp_uid); + TestValidator.equals("amount")(output.response.amount)(payment.amount); - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - TestValidator.equals("issue")(reloaded.response.cash_receipt_issue)(true); + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + TestValidator.equals("issue")(reloaded.response.cash_receipt_issue)(true); } diff --git a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_again.ts b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_again.ts index 303de9a..f732320 100644 --- a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_again.ts +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_again.ts @@ -10,82 +10,81 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; export async function test_fake_subscription_payment_again( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - /** - * 간편 결제 카드 등록을 위한 고객 식별자 키. - * - * 이는 전적으로 아임포트를 사용하는 서비스에서 발급하여 관리하며, - * 아임포트는 이를 고객 식별자 키라고 이름 지었지만, - * 실제 역할은 간편 결제로 등록한 카드 식별자 키로써 역할한다. - */ - const customer_uid: string = v4(); + /** + * 간편 결제 카드 등록을 위한 고객 식별자 키. + * + * 이는 전적으로 아임포트를 사용하는 서비스에서 발급하여 관리하며, + * 아임포트는 이를 고객 식별자 키라고 이름 지었지만, + * 실제 역할은 간편 결제로 등록한 카드 식별자 키로써 역할한다. + */ + const customer_uid: string = v4(); - /** - * 간편 결제 카드 등록하기. - * - * 아임포트에 고객의 카드를 간편 결제 카드로써 등록하면, 매번 결제시마다 - * 카드 정보를 반복 입력하는 일 없이, `customer_uid` 만을 사용하여 매우 - * 간단하게 결제할 수 있다. - */ - await imp.functional.subscribe.customers.store( - await connector.get(), - customer_uid, - { - customer_uid, - card_number: AdvancedRandomGenerator.cardNumber(), - expiry: "2028-12", - birth: "880311", - }, - ); - - /** - * 간편 결제 카드로 결제하기. - * - * `customer_uid` 만으로 간편하게 결제할 수 있다. - */ - const output: IIamportResponse = - await imp.functional.subscribe.payments.again(await connector.get(), { - customer_uid, - merchant_uid: v4(), - amount: 10_000, - name: "Fake 주문", - }); - typia.assert(output); + /** + * 간편 결제 카드 등록하기. + * + * 아임포트에 고객의 카드를 간편 결제 카드로써 등록하면, 매번 결제시마다 + * 카드 정보를 반복 입력하는 일 없이, `customer_uid` 만을 사용하여 매우 + * 간단하게 결제할 수 있다. + */ + await imp.functional.subscribe.customers.store( + await connector.get(), + customer_uid, + { + customer_uid, + card_number: AdvancedRandomGenerator.cardNumber(), + expiry: "2028-12", + birth: "880311", + }, + ); - /** - * 아임포트 서버로부터의 웹훅 데이터. - * - * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 - * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. - */ - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("imp_uid")(webhook.imp_uid)(output.response.imp_uid); - TestValidator.equals("status")(webhook.status)("paid"); + /** + * 간편 결제 카드로 결제하기. + * + * `customer_uid` 만으로 간편하게 결제할 수 있다. + */ + const output: IIamportResponse = + await imp.functional.subscribe.payments.again(await connector.get(), { + customer_uid, + merchant_uid: v4(), + amount: 10_000, + name: "Fake 주문", + }); + typia.assert(output); - /** - * 결제 내역 조회하기. - * - * 위에서 발행한 간편 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 - * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 카드 결제가 - * 무사히 완료되었음을, 그리고 관련 결제 정보 {@link IIamportCardPayment} 정보가 - * 완전하게 구성되었음을 알 수 있다. - */ - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - webhook.imp_uid, - {}, - ); - typia.assert(reloaded); + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. + */ + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("imp_uid")(webhook.imp_uid)(output.response.imp_uid); + TestValidator.equals("status")(webhook.status)("paid"); - // 결제 방식 및 완료 여부 확인 - const payment: IIamportCardPayment = typia.assert( - reloaded.response, + /** + * 결제 내역 조회하기. + * + * 위에서 발행한 간편 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 + * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 카드 결제가 + * 무사히 완료되었음을, 그리고 관련 결제 정보 {@link IIamportCardPayment} 정보가 + * 완전하게 구성되었음을 알 수 있다. + */ + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + webhook.imp_uid, + {}, ); - TestValidator.predicate("paid_at")(() => payment.paid_at !== 0); - TestValidator.equals("status")(payment.status)("paid"); + typia.assert(reloaded); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportCardPayment = typia.assert( + reloaded.response, + ); + TestValidator.predicate("paid_at")(() => payment.paid_at !== 0); + TestValidator.equals("status")(payment.status)("paid"); - return payment; + return payment; } diff --git a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel.ts b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel.ts index 03017ec..ca0fa64 100644 --- a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel.ts +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel.ts @@ -9,60 +9,59 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { test_fake_subscription_payment_again } from "./test_fake_subscription_payment_again"; export async function test_fake_subscription_payment_cancel( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 구독을 통한 결제하기 - const payment: IIamportCardPayment = - await test_fake_subscription_payment_again(connector); + // 구독을 통한 결제하기 + const payment: IIamportCardPayment = + await test_fake_subscription_payment_again(connector); - // 검증 로직 준비 - const validate = (cancelled: boolean) => (p: IIamportPayment) => { - TestValidator.equals("cancel_amount")(p.cancel_amount)( - cancelled ? payment.amount : 0, - ); - TestValidator.predicate("cancelled_at")( - () => !!p.cancelled_at === cancelled, - ); - TestValidator.predicate("cancel_history")( - cancelled - ? () => - p.cancel_history.length === 1 && - p.cancel_history[0].amount === payment.amount - : () => p.cancel_history.length === 0, - ); - }; - validate(false)(payment); + // 검증 로직 준비 + const validate = (cancelled: boolean) => (p: IIamportPayment) => { + TestValidator.equals("cancel_amount")(p.cancel_amount)( + cancelled ? payment.amount : 0, + ); + TestValidator.predicate("cancelled_at")( + () => !!p.cancelled_at === cancelled, + ); + TestValidator.predicate("cancel_history")( + cancelled + ? () => + p.cancel_history.length === 1 && + p.cancel_history[0].amount === payment.amount + : () => p.cancel_history.length === 0, + ); + }; + validate(false)(payment); - // 결제 취소하기 (전액) - const reply: IIamportResponse = - await imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount, - checksum: payment.amount, - reason: "테스트 결제 취소", - }); + // 결제 취소하기 (전액) + const reply: IIamportResponse = + await imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount, + checksum: payment.amount, + reason: "테스트 결제 취소", + }); - // 데이터 및 로직 검증 - typia.assert(reply); - typia.assert(reply.response); - validate(true)(reply.response); + // 데이터 및 로직 검증 + typia.assert(reply); + typia.assert(reply.response); + validate(true)(reply.response); - // 웹훅 검증 - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); - TestValidator.equals("webhook.status")(webhook.status)("cancelled"); + // 웹훅 검증 + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); + TestValidator.equals("webhook.status")(webhook.status)("cancelled"); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - typia.assert(reloaded.response); - validate(true)(reloaded.response); + // 결제 내역 재 조회하여 다시 검증 + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + typia.assert(reloaded.response); + validate(true)(reloaded.response); } diff --git a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_over.ts b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_over.ts index d9bdded..b550a4c 100644 --- a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_over.ts +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_over.ts @@ -8,31 +8,31 @@ import typia from "typia"; import { test_fake_subscription_payment_again } from "./test_fake_subscription_payment_again"; export async function test_fake_subscription_payment_cancel_over( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 카드 결제하기 - const payment: IIamportCardPayment = - await test_fake_subscription_payment_again(connector); + // 카드 결제하기 + const payment: IIamportCardPayment = + await test_fake_subscription_payment_again(connector); - // 결제 취소하기 (전액 + 100) - await TestValidator.error("over")(async () => - imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount + 100, - checksum: null, - reason: "테스트 결제 취소", - }), - ); + // 결제 취소하기 (전액 + 100) + await TestValidator.error("over")(async () => + imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount + 100, + checksum: null, + reason: "테스트 결제 취소", + }), + ); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - typia.assert(reloaded.response); - TestValidator.equals("cancel_amount")(reloaded.response.cancel_amount)(0); + // 결제 내역 재 조회하여 다시 검증 + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + typia.assert(reloaded.response); + TestValidator.equals("cancel_amount")(reloaded.response.cancel_amount)(0); } diff --git a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_partial.ts b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_partial.ts index 5f847ee..abc6056 100644 --- a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_partial.ts +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_partial.ts @@ -8,52 +8,49 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { test_fake_subscription_payment_again } from "./test_fake_subscription_payment_again"; export async function test_fake_card_subscription_payment_cancel_partial( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 구독을 통한 결제하기 - const payment: IIamportCardPayment = - await test_fake_subscription_payment_again(connector); + // 구독을 통한 결제하기 + const payment: IIamportCardPayment = + await test_fake_subscription_payment_again(connector); - // 검증 로직 준비 - const validate = - (count: number) => - async (p: IIamportPayment): Promise => { - // VALIDATE CANCELLED AMOUNT - const expected: number = (payment.amount / 5) * count; - TestValidator.equals("cancel_amount")(p.cancel_amount)(expected); - TestValidator.predicate("cancelled_at")( - () => !!p.cancelled_at === !!count, - ); - TestValidator.predicate("cancel_history")( - () => - p.cancel_history.length === count && - p.cancel_history - .map((h) => h.amount) - .reduce((a, b) => a + b, 0) === expected, - ); - if (count === 0) return; + // 검증 로직 준비 + const validate = + (count: number) => + async (p: IIamportPayment): Promise => { + // VALIDATE CANCELLED AMOUNT + const expected: number = (payment.amount / 5) * count; + TestValidator.equals("cancel_amount")(p.cancel_amount)(expected); + TestValidator.predicate("cancelled_at")( + () => !!p.cancelled_at === !!count, + ); + TestValidator.predicate("cancel_history")( + () => + p.cancel_history.length === count && + p.cancel_history.map((h) => h.amount).reduce((a, b) => a + b, 0) === + expected, + ); + if (count === 0) return; - // VALIDATE WEBHOOK - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)( - payment.imp_uid, - ); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); - TestValidator.equals("webhook.status")(webhook.status)("cancelled"); - }; - validate(0)(payment); + // VALIDATE WEBHOOK + const webhook: IIamportPayment.IWebhook = + FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); + TestValidator.equals("webhook.status")(webhook.status)("cancelled"); + }; + validate(0)(payment); - // 5 회에 걸쳐 분할 취소 하기 - await ArrayUtil.asyncRepeat(5)(async (i) => { - const reply: IIamportResponse = - await imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount / 5, - checksum: payment.amount - (payment.amount * i) / 5, - reason: `테스트 결제 취소 ${i}`, - }); - await validate(i + 1)(reply.response); - }); + // 5 회에 걸쳐 분할 취소 하기 + await ArrayUtil.asyncRepeat(5)(async (i) => { + const reply: IIamportResponse = + await imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount / 5, + checksum: payment.amount - (payment.amount * i) / 5, + reason: `테스트 결제 취소 ${i}`, + }); + await validate(i + 1)(reply.response); + }); } diff --git a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_onetime.ts b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_onetime.ts index bccf653..47f8d73 100644 --- a/packages/fake-iamport-server/test/features/test_fake_subscription_payment_onetime.ts +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_onetime.ts @@ -10,97 +10,96 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; export async function test_fake_subscription_payment_onetime( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - /** - * 간편 결제 카드 등록을 위한 고객 식별자 키. - * - * 이는 전적으로 아임포트를 사용하는 서비스에서 발급하여 관리하며, - * 아임포트는 이를 고객 식별자 키라고 이름 지었지만, - * 실제 역할은 간편 결제로 등록한 카드 식별자 키로써 역할한다. - */ - const customer_uid: string = v4(); + /** + * 간편 결제 카드 등록을 위한 고객 식별자 키. + * + * 이는 전적으로 아임포트를 사용하는 서비스에서 발급하여 관리하며, + * 아임포트는 이를 고객 식별자 키라고 이름 지었지만, + * 실제 역할은 간편 결제로 등록한 카드 식별자 키로써 역할한다. + */ + const customer_uid: string = v4(); - /** - * 결제 요청 레코드 발행하기. - * - * 아임포트의 경우 {@link subscribe.payments.onetime} 을 이용하면, API 만을 - * 가지고도 카드 결제를 진행할 수 있다. 그리고 이 때, *input* 값에서 - * {@link IIamportSubscription.IOnetime.customer_uid} 를 입력하면, 해당 카드가 - * 간편 결제용으로 등록된다. - * - * 반대로 *input* 값에서 {@link IIamportSubscription.IOnetime.customer_uid} 를 - * 빼 버리면, 해당 카드가 간편 결제용으로 등록되는 일은 없다. - */ - const onetime: IIamportResponse = - await imp.functional.subscribe.payments.onetime(await connector.get(), { - customer_uid, + /** + * 결제 요청 레코드 발행하기. + * + * 아임포트의 경우 {@link subscribe.payments.onetime} 을 이용하면, API 만을 + * 가지고도 카드 결제를 진행할 수 있다. 그리고 이 때, *input* 값에서 + * {@link IIamportSubscription.IOnetime.customer_uid} 를 입력하면, 해당 카드가 + * 간편 결제용으로 등록된다. + * + * 반대로 *input* 값에서 {@link IIamportSubscription.IOnetime.customer_uid} 를 + * 빼 버리면, 해당 카드가 간편 결제용으로 등록되는 일은 없다. + */ + const onetime: IIamportResponse = + await imp.functional.subscribe.payments.onetime(await connector.get(), { + customer_uid, - card_number: AdvancedRandomGenerator.cardNumber(), - expiry: "2028-12", - birth: "880311", + card_number: AdvancedRandomGenerator.cardNumber(), + expiry: "2028-12", + birth: "880311", - merchant_uid: v4(), - amount: 25_000, - name: "Fake 주문", - }); - typia.assert(onetime); - await validate(connector, onetime.response.imp_uid); + merchant_uid: v4(), + amount: 25_000, + name: "Fake 주문", + }); + typia.assert(onetime); + await validate(connector, onetime.response.imp_uid); - /** - * 간편 결제 카드로 결제하기. - * - * 앞서 {@link subscribe.payments.onetime} 때 사용한 `customer_uid` 를 - * 재활용, 카드 정보를 다시 입력하는 일 없이, 매우 간편하게 결제할 수 있다. - */ - const again: IIamportResponse = - await imp.functional.subscribe.payments.again(await connector.get(), { - customer_uid, - merchant_uid: v4(), - amount: 10_000, - name: "Fake 주문", - }); - typia.assert(again); - await validate(connector, again.response.imp_uid); + /** + * 간편 결제 카드로 결제하기. + * + * 앞서 {@link subscribe.payments.onetime} 때 사용한 `customer_uid` 를 + * 재활용, 카드 정보를 다시 입력하는 일 없이, 매우 간편하게 결제할 수 있다. + */ + const again: IIamportResponse = + await imp.functional.subscribe.payments.again(await connector.get(), { + customer_uid, + merchant_uid: v4(), + amount: 10_000, + name: "Fake 주문", + }); + typia.assert(again); + await validate(connector, again.response.imp_uid); } async function validate( - connector: imp.IamportConnector, - imp_uid: string, + connector: imp.IamportConnector, + imp_uid: string, ): Promise { - /** - * 아임포트 서버로부터의 웹훅 데이터. - * - * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 - * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. - */ - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(imp_uid); - TestValidator.equals("webhook.status")(webhook.status)("paid"); + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. + */ + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(imp_uid); + TestValidator.equals("webhook.status")(webhook.status)("paid"); - /** - * 결제 내역 조회하기. - * - * 위에서 발행한 간편 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 - * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 카드 결제가 - * 무사히 완료되었음을, 그리고 관련 결제 정보 {@link IIamportCardPayment} 정보가 - * 완전하게 구성되었음을 알 수 있다. - */ - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - webhook.imp_uid, - {}, - ); - typia.assert(reloaded); - - // 결제 방식 및 완료 여부 확인 - const payment: IIamportCardPayment = typia.assert( - reloaded.response, + /** + * 결제 내역 조회하기. + * + * 위에서 발행한 간편 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 + * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 카드 결제가 + * 무사히 완료되었음을, 그리고 관련 결제 정보 {@link IIamportCardPayment} 정보가 + * 완전하게 구성되었음을 알 수 있다. + */ + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + webhook.imp_uid, + {}, ); - TestValidator.predicate("paid_at")(() => payment.paid_at !== 0); - TestValidator.equals("status")(payment.status)("paid"); + typia.assert(reloaded); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportCardPayment = typia.assert( + reloaded.response, + ); + TestValidator.predicate("paid_at")(() => payment.paid_at !== 0); + TestValidator.equals("status")(payment.status)("paid"); - return payment; + return payment; } diff --git a/packages/fake-iamport-server/test/features/test_fake_vbank_payment.ts b/packages/fake-iamport-server/test/features/test_fake_vbank_payment.ts index dcc5f2f..1155f0c 100644 --- a/packages/fake-iamport-server/test/features/test_fake_vbank_payment.ts +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment.ts @@ -10,118 +10,116 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; export async function test_fake_vbank_payment( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 가상 결제 발행하기 - const ready: IIamportVBankPayment = await issue(connector); + // 가상 결제 발행하기 + const ready: IIamportVBankPayment = await issue(connector); - // 입금 시뮬레이션 - return deposit(connector, ready); + // 입금 시뮬레이션 + return deposit(connector, ready); } async function issue( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - /** - * 가상 계좌로 결제 요청하기. - * - * 가상 계좌란 결국 무통장 입금의 일환, 고로 즉시 결제가 완료되는 것이 아니다. - * - * 향후 고객이 결제 금액을 모두 입금하기 전까지, `ready` 상태가 계속된다. - */ - const output: IIamportResponse = - await imp.functional.vbanks.store(await connector.get(), { - merchant_uid: v4(), - amount: 40_000, - vbank_code: AdvancedRandomGenerator.alphabets(8), - vbank_due: Date.now() / 1000 + 7 * 24 * 60 * 60, - vbank_holder: AdvancedRandomGenerator.name(), - }); - typia.assert(output); - TestValidator.equals("status")(output.response.status)("ready"); + /** + * 가상 계좌로 결제 요청하기. + * + * 가상 계좌란 결국 무통장 입금의 일환, 고로 즉시 결제가 완료되는 것이 아니다. + * + * 향후 고객이 결제 금액을 모두 입금하기 전까지, `ready` 상태가 계속된다. + */ + const output: IIamportResponse = + await imp.functional.vbanks.store(await connector.get(), { + merchant_uid: v4(), + amount: 40_000, + vbank_code: AdvancedRandomGenerator.alphabets(8), + vbank_due: Date.now() / 1000 + 7 * 24 * 60 * 60, + vbank_holder: AdvancedRandomGenerator.name(), + }); + typia.assert(output); + TestValidator.equals("status")(output.response.status)("ready"); - /** - * 아임포트 서버로부터의 웹훅 데이터. - * - * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 - * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. - */ - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("imp_uid")(webhook.imp_uid)(output.response.imp_uid); - TestValidator.equals("status")(webhook.status)("ready"); + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. + */ + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("imp_uid")(webhook.imp_uid)(output.response.imp_uid); + TestValidator.equals("status")(webhook.status)("ready"); - /** - * 결제 내역 조회하기. - * - * 위에서 발행한 가상 계좌 신청 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 - * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 가상 계좌가 - * 무사히 발급되었음을, 그리고 고객이 입금해야 할 가상 계좌 정보가 완전하게 - * 구성되었음을 알 수 있다. - */ - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - webhook.imp_uid, - {}, - ); - typia.assert(reloaded); - - // 결제 방식 및 완료 여부 확인 - const payment: IIamportVBankPayment = typia.assert( - reloaded.response, + /** + * 결제 내역 조회하기. + * + * 위에서 발행한 가상 계좌 신청 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 + * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 가상 계좌가 + * 무사히 발급되었음을, 그리고 고객이 입금해야 할 가상 계좌 정보가 완전하게 + * 구성되었음을 알 수 있다. + */ + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + webhook.imp_uid, + {}, ); - TestValidator.equals("imp_uid")(payment.imp_uid)(output.response.imp_uid); - TestValidator.equals("status")(payment.status)("ready"); + typia.assert(reloaded); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportVBankPayment = typia.assert( + reloaded.response, + ); + TestValidator.equals("imp_uid")(payment.imp_uid)(output.response.imp_uid); + TestValidator.equals("status")(payment.status)("ready"); - // FOR THE NEXT STEP - return payment; + // FOR THE NEXT STEP + return payment; } async function deposit( - connector: imp.IamportConnector, - ready: IIamportVBankPayment, + connector: imp.IamportConnector, + ready: IIamportVBankPayment, ): Promise { - /** - * 입금 시뮬레이션. - * - * 고객이 자신 앞으로 발급된 가상 계좌에, 결제 금액을 입금함. - * - * 이 API 함수는 오직 `fake-iamport-server` 에만 존재하는 테스트용 함수. - */ - await imp.functional.internal.deposit(await connector.get(), ready.imp_uid); + /** + * 입금 시뮬레이션. + * + * 고객이 자신 앞으로 발급된 가상 계좌에, 결제 금액을 입금함. + * + * 이 API 함수는 오직 `fake-iamport-server` 에만 존재하는 테스트용 함수. + */ + await imp.functional.internal.deposit(await connector.get(), ready.imp_uid); - /** - * 아임포트 서버로부터의 웹훅 데이터. - * - * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 - * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. - */ - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("imp_uid")(webhook.imp_uid)(ready.imp_uid); - TestValidator.equals("status")(webhook.status)("paid"); + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@link payments.at} API 함수를 호출해야 한다. + */ + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("imp_uid")(webhook.imp_uid)(ready.imp_uid); + TestValidator.equals("status")(webhook.status)("paid"); - /** - * 결제 내역 조회하기. - * - * 위에서 발행한 가상 계좌 신청 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 - * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 고객이 - * 가상계좌에 입금을 완료하여, 결제가 완료되었음을 알 수 있다. - */ - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - webhook.imp_uid, - {}, - ); - typia.assert(reloaded); - - // 결제 방식 및 완료 여부 확인 - const payment: IIamportVBankPayment = typia.assert( - reloaded.response, + /** + * 결제 내역 조회하기. + * + * 위에서 발행한 가상 계좌 신청 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 + * 서버로부터 {@link payments.at} API 함수를 호출하여 재 조회해보면, 고객이 + * 가상계좌에 입금을 완료하여, 결제가 완료되었음을 알 수 있다. + */ + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + webhook.imp_uid, + {}, ); - TestValidator.equals("imp_uid")(payment.imp_uid)(reloaded.response.imp_uid); - TestValidator.equals("status")(payment.status)("paid"); - return payment; + typia.assert(reloaded); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportVBankPayment = typia.assert( + reloaded.response, + ); + TestValidator.equals("imp_uid")(payment.imp_uid)(reloaded.response.imp_uid); + TestValidator.equals("status")(payment.status)("paid"); + return payment; } diff --git a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel.ts b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel.ts index f2f9a15..f3110e5 100644 --- a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel.ts +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel.ts @@ -9,65 +9,64 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { test_fake_vbank_payment } from "./test_fake_vbank_payment"; export async function test_fake_vbank_payment_cancel( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 가상 계좌를 통한 결제하기 - const payment: IIamportVBankPayment = await test_fake_vbank_payment( - connector, - ); + // 가상 계좌를 통한 결제하기 + const payment: IIamportVBankPayment = await test_fake_vbank_payment( + connector, + ); - // 검증 로직 준비 - const validate = (cancelled: boolean) => (p: IIamportPayment) => { - TestValidator.equals("cancel_amount")(p.cancel_amount)( - cancelled ? payment.amount : 0, - ); - TestValidator.predicate("cancelled_at")( - () => !!p.cancelled_at === cancelled, - ); - TestValidator.predicate("cancel_history")( - cancelled - ? () => - p.cancel_history.length === 1 && - p.cancel_history[0].amount === payment.amount - : () => p.cancel_history.length === 0, - ); - }; - validate(false)(payment); + // 검증 로직 준비 + const validate = (cancelled: boolean) => (p: IIamportPayment) => { + TestValidator.equals("cancel_amount")(p.cancel_amount)( + cancelled ? payment.amount : 0, + ); + TestValidator.predicate("cancelled_at")( + () => !!p.cancelled_at === cancelled, + ); + TestValidator.predicate("cancel_history")( + cancelled + ? () => + p.cancel_history.length === 1 && + p.cancel_history[0].amount === payment.amount + : () => p.cancel_history.length === 0, + ); + }; + validate(false)(payment); - // 결제 취소하기 (전액) - const reply: IIamportResponse = - await imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount, - checksum: payment.amount, - reason: "테스트 결제 취소", - refund_account: "1101234567890", - refund_holder: "홍길동", - refund_bank: "국민은행", - refund_tel: "01012345678", - }); + // 결제 취소하기 (전액) + const reply: IIamportResponse = + await imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount, + checksum: payment.amount, + reason: "테스트 결제 취소", + refund_account: "1101234567890", + refund_holder: "홍길동", + refund_bank: "국민은행", + refund_tel: "01012345678", + }); - // 데이터 및 로직 검증 - typia.assert(reply); - typia.assert(reply.response); - validate(true)(reply.response); + // 데이터 및 로직 검증 + typia.assert(reply); + typia.assert(reply.response); + validate(true)(reply.response); - // 웹훅 검증 - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); - TestValidator.equals("webhook.status")(webhook.status)("cancelled"); + // 웹훅 검증 + const webhook: IIamportPayment.IWebhook = FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); + TestValidator.equals("webhook.status")(webhook.status)("cancelled"); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - typia.assert(reloaded.response); - validate(true)(reloaded.response); + // 결제 내역 재 조회하여 다시 검증 + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + typia.assert(reloaded.response); + validate(true)(reloaded.response); } diff --git a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_over.ts b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_over.ts index 8e4ebd6..8fc929e 100644 --- a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_over.ts +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_over.ts @@ -8,32 +8,32 @@ import typia from "typia"; import { test_fake_vbank_payment } from "./test_fake_vbank_payment"; export async function test_fake_vbank_payment_cancel_over( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 카드 결제하기 - const payment: IIamportVBankPayment = await test_fake_vbank_payment( - connector, - ); + // 카드 결제하기 + const payment: IIamportVBankPayment = await test_fake_vbank_payment( + connector, + ); - // 결제 취소하기 (전액 + 100) - await TestValidator.error("over")(async () => - imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount + 100, - checksum: null, - reason: "테스트 결제 취소", - }), - ); + // 결제 취소하기 (전액 + 100) + await TestValidator.error("over")(async () => + imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount + 100, + checksum: null, + reason: "테스트 결제 취소", + }), + ); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: IIamportResponse = - await imp.functional.payments.at( - await connector.get(), - payment.imp_uid, - {}, - ); - typia.assert(reloaded); - typia.assert(reloaded.response); - TestValidator.equals("cancel_amount")(reloaded.response.cancel_amount)(0); + // 결제 내역 재 조회하여 다시 검증 + const reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + payment.imp_uid, + {}, + ); + typia.assert(reloaded); + typia.assert(reloaded.response); + TestValidator.equals("cancel_amount")(reloaded.response.cancel_amount)(0); } diff --git a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_partial.ts b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_partial.ts index 8b11777..0b1f2b1 100644 --- a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_partial.ts +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_partial.ts @@ -8,57 +8,54 @@ import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; import { test_fake_vbank_payment } from "./test_fake_vbank_payment"; export async function test_fake_vbank_payment_cancel_partial( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - // 카드 결제하기 - const payment: IIamportVBankPayment = await test_fake_vbank_payment( - connector, - ); + // 카드 결제하기 + const payment: IIamportVBankPayment = await test_fake_vbank_payment( + connector, + ); - // 검증 로직 준비 - const validate = - (count: number) => - async (p: IIamportPayment): Promise => { - // VALIDATE CANCELLED AMOUNT - const expected: number = (payment.amount / 5) * count; - TestValidator.equals("cancel_amount")(p.cancel_amount)(expected); - TestValidator.predicate("cancelled_at")( - () => !!p.cancelled_at === !!count, - ); - TestValidator.predicate("cancel_history")( - () => - p.cancel_history.length === count && - p.cancel_history - .map((h) => h.amount) - .reduce((a, b) => a + b, 0) === expected, - ); - if (count === 0) return; + // 검증 로직 준비 + const validate = + (count: number) => + async (p: IIamportPayment): Promise => { + // VALIDATE CANCELLED AMOUNT + const expected: number = (payment.amount / 5) * count; + TestValidator.equals("cancel_amount")(p.cancel_amount)(expected); + TestValidator.predicate("cancelled_at")( + () => !!p.cancelled_at === !!count, + ); + TestValidator.predicate("cancel_history")( + () => + p.cancel_history.length === count && + p.cancel_history.map((h) => h.amount).reduce((a, b) => a + b, 0) === + expected, + ); + if (count === 0) return; - // VALIDATE WEBHOOK - const webhook: IIamportPayment.IWebhook = - FakeIamportStorage.webhooks.back(); - TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)( - payment.imp_uid, - ); - TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); - TestValidator.equals("webhook.status")(webhook.status)("cancelled"); - }; - validate(0)(payment); + // VALIDATE WEBHOOK + const webhook: IIamportPayment.IWebhook = + FakeIamportStorage.webhooks.back(); + TestValidator.equals("webhook.imp_uid")(webhook.imp_uid)(payment.imp_uid); + TestValidator.equals("webhook.merchant_uid")(webhook.merchant_uid); + TestValidator.equals("webhook.status")(webhook.status)("cancelled"); + }; + validate(0)(payment); - // 5 회에 걸쳐 분할 취소 하기 - await ArrayUtil.asyncRepeat(5)(async (i) => { - const reply: IIamportResponse = - await imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount / 5, - checksum: payment.amount - (payment.amount * i) / 5, - reason: `테스트 결제 취소 ${i}`, - refund_account: "1101234567890", - refund_holder: "홍길동", - refund_bank: "국민은행", - refund_tel: "01012345678", - }); - await validate(i + 1)(reply.response); - }); + // 5 회에 걸쳐 분할 취소 하기 + await ArrayUtil.asyncRepeat(5)(async (i) => { + const reply: IIamportResponse = + await imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount / 5, + checksum: payment.amount - (payment.amount * i) / 5, + reason: `테스트 결제 취소 ${i}`, + refund_account: "1101234567890", + refund_holder: "홍길동", + refund_bank: "국민은행", + refund_tel: "01012345678", + }); + await validate(i + 1)(reply.response); + }); } diff --git a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_without_refund.ts b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_without_refund.ts index 41d791a..2907760 100644 --- a/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_without_refund.ts +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_without_refund.ts @@ -5,22 +5,22 @@ import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamport import { test_fake_vbank_payment } from "./test_fake_vbank_payment"; export async function test_fake_vbank_payment_cancel_without_refund( - connector: imp.IamportConnector, + connector: imp.IamportConnector, ): Promise { - const payment: IIamportVBankPayment = await test_fake_vbank_payment( - connector, - ); - await TestValidator.error("cancel without refund info")(async () => - imp.functional.payments.cancel(await connector.get(), { - imp_uid: payment.imp_uid, - merchant_uid: payment.merchant_uid, - amount: payment.amount, - checksum: payment.amount, - reason: "테스트 결제 취소", - // refund_account: "1101234567890", - // refund_holder: "홍길동", - // refund_bank: "국민은행", - // refund_tel: "01012345678", - }), - ); + const payment: IIamportVBankPayment = await test_fake_vbank_payment( + connector, + ); + await TestValidator.error("cancel without refund info")(async () => + imp.functional.payments.cancel(await connector.get(), { + imp_uid: payment.imp_uid, + merchant_uid: payment.merchant_uid, + amount: payment.amount, + checksum: payment.amount, + reason: "테스트 결제 취소", + // refund_account: "1101234567890", + // refund_holder: "홍길동", + // refund_bank: "국민은행", + // refund_tel: "01012345678", + }), + ); } diff --git a/packages/fake-iamport-server/test/index.ts b/packages/fake-iamport-server/test/index.ts index 524f75a..edec2c3 100644 --- a/packages/fake-iamport-server/test/index.ts +++ b/packages/fake-iamport-server/test/index.ts @@ -6,46 +6,46 @@ import { FakeIamportConfiguration } from "../src/FakeIamportConfiguration"; import { ErrorUtil } from "../src/utils/ErrorUtil"; async function handle_error(exp: any): Promise { - ErrorUtil.toJSON(exp); + ErrorUtil.toJSON(exp); } async function main(): Promise { - // BACKEND SERVER - const backend: FakeIamportBackend = new FakeIamportBackend(); - await backend.open(); + // BACKEND SERVER + const backend: FakeIamportBackend = new FakeIamportBackend(); + await backend.open(); - // PARAMETER - const connector: IamportConnector = new IamportConnector( - `http://127.0.0.1:${FakeIamportConfiguration.API_PORT}`, - { - imp_key: "test_imp_key", - imp_secret: "test_imp_secret", - }, - ); - global.process.on("uncaughtException", handle_error); - global.process.on("unhandledRejection", handle_error); + // PARAMETER + const connector: IamportConnector = new IamportConnector( + `http://127.0.0.1:${FakeIamportConfiguration.API_PORT}`, + { + imp_key: "test_imp_key", + imp_secret: "test_imp_secret", + }, + ); + global.process.on("uncaughtException", handle_error); + global.process.on("unhandledRejection", handle_error); - // DO TEST - const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ - prefix: "test", - parameters: () => [connector], - })(__dirname + "/features"); + // DO TEST + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + prefix: "test", + parameters: () => [connector], + })(__dirname + "/features"); - // TERMINATE - await backend.close(); + // TERMINATE + await backend.close(); - const exceptions: Error[] = report.executions - .filter((exec) => exec.error !== null) - .map((exec) => exec.error!); - if (exceptions.length === 0) { - console.log(`Total elapsed time: ${report.time.toLocaleString()} ms`); - console.log("Success"); - } else { - for (const exp of exceptions) console.log(exp); - process.exit(-1); - } + const exceptions: Error[] = report.executions + .filter((exec) => exec.error !== null) + .map((exec) => exec.error!); + if (exceptions.length === 0) { + console.log(`Total elapsed time: ${report.time.toLocaleString()} ms`); + console.log("Success"); + } else { + for (const exp of exceptions) console.log(exp); + process.exit(-1); + } } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/fake-iamport-server/test/tsconfig.json b/packages/fake-iamport-server/test/tsconfig.json index 25ae87e..ba053f3 100644 --- a/packages/fake-iamport-server/test/tsconfig.json +++ b/packages/fake-iamport-server/test/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "outDir": "../bin", }, - "include": [".", "../src"] + "include": [ + ".", + "../src", + ], } \ No newline at end of file diff --git a/packages/fake-toss-payments-server/nestia.config.ts b/packages/fake-toss-payments-server/nestia.config.ts index 2919f82..6a5ed94 100644 --- a/packages/fake-toss-payments-server/nestia.config.ts +++ b/packages/fake-toss-payments-server/nestia.config.ts @@ -5,7 +5,7 @@ import { FakeTossModule } from "./src/FakeTossModule"; const NESTIA_CONFIG: INestiaConfig = { simulate: true, - input: async () => NestFactory.create(await FakeTossModule()), + input: () => NestFactory.create(FakeTossModule), output: "src/api", distribute: "../toss-payments-server-api", swagger: { diff --git a/packages/fake-toss-payments-server/package.json b/packages/fake-toss-payments-server/package.json index 11b5ee9..7e70532 100644 --- a/packages/fake-toss-payments-server/package.json +++ b/packages/fake-toss-payments-server/package.json @@ -1,6 +1,6 @@ { "name": "fake-toss-payments-server", - "version": "5.0.6", + "version": "5.1.0", "description": "Fake toss-payments server for testing", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -15,7 +15,6 @@ "dev": "npm run build:test -- --watch", "eslint": "eslint src && eslint --config .eslintrc.test.cjs test", "eslint:fix": "eslint --fix src && eslint --fix --config .eslintrc.test.cjs test", - "prettier": "prettier src --write && prettier test --write", "------------------------------------------------": "", "package:api": "npm run build:swagger && npm run build:api && cd packages/api && npm publish", "package:latest": "npm run build && npm run test && npm publish", @@ -40,8 +39,7 @@ "homepage": "https://github.com/samchon/fake-toss-payments-server", "devDependencies": { "@nestia/e2e": "^0.3.6", - "@nestia/sdk": "^2.3.4", - "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@nestia/sdk": "^2.3.9", "@types/atob": "^2.1.2", "@types/btoa": "^1.2.3", "@types/cli": "^0.11.19", @@ -53,7 +51,6 @@ "copyfiles": "^2.4.1", "nestia": "^5.0.1", "pm2": "^4.5.6", - "prettier": "^2.6.2", "rimraf": "^3.0.2", "sloc": "^0.2.1", "ts-node": "^10.9.1", @@ -62,13 +59,17 @@ "typescript-transform-paths": "^3.4.6" }, "dependencies": { - "@nestia/core": "^2.3.4", + "@nestia/core": "^2.3.9", + "@nestjs/common": "^10.2.8", + "@nestjs/core": "^10.2.8", + "@nestjs/platform-fastify": "^10.2.8", "atob": "^2.1.2", "btoa": "^1.2.1", + "fastify": "^4.24.3", "serialize-error": "^4.1.0", "source-map-support": "^0.5.19", "tstl": "^2.5.13", - "typia": "^5.2.4", + "typia": "^5.2.6", "uuid": "^9.0.0" }, "keywords": [ diff --git a/packages/fake-toss-payments-server/src/FakeTossBackend.ts b/packages/fake-toss-payments-server/src/FakeTossBackend.ts index 24459d4..f338d77 100644 --- a/packages/fake-toss-payments-server/src/FakeTossBackend.ts +++ b/packages/fake-toss-payments-server/src/FakeTossBackend.ts @@ -1,7 +1,7 @@ import { NestFactory } from "@nestjs/core"; import { - FastifyAdapter, - NestFastifyApplication, + FastifyAdapter, + NestFastifyApplication, } from "@nestjs/platform-fastify"; import { FakeTossConfiguration } from "./FakeTossConfiguration"; @@ -13,47 +13,47 @@ import { FakeTossModule } from "./FakeTossModule"; * @author Samchon */ export class FakeTossBackend { - private application_?: NestFastifyApplication; - - /** - * 서버 개설. - */ - public async open(): Promise { - //---- - // OPEN THE BACKEND SERVER - //---- - // MOUNT CONTROLLERS - this.application_ = await NestFactory.create( - await FakeTossModule(), - new FastifyAdapter(), - { logger: false }, - ); - - // DO OPEN - this.application_.enableCors(); - await this.application_.listen(FakeTossConfiguration.API_PORT); - - //---- - // POST-PROCESSES - //---- - // INFORM TO THE PM2 - if (process.send) process.send("ready"); - - // WHEN KILL COMMAND COMES - process.on("SIGINT", async () => { - await this.close(); - process.exit(0); - }); - } - - /** - * 서버 폐쇄. - */ - public async close(): Promise { - if (this.application_ === undefined) return; - - // DO CLOSE - await this.application_.close(); - delete this.application_; - } + private application_?: NestFastifyApplication; + + /** + * 서버 개설. + */ + public async open(): Promise { + //---- + // OPEN THE BACKEND SERVER + //---- + // MOUNT CONTROLLERS + this.application_ = await NestFactory.create( + FakeTossModule, + new FastifyAdapter(), + { logger: false }, + ); + + // DO OPEN + this.application_.enableCors(); + await this.application_.listen(FakeTossConfiguration.API_PORT); + + //---- + // POST-PROCESSES + //---- + // INFORM TO THE PM2 + if (process.send) process.send("ready"); + + // WHEN KILL COMMAND COMES + process.on("SIGINT", async () => { + await this.close(); + process.exit(0); + }); + } + + /** + * 서버 폐쇄. + */ + public async close(): Promise { + if (this.application_ === undefined) return; + + // DO CLOSE + await this.application_.close(); + delete this.application_; + } } diff --git a/packages/fake-toss-payments-server/src/FakeTossConfiguration.ts b/packages/fake-toss-payments-server/src/FakeTossConfiguration.ts index d958d96..0b54c8e 100644 --- a/packages/fake-toss-payments-server/src/FakeTossConfiguration.ts +++ b/packages/fake-toss-payments-server/src/FakeTossConfiguration.ts @@ -1,12 +1,17 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; +import { + ConflictException, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException, +} from "@nestjs/common"; import { DomainError } from "tstl/exception/DomainError"; import { InvalidArgument } from "tstl/exception/InvalidArgument"; import { OutOfRange } from "tstl/exception/OutOfRange"; /* eslint-disable */ -const EXTENSION = __filename.substr(-2); +const EXTENSION = __filename.substring(__filename.length - 2); if (EXTENSION === "js") require("source-map-support").install(); /** @@ -15,77 +20,77 @@ if (EXTENSION === "js") require("source-map-support").install(); * @author Samchon */ export namespace FakeTossConfiguration { - /** - * @internal - */ - export const ASSETS = __dirname + "/../assets"; + /** + * @internal + */ + export const ASSETS = __dirname + "/../assets"; - /** - * 임시 저장소의 레코드 만료 기한. - */ - export const EXPIRATION: IExpiration = { - time: 3 * 60 * 1000, - capacity: 1000, - }; + /** + * 임시 저장소의 레코드 만료 기한. + */ + export const EXPIRATION: IExpiration = { + time: 3 * 60 * 1000, + capacity: 1000, + }; - /** - * 서버가 사용할 포트 번호. - */ - export let API_PORT: number = 30771; + /** + * 서버가 사용할 포트 번호. + */ + export let API_PORT: number = 30771; - /** - * Webhook 이벤트를 수신할 URL 주소. - */ - export let WEBHOOK_URL: string = `http://127.0.0.1:${API_PORT}/internal/webhook`; + /** + * Webhook 이벤트를 수신할 URL 주소. + */ + export let WEBHOOK_URL: string = `http://127.0.0.1:${API_PORT}/internal/webhook`; + /** + * 토큰 인증 함수. + * + * 클라이언트가 전송한 Basic 토큰값이 제대로 된 것인지 판별한다. + * + * @param token 토큰값 + */ + export let authorize: (token: string) => boolean = (token) => { + return token === "test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy"; + }; + + /** + * 임시 저장소의 레코드 만료 기한. + */ + export interface IExpiration { /** - * 토큰 인증 함수. - * - * 클라이언트가 전송한 Basic 토큰값이 제대로 된 것인지 판별한다. - * - * @param token 토큰값 + * 만료 시간. */ - export let authorize: (token: string) => boolean = (token) => { - return token === "test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy"; - }; + time: number; /** - * 임시 저장소의 레코드 만료 기한. + * 최대 수용량. */ - export interface IExpiration { - /** - * 만료 시간. - */ - time: number; - - /** - * 최대 수용량. - */ - capacity: number; - } + capacity: number; + } } // CUSTOM EXCEPTIION CONVERSION core.ExceptionManager.insert( - OutOfRange, - (exp) => new nest.NotFoundException(exp.message), + OutOfRange, + (exp) => new NotFoundException(exp.message), ); core.ExceptionManager.insert( - InvalidArgument, - (exp) => new nest.ConflictException(exp.message), + InvalidArgument, + (exp) => new ConflictException(exp.message), ); core.ExceptionManager.insert( - DomainError, - (exp) => new nest.UnprocessableEntityException(exp.message), + DomainError, + (exp) => new UnprocessableEntityException(exp.message), ); // TRACE EXACT SERVER INTERNAL ERROR core.ExceptionManager.insert( - Error, - (exp) => - new nest.InternalServerErrorException({ - message: exp.message, - name: exp.name, - stack: exp.stack, - }), + Error, + (exp) => + new InternalServerErrorException({ + message: exp.message, + name: exp.name, + stack: exp.stack, + }), ); diff --git a/packages/fake-toss-payments-server/src/FakeTossModule.ts b/packages/fake-toss-payments-server/src/FakeTossModule.ts index b47ca14..17d538d 100644 --- a/packages/fake-toss-payments-server/src/FakeTossModule.ts +++ b/packages/fake-toss-payments-server/src/FakeTossModule.ts @@ -1,4 +1,18 @@ -import { DynamicModule } from "@nestia/core"; +import { Module } from "@nestjs/common"; -export const FakeTossModule = () => - DynamicModule.mount(`${__dirname}/controllers`); +import { FakeTossBillingController } from "./controllers/FakeTossBillingController"; +import { FakeTossCashReceiptsController } from "./controllers/FakeTossCashReceiptsController"; +import { FakeTossInternalController } from "./controllers/FakeTossInternalController"; +import { FakeTossPaymentsController } from "./controllers/FakeTossPaymentsController"; +import { FakeTossVirtualAccountsController } from "./controllers/FakeTossVirtualAccountsController"; + +@Module({ + controllers: [ + FakeTossBillingController, + FakeTossCashReceiptsController, + FakeTossInternalController, + FakeTossPaymentsController, + FakeTossVirtualAccountsController, + ], +}) +export class FakeTossModule {} diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts b/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts index fa41106..bf09fd0 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts @@ -10,160 +10,160 @@ import { tags } from "typia"; * @author Samchon */ export interface ITossBilling extends ITossBilling.ICustomerKey { + /** + * 가맹점 ID. + * + * 현재 tosspayments 가 쓰임. + */ + mId: string; + + /** + * {@link ITossBilling} 의 식별자 키. + */ + billingKey: string; + + /** + * 결제 수단. + */ + method: "카드"; + + /** + * 카드사 이름. + */ + cardCompany: string; + + /** + * 카드 번호. + */ + cardNumber: string & tags.Pattern<"[0-9]{16}">; + + /** + * 인증 일시. + */ + authenticatedAt: string & tags.Format<"date-time">; +} +export namespace ITossBilling { + /** + * 간편 결제 카드 등록 정보. + */ + export interface IStore extends ICustomerKey { /** - * 가맹점 ID. - * - * 현재 tosspayments 가 쓰임. + * 카드 번호. */ - mId: string; + cardNumber: string & tags.Pattern<"[0-9]{16}">; /** - * {@link ITossBilling} 의 식별자 키. + * 카드 만료 년도 (2 자리). */ - billingKey: string; + cardExpirationYear: string & tags.Pattern<"\\d{2}">; /** - * 결제 수단. + * 카드 만료 월 (2 자리). */ - method: "카드"; + cardExpirationMonth: string & tags.Pattern<"^(0[1-9]|1[012])$">; /** - * 카드사 이름. + * 카드 비밀번호. */ - cardCompany: string; + cardPassword: string; /** - * 카드 번호. + * 고객의 생년월일. + * + * 표기 형식 YYMMDD. */ - cardNumber: string & tags.Pattern<"[0-9]{16}">; + customerBirthday: string & + tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; /** - * 인증 일시. + * 고객의 이름. */ - authenticatedAt: string & tags.Format<"date-time">; -} -export namespace ITossBilling { + consumerName?: string; + /** - * 간편 결제 카드 등록 정보. + * 고객의 이메일. */ - export interface IStore extends ICustomerKey { - /** - * 카드 번호. - */ - cardNumber: string & tags.Pattern<"[0-9]{16}">; - - /** - * 카드 만료 년도 (2 자리). - */ - cardExpirationYear: string & tags.Pattern<"\\d{2}">; - - /** - * 카드 만료 월 (2 자리). - */ - cardExpirationMonth: string & tags.Pattern<"^(0[1-9]|1[012])$">; - - /** - * 카드 비밀번호. - */ - cardPassword: string; - - /** - * 고객의 생년월일. - * - * 표기 형식 YYMMDD. - */ - customerBirthday: string & - tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; - - /** - * 고객의 이름. - */ - consumerName?: string; - - /** - * 고객의 이메일. - */ - customerEmail?: string & tags.Format<"email">; - - /** - * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. - * - * 3DS 인증 결과를 전송해야 하는 경우에만 필수. - */ - vbv?: { - /** - * 3D Secure 인증 세션에 대한 인증 값. - */ - cavv: string; - - /** - * 트랜잭션 ID. - */ - xid: string; - - /** - * 3DS 인증 결과에 대한 코드 값. - */ - eci: string; - }; - } + customerEmail?: string & tags.Format<"email">; /** - * 간편 결제를 이용한 결제 신청 정보. + * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. + * + * 3DS 인증 결과를 전송해야 하는 경우에만 필수. */ - export interface IPaymentStore extends ICustomerKey { - /** - * 결제 수단이 간편 결제임을 의미함. - */ - method: "billing"; - - /** - * {@link IPaymentStore} 의 식별자 키. - */ - billingKey: string; - - /** - * 주문 식별자 키. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - orderId: string; - - /** - * 결제 총액. - */ - amount: number; - } + vbv?: { + /** + * 3D Secure 인증 세션에 대한 인증 값. + */ + cavv: string; + + /** + * 트랜잭션 ID. + */ + xid: string; + + /** + * 3DS 인증 결과에 대한 코드 값. + */ + eci: string; + }; + } + + /** + * 간편 결제를 이용한 결제 신청 정보. + */ + export interface IPaymentStore extends ICustomerKey { + /** + * 결제 수단이 간편 결제임을 의미함. + */ + method: "billing"; + + /** + * {@link IPaymentStore} 의 식별자 키. + */ + billingKey: string; /** - * {@link ITossBilling} 의 식별자 정보. + * 주문 식별자 키. * - * `ITossBilling.IAccessor` 는 프론트 어플리케이션이 토스 페이먼츠에서 제공해주는 - * 간편 결제 카드 등록 창을 이용했을 때, 해당 창에서 모든 과정이 완료된 후 보내주는 - * 정보를 형상화한 자료구조 인터페이스이다. + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + */ + orderId: string; + + /** + * 결제 총액. + */ + amount: number; + } + + /** + * {@link ITossBilling} 의 식별자 정보. + * + * `ITossBilling.IAccessor` 는 프론트 어플리케이션이 토스 페이먼츠에서 제공해주는 + * 간편 결제 카드 등록 창을 이용했을 때, 해당 창에서 모든 과정이 완료된 후 보내주는 + * 정보를 형상화한 자료구조 인터페이스이다. + * + * 프론트 어플리케이션이 이 식별자 정보를 백엔드 서버로 보내면, 백엔드 서버는 토스 + * 페이먼츠 서버의 {@link functional.billing.at} 함수를 호출함으로써, 해당 간편 결제 + * 수단 정보를 취득할 수 있다. + */ + export interface IAccessor extends ICustomerKey { + /** + * 토스 페이먼츠에서 redirect URL 로 보내준 값. * - * 프론트 어플리케이션이 이 식별자 정보를 백엔드 서버로 보내면, 백엔드 서버는 토스 - * 페이먼츠 서버의 {@link functional.billing.at} 함수를 호출함으로써, 해당 간편 결제 - * 수단 정보를 취득할 수 있다. + * 실상 Billing 의 식별자 {@link ITossBilling.billingKey} 그 자체라고 보면 됨. */ - export interface IAccessor extends ICustomerKey { - /** - * 토스 페이먼츠에서 redirect URL 로 보내준 값. - * - * 실상 Billing 의 식별자 {@link ITossBilling.billingKey} 그 자체라고 보면 됨. - */ - authKey: string; - } + authKey: string; + } + /** + * 고객 식별자 정보. + */ + export interface ICustomerKey { /** - * 고객 식별자 정보. + * 고객 식별자 키. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ - export interface ICustomerKey { - /** - * 고객 식별자 키. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - customerKey: string; - } + customerKey: string; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts index 7442834..7485354 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts @@ -8,209 +8,209 @@ import { ITossPayment } from "./ITossPayment"; * @author Samchon */ export interface ITossCardPayment - extends ITossPayment.IBase<"카드", "NORMAL" | "BILLING"> { + extends ITossPayment.IBase<"카드", "NORMAL" | "BILLING"> { + /** + * 카드 정보. + */ + card: ITossCardPayment.ICard; + + /** + * 카드사의 즉시 할인 프로모션 정보. + */ + discount: null | ITossCardPayment.IDiscount; + + /** + * 간편결제로 결제한 경우 간편결제 타입 정보. + */ + easyPay: null | "토스결제" | "페이코" | "삼성페이"; +} +export namespace ITossCardPayment { + /** + * 카드 정보. + */ + export interface ICard { /** - * 카드 정보. + * 카드사 이름. */ - card: ITossCardPayment.ICard; + company: string; /** - * 카드사의 즉시 할인 프로모션 정보. + * 카드 번호. */ - discount: null | ITossCardPayment.IDiscount; + number: string & tags.Pattern<"[0-9]{16}">; /** - * 간편결제로 결제한 경우 간편결제 타입 정보. + * 할부 개월 수. */ - easyPay: null | "토스결제" | "페이코" | "삼성페이"; -} -export namespace ITossCardPayment { + installmentPlanMonths: number; + + /** + * 무이자 할부 적용 여부. + */ + isInterestFree: boolean; + + /** + * 승인 번호. + */ + approveNo: string; + + /** + * 카드 포인트 사용 여부. + */ + useCardPoint: false; + + /** + * 카드 타입. + */ + cardType: "신용" | "체크" | "기프트"; + + /** + * 카드의 소유자 타입. + */ + ownerType: "개인" | "법인"; + + /** + * 카드 결제의 매입 상태. + * + * - READY: 매입 대기 + * - REQUESTED: 매입 요청됨 + * - COMPLETED: 매입 완료 + * - CANCEL_REQUESTED: 매입 취소 요청됨 + * - CANCELD: 매입 취소됨 + */ + acquireStatus: + | "READY" + | "REQUESTED" + | "COMPLETED" + | "CANCEL_REQUESTED" + | "CANCELED"; + + /** + * 영수증 URL. + */ + receiptUrl: string & tags.Format<"url">; + } + + /** + * 카드사의 즉시 할인 프로모션 정보. + */ + export interface IDiscount { + /** + * 카드사의 즉시 할인 프로모션을 적용한 금액. + */ + amount: number; + } + + /** + * 신용 카드를 이용한 결제 신청 정보. + */ + export interface IStore { + /** + * 결제 수단이 신용 카드임을 의미. + */ + method: "card"; + + /** + * 카드 번호. + */ + cardNumber: string & tags.Pattern<"[0-9]{16}">; + /** - * 카드 정보. - */ - export interface ICard { - /** - * 카드사 이름. - */ - company: string; - - /** - * 카드 번호. - */ - number: string & tags.Pattern<"[0-9]{16}">; - - /** - * 할부 개월 수. - */ - installmentPlanMonths: number; - - /** - * 무이자 할부 적용 여부. - */ - isInterestFree: boolean; - - /** - * 승인 번호. - */ - approveNo: string; - - /** - * 카드 포인트 사용 여부. - */ - useCardPoint: false; - - /** - * 카드 타입. - */ - cardType: "신용" | "체크" | "기프트"; - - /** - * 카드의 소유자 타입. - */ - ownerType: "개인" | "법인"; - - /** - * 카드 결제의 매입 상태. - * - * - READY: 매입 대기 - * - REQUESTED: 매입 요청됨 - * - COMPLETED: 매입 완료 - * - CANCEL_REQUESTED: 매입 취소 요청됨 - * - CANCELD: 매입 취소됨 - */ - acquireStatus: - | "READY" - | "REQUESTED" - | "COMPLETED" - | "CANCEL_REQUESTED" - | "CANCELED"; - - /** - * 영수증 URL. - */ - receiptUrl: string & tags.Format<"url">; - } - - /** - * 카드사의 즉시 할인 프로모션 정보. - */ - export interface IDiscount { - /** - * 카드사의 즉시 할인 프로모션을 적용한 금액. - */ - amount: number; - } - - /** - * 신용 카드를 이용한 결제 신청 정보. - */ - export interface IStore { - /** - * 결제 수단이 신용 카드임을 의미. - */ - method: "card"; - - /** - * 카드 번호. - */ - cardNumber: string & tags.Pattern<"[0-9]{16}">; - - /** - * 카드 만료 년도 (2 자리). - */ - cardExpirationYear: string & tags.Pattern<"\\d{2}">; - - /** - * 카드 만료 월 (2 자리). - */ - cardExpirationMonth: string & tags.Pattern<"^(0[1-9]|1[012])$">; - - /** - * 카드 비밀번호. - */ - cardPassword?: string; - - /** - * 할부 개월 수. - */ - cardInstallmentPlan?: number; - - /** - * 지불 총액. - */ - amount: number; - - /** - * 면세금 총액. - */ - taxFreeAmount?: number; - - /** - * 주문 식별자 키. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - orderId: string; - - /** - * 주문 이름. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. - */ - orderName?: string; - - /** - * 고객의 생년월일. - * - * 표기 형식 YYMMDD. - */ - customerBirthday?: string & - tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; - - /** - * 고객의 이메일. - */ - customerEmail?: string & tags.Format<"email">; - - /** - * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. - * - * 3DS 인증 결과를 전송해야 하는 경우에만 필수. - */ - vbv?: { - /** - * 3D Secure 인증 세션에 대한 인증 값. - */ - cavv: string; - - /** - * 트랜잭션 ID. - */ - xid: string; - - /** - * 3DS 인증 결과에 대한 코드 값. - */ - eci: string; - }; - - /** - * 결제 승인 여부. - * - * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, - * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 - * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 - * 상황을 시뮬레이션할 수 있다. - * - * 본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고, - * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, - * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 - * 정식 결제로 인청치 아니한다. - * - * 반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는 - * 이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가 - * 필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것. - */ - __approved?: boolean; - } + * 카드 만료 년도 (2 자리). + */ + cardExpirationYear: string & tags.Pattern<"\\d{2}">; + + /** + * 카드 만료 월 (2 자리). + */ + cardExpirationMonth: string & tags.Pattern<"^(0[1-9]|1[012])$">; + + /** + * 카드 비밀번호. + */ + cardPassword?: string; + + /** + * 할부 개월 수. + */ + cardInstallmentPlan?: number; + + /** + * 지불 총액. + */ + amount: number; + + /** + * 면세금 총액. + */ + taxFreeAmount?: number; + + /** + * 주문 식별자 키. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + */ + orderId: string; + + /** + * 주문 이름. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. + */ + orderName?: string; + + /** + * 고객의 생년월일. + * + * 표기 형식 YYMMDD. + */ + customerBirthday?: string & + tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; + + /** + * 고객의 이메일. + */ + customerEmail?: string & tags.Format<"email">; + + /** + * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. + * + * 3DS 인증 결과를 전송해야 하는 경우에만 필수. + */ + vbv?: { + /** + * 3D Secure 인증 세션에 대한 인증 값. + */ + cavv: string; + + /** + * 트랜잭션 ID. + */ + xid: string; + + /** + * 3DS 인증 결과에 대한 코드 값. + */ + eci: string; + }; + + /** + * 결제 승인 여부. + * + * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, + * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 + * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 + * 상황을 시뮬레이션할 수 있다. + * + * 본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고, + * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, + * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 + * 정식 결제로 인청치 아니한다. + * + * 반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는 + * 이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가 + * 필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것. + */ + __approved?: boolean; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts b/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts index 3d375d9..7245bba 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts @@ -6,144 +6,144 @@ import { tags } from "typia"; * @author Samchon */ export interface ITossCashReceipt { + /** + * 현금 영수증의 식별자 키. + */ + receiptKey: string; + + /** + * 현금 영수증의 종류. + */ + type: ITossCashReceipt.Type; + + /** + * 주문의 식별자 ID. + */ + orderId: string; + + /** + * 주문 이름. + */ + orderName: string; + + /** + * 현금 영수증 승인 번호. + */ + approvalNumber: string; + + /** + * 현금 영수증 승인 일시. + */ + approvedAt: string & tags.Format<"date-time">; + + /** + * 현금 영수증 취소 일시. + */ + canceledAt: null | (string & tags.Format<"date-time">); + + /** + * 영수증 URL. + */ + receiptUrl: string; + + /** + * @internal + */ + __paymentKey: string; +} +export namespace ITossCashReceipt { + /** + * 현금 영수증의 종류. + */ + export type Type = "소득공제" | "지출증빙"; + + /** + * 현금 영수증 요약 정보. + */ + export interface ISummary { /** - * 현금 영수증의 식별자 키. + * 현금 영수증의 종류. */ - receiptKey: string; + type: Type; /** - * 현금 영수증의 종류. + * 현금 영수증 처리된 금액. */ - type: ITossCashReceipt.Type; + amount: number; /** - * 주문의 식별자 ID. + * 면세 처리된 금액. */ - orderId: string; + taxFreeAmount: number; /** - * 주문 이름. + * 현금영수증 발급번호. */ - orderName: string; + issueNumber: string; + + /** + * 현금영수증 조회 페이지 주소. + */ + receiptUrl: string; + } + /** + * 현금 영수증 입력 정보. + */ + export interface IStore { /** - * 현금 영수증 승인 번호. + * 현금 영수증의 종류. */ - approvalNumber: string; + type: Type; /** - * 현금 영수증 승인 일시. + * 귀속 결제의 {@link ITossPayment.paymentKey}. */ - approvedAt: string & tags.Format<"date-time">; + paymentKey: string; /** - * 현금 영수증 취소 일시. + * 주문의 식별자 ID. */ - canceledAt: null | (string & tags.Format<"date-time">); + orderId: string; /** - * 영수증 URL. + * 주문 이름. */ - receiptUrl: string; + orderName: string; /** - * @internal + * 현금 영수증 발급을 위한 개인 식별 번호. + * + * 현금 영수증의 종류에 따라 휴대폰 번호나 주민등록번호 또는 사업자등록번호 및 + * 카드 번호를 입력할 수 있다. */ - __paymentKey: string; -} -export namespace ITossCashReceipt { + registrationNumber: string; + /** - * 현금 영수증의 종류. + * 현금 영수증을 발행할 금액. */ - export type Type = "소득공제" | "지출증빙"; + amount: number; /** - * 현금 영수증 요약 정보. + * 면세 금액. */ - export interface ISummary { - /** - * 현금 영수증의 종류. - */ - type: Type; - - /** - * 현금 영수증 처리된 금액. - */ - amount: number; - - /** - * 면세 처리된 금액. - */ - taxFreeAmount: number; - - /** - * 현금영수증 발급번호. - */ - issueNumber: string; - - /** - * 현금영수증 조회 페이지 주소. - */ - receiptUrl: string; - } + taxFreeAmount?: number; /** - * 현금 영수증 입력 정보. + * 사업자 등록번호. */ - export interface IStore { - /** - * 현금 영수증의 종류. - */ - type: Type; - - /** - * 귀속 결제의 {@link ITossPayment.paymentKey}. - */ - paymentKey: string; - - /** - * 주문의 식별자 ID. - */ - orderId: string; - - /** - * 주문 이름. - */ - orderName: string; - - /** - * 현금 영수증 발급을 위한 개인 식별 번호. - * - * 현금 영수증의 종류에 따라 휴대폰 번호나 주민등록번호 또는 사업자등록번호 및 - * 카드 번호를 입력할 수 있다. - */ - registrationNumber: string; - - /** - * 현금 영수증을 발행할 금액. - */ - amount: number; - - /** - * 면세 금액. - */ - taxFreeAmount?: number; - - /** - * 사업자 등록번호. - */ - businessNumber?: string; - } + businessNumber?: string; + } + /** + * 현금 영수증 취소 입력 정보. + */ + export interface ICancel { /** - * 현금 영수증 취소 입력 정보. + * 취소 금액. + * + * 미 입력시 현금 영수증에 기재된 {@link ITossCashReceipt.amount 총액}이 취소됨. */ - export interface ICancel { - /** - * 취소 금액. - * - * 미 입력시 현금 영수증에 기재된 {@link ITossCashReceipt.amount 총액}이 취소됨. - */ - amount?: number; - } + amount?: number; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts index 101f0ad..03046ec 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts @@ -6,25 +6,25 @@ import { ITossPayment } from "./ITossPayment"; * @author Samchon */ export interface ITossGiftCertificatePayment - extends ITossPayment.IBase<"상품권", "NORMAL"> { - /** - * 상품권 정보. - */ - giftCertificate: ITossGiftCertificatePayment.IGiftCertificate; + extends ITossPayment.IBase<"상품권", "NORMAL"> { + /** + * 상품권 정보. + */ + giftCertificate: ITossGiftCertificatePayment.IGiftCertificate; } export namespace ITossGiftCertificatePayment { + /** + * 상품권 정보. + */ + export interface IGiftCertificate { /** - * 상품권 정보. + * 승인 번호. */ - export interface IGiftCertificate { - /** - * 승인 번호. - */ - approveNo: string; + approveNo: string; - /** - * 정산 상태. - */ - settlementStatus: "COMPLETE" | "INCOMPLETE"; - } + /** + * 정산 상태. + */ + settlementStatus: "COMPLETE" | "INCOMPLETE"; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts index 3ad8764..3c61455 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts @@ -6,30 +6,30 @@ import { ITossPayment } from "./ITossPayment"; * @author Samchon */ export interface ITossMobilePhonePayment - extends ITossPayment.IBase<"휴대폰", "NORMAL"> { - /** - * 휴대폰 정보. - */ - mobilePhone: ITossMobilePhonePayment.IMobilePhone; + extends ITossPayment.IBase<"휴대폰", "NORMAL"> { + /** + * 휴대폰 정보. + */ + mobilePhone: ITossMobilePhonePayment.IMobilePhone; } export namespace ITossMobilePhonePayment { + /** + * 휴대폰 정보. + */ + export interface IMobilePhone { /** - * 휴대폰 정보. + * 통신사. */ - export interface IMobilePhone { - /** - * 통신사. - */ - carrier: string; + carrier: string; - /** - * 고객 휴대폰 번호. - */ - customerMobilePhone: string; + /** + * 고객 휴대폰 번호. + */ + customerMobilePhone: string; - /** - * 정산 상태. - */ - settlementStatus: "INCOMPLETED" | "COMPLETED"; - } + /** + * 정산 상태. + */ + settlementStatus: "INCOMPLETED" | "COMPLETED"; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts index 79ad1d7..d0d215f 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts @@ -24,203 +24,203 @@ import { ITossVirtualAccountPayment } from "./ITossVirtualAccountPayment"; * @author Samchon */ export type ITossPayment = - | ITossCardPayment - | ITossGiftCertificatePayment - | ITossMobilePhonePayment - | ITossTransferPayment - | ITossVirtualAccountPayment; + | ITossCardPayment + | ITossGiftCertificatePayment + | ITossMobilePhonePayment + | ITossTransferPayment + | ITossVirtualAccountPayment; export namespace ITossPayment { - /* ---------------------------------------------------------------- + /* ---------------------------------------------------------------- RESPONSE ---------------------------------------------------------------- */ + /** + * 결제의 기본 정보. + */ + export interface IBase< + Method extends string, + Type extends string, + Status extends string = + | "READY" + | "IN_PROGRESS" + | "WAITING_FOR_DEPOSIT" + | "DONE" + | "CANCELED" + | "PARTIAL_CANCELED" + | "ABORTED" + | "EXPIRED", + > { /** - * 결제의 기본 정보. - */ - export interface IBase< - Method extends string, - Type extends string, - Status extends string = - | "READY" - | "IN_PROGRESS" - | "WAITING_FOR_DEPOSIT" - | "DONE" - | "CANCELED" - | "PARTIAL_CANCELED" - | "ABORTED" - | "EXPIRED", - > { - /** - * 결제 수단. - */ - method: Method; - - /** - * 결제 타입. - * - * - NORMAL: 일반 결제 - * - BILLING: 미리 등록한 카드에 의한 간편 결제. - */ - type: Type; - - /** - * 결제 상태. - * - * - READY - * - IN_PROGRESS - * - WAITING_FOR_DEPOSIT - * - DONE - * - CANCELED - * - PARTIAL_CANCELED - * - ABORTED - * - EXPIRED - */ - status: Status; - - /** - * 가맹점 ID. - * - * 현재 tosspayments 가 쓰임. - */ - mId: string; - - /** - * 사용 중인 토스 페이먼츠 API 의 버전. - */ - version: string; - - /** - * 결제 내역의 식별자 번호. - */ - paymentKey: string; - - /** - * 주문 식별자 키. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - orderId: string; - - /** - * 거래 건에 대한 고유한 키 값. - * - * {@link paymentKey} 와 달리, 이를 사용할 일은 없더라. - */ - transactionKey: string; - - /** - * 주문 이름. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. - */ - orderName: string; - - /** - * 화폐 단위. - * - * 현재 토스 페이먼츠는 KRW 만 사용 가능. - */ - currency: string; - - /** - * 총 결제 금액. - */ - totalAmount: number; - - /** - * 취소할 수 있는 금액. - */ - balanceAmount: number; - - /** - * 공급가액. - */ - suppliedAmount: number; - - /** - * 면세액. - */ - taxFreeAmount: number; - - /** - * 부가세. - */ - vat: number; - - /** - * 에스크로 사용 여부. - */ - useEscrow: boolean; - - /** - * 문화비 지출 여부. - * - * 도석입, 공연 티켓, 박물관/미술관 입장권 등. - */ - cultureExpense: boolean; - - /** - * 결제 요청 일시. - */ - requestedAt: string & tags.Format<"date-time">; - - /** - * 결제 승인 일시. - */ - approvedAt: null | (string & tags.Format<"date-time">); - - /** - * 결제 취소 내역. - */ - cancels: null | ITossPaymentCancel[]; - - /** - * 현금 영수증 정보. - */ - cashReceipt: null | ITossCashReceipt.ISummary; - } - - /* ---------------------------------------------------------------- - REQUEST - ---------------------------------------------------------------- */ + * 결제 수단. + */ + method: Method; + /** - * 결제 승인 정보. + * 결제 타입. + * + * - NORMAL: 일반 결제 + * - BILLING: 미리 등록한 카드에 의한 간편 결제. */ - export interface IApproval { - /** - * 주문 식별자 키. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - orderId: string; + type: Type; - /** - * 결제 총액. - */ - amount: number; - } + /** + * 결제 상태. + * + * - READY + * - IN_PROGRESS + * - WAITING_FOR_DEPOSIT + * - DONE + * - CANCELED + * - PARTIAL_CANCELED + * - ABORTED + * - EXPIRED + */ + status: Status; /** - * 결제 신청 정보. + * 가맹점 ID. * - * `ITossPayment.IStore` 는 결제 신청 정보를 형상화한 자료구조이자 유니언 타입의 - * 인터페이스로써, if condition 을 이용하여 대상 method 를 특정하면, 파생 타입이 - * 자동으로 지정된다. + * 현재 tosspayments 가 쓰임. + */ + mId: string; + + /** + * 사용 중인 토스 페이먼츠 API 의 버전. + */ + version: string; + + /** + * 결제 내역의 식별자 번호. + */ + paymentKey: string; + + /** + * 주문 식별자 키. * - * ```typescript - * if (input.method === "card") - * input.cardNumber; // input is ITossCardPayment.IStore - * ``` - */ - export type IStore = - | ITossCardPayment.IStore - | ITossBilling.IPaymentStore - | ITossVirtualAccountPayment.IStore; - - // export interface IFinalize - // { - // paymentKey: string; - // orderId: string; - // amount: number; - // } + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + */ + orderId: string; + + /** + * 거래 건에 대한 고유한 키 값. + * + * {@link paymentKey} 와 달리, 이를 사용할 일은 없더라. + */ + transactionKey: string; + + /** + * 주문 이름. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. + */ + orderName: string; + + /** + * 화폐 단위. + * + * 현재 토스 페이먼츠는 KRW 만 사용 가능. + */ + currency: string; + + /** + * 총 결제 금액. + */ + totalAmount: number; + + /** + * 취소할 수 있는 금액. + */ + balanceAmount: number; + + /** + * 공급가액. + */ + suppliedAmount: number; + + /** + * 면세액. + */ + taxFreeAmount: number; + + /** + * 부가세. + */ + vat: number; + + /** + * 에스크로 사용 여부. + */ + useEscrow: boolean; + + /** + * 문화비 지출 여부. + * + * 도석입, 공연 티켓, 박물관/미술관 입장권 등. + */ + cultureExpense: boolean; + + /** + * 결제 요청 일시. + */ + requestedAt: string & tags.Format<"date-time">; + + /** + * 결제 승인 일시. + */ + approvedAt: null | (string & tags.Format<"date-time">); + + /** + * 결제 취소 내역. + */ + cancels: null | ITossPaymentCancel[]; + + /** + * 현금 영수증 정보. + */ + cashReceipt: null | ITossCashReceipt.ISummary; + } + + /* ---------------------------------------------------------------- + REQUEST + ---------------------------------------------------------------- */ + /** + * 결제 승인 정보. + */ + export interface IApproval { + /** + * 주문 식별자 키. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + */ + orderId: string; + + /** + * 결제 총액. + */ + amount: number; + } + + /** + * 결제 신청 정보. + * + * `ITossPayment.IStore` 는 결제 신청 정보를 형상화한 자료구조이자 유니언 타입의 + * 인터페이스로써, if condition 을 이용하여 대상 method 를 특정하면, 파생 타입이 + * 자동으로 지정된다. + * + * ```typescript + * if (input.method === "card") + * input.cardNumber; // input is ITossCardPayment.IStore + * ``` + */ + export type IStore = + | ITossCardPayment.IStore + | ITossBilling.IPaymentStore + | ITossVirtualAccountPayment.IStore; + + // export interface IFinalize + // { + // paymentKey: string; + // orderId: string; + // amount: number; + // } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts index f657ad6..dd70236 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts @@ -6,10 +6,45 @@ import { tags } from "typia"; * @author Samchon */ export interface ITossPaymentCancel { + /** + * 취소 총액. + */ + cancelAmount: number; + + /** + * 취소 사유. + */ + cancelReason: string; + + /** + * 면세 처리된 금액. + */ + taxFreeAmount: number; + + /** + * 과세 처리된 금액. + */ + taxAmount: number; + + /** + * 결제 취소 후 환불 가능 잔액. + */ + refundableAmount: number; + + /** + * 취소 일시. + */ + canceledAt: string & tags.Format<"date-time">; +} +export namespace ITossPaymentCancel { + /** + * 결제 취소 신청 정보. + */ + export interface IStore { /** - * 취소 총액. + * {@link ITossPayment} 의 식별자 키. */ - cancelAmount: number; + paymentKey: string; /** * 취소 사유. @@ -17,80 +52,45 @@ export interface ITossPaymentCancel { cancelReason: string; /** - * 면세 처리된 금액. + * 취소 총액. */ - taxFreeAmount: number; + cancelAmount?: number; /** - * 과세 처리된 금액. + * 환불 계좌 정보. + * + * 결제를 가상 계좌로 하였을 때에만 해당함. */ - taxAmount: number; + refundReceiveAccount?: { + /** + * 은행 정보. + */ + bank: string; + + /** + * 계좌 번호. + */ + accountNumber: string & tags.Pattern<"^[0-9]{0,20}$">; + + /** + * 예금주. + */ + holderName: string; + }; /** - * 결제 취소 후 환불 가능 잔액. + * 과세 처리 금액. */ - refundableAmount: number; + taxAmount?: number; /** - * 취소 일시. + * 면세 처리 금액. */ - canceledAt: string & tags.Format<"date-time">; -} -export namespace ITossPaymentCancel { + taxFreeAmount?: number; + /** - * 결제 취소 신청 정보. + * 결제 취소 후 환불 가능 잔액. */ - export interface IStore { - /** - * {@link ITossPayment} 의 식별자 키. - */ - paymentKey: string; - - /** - * 취소 사유. - */ - cancelReason: string; - - /** - * 취소 총액. - */ - cancelAmount?: number; - - /** - * 환불 계좌 정보. - * - * 결제를 가상 계좌로 하였을 때에만 해당함. - */ - refundReceiveAccount?: { - /** - * 은행 정보. - */ - bank: string; - - /** - * 계좌 번호. - */ - accountNumber: string & tags.Pattern<"^[0-9]{0,20}$">; - - /** - * 예금주. - */ - holderName: string; - }; - - /** - * 과세 처리 금액. - */ - taxAmount?: number; - - /** - * 면세 처리 금액. - */ - taxFreeAmount?: number; - - /** - * 결제 취소 후 환불 가능 잔액. - */ - refundableAmount?: number; - } + refundableAmount?: number; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts index 77e6a09..fcaf9d1 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts @@ -4,45 +4,41 @@ * @author Samchon */ export interface ITossPaymentWebhook { - /** - * 이벤트 타입. - */ - eventType: "PAYMENT_STATUS_CHANGED"; + /** + * 이벤트 타입. + */ + eventType: "PAYMENT_STATUS_CHANGED"; - /** - * 이벤트 데이터. - */ - data: ITossPaymentWebhook.IData; + /** + * 이벤트 데이터. + */ + data: ITossPaymentWebhook.IData; } export namespace ITossPaymentWebhook { + /** + * 웹훅 이벤트 데이터. + */ + export interface IData { /** - * 웹훅 이벤트 데이터. + * {@link ITossPayment} 의 식별자 키. */ - export interface IData { - /** - * {@link ITossPayment} 의 식별자 키. - */ - paymentKey: string; + paymentKey: string; - /** - * 주문 식별자 키. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - orderId: string; + /** + * 주문 식별자 키. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. + */ + orderId: string; - /** - * 결제 상태. - * - * - DONE: 결제 완료 - * - CANCELED: 결제가 취소됨 - * - PARTIAL_CANCELED: 결제가 부분 취소됨 - * - WAITING_FOR_DEPOSIT: 입금 대기 중 - */ - status: - | "DONE" - | "CANCELED" - | "PARTIAL_CANCELED" - | "WAITING_FOR_DEPOSIT"; - } + /** + * 결제 상태. + * + * - DONE: 결제 완료 + * - CANCELED: 결제가 취소됨 + * - PARTIAL_CANCELED: 결제가 부분 취소됨 + * - WAITING_FOR_DEPOSIT: 입금 대기 중 + */ + status: "DONE" | "CANCELED" | "PARTIAL_CANCELED" | "WAITING_FOR_DEPOSIT"; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts index ca22544..61b092f 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts @@ -6,25 +6,25 @@ import { ITossPayment } from "./ITossPayment"; * @author Samchon */ export interface ITossTransferPayment - extends ITossPayment.IBase<"계좌이체", "NORMAL"> { - /** - * 계좌 이체 정보. - */ - transfer: ITossTransferPayment.ITransfer; + extends ITossPayment.IBase<"계좌이체", "NORMAL"> { + /** + * 계좌 이체 정보. + */ + transfer: ITossTransferPayment.ITransfer; } export namespace ITossTransferPayment { + /** + * 계좌 이체 정보. + */ + export interface ITransfer { /** - * 계좌 이체 정보. + * 은행명. */ - export interface ITransfer { - /** - * 은행명. - */ - bank: string; + bank: string; - /** - * 이체 상태. - */ - settlementStatus: "INCOMPLETED" | "COMPLETED"; - } + /** + * 이체 상태. + */ + settlementStatus: "INCOMPLETED" | "COMPLETED"; + } } diff --git a/packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts index 7279d98..32f720e 100644 --- a/packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts @@ -8,129 +8,129 @@ import { ITossPayment } from "./ITossPayment"; * @author Samchon */ export interface ITossVirtualAccountPayment - extends ITossPayment.IBase<"가상계좌", "NORMAL"> { + extends ITossPayment.IBase<"가상계좌", "NORMAL"> { + /** + * 가상 계좌로 결제할 때 전달되는 입금 콜백을 검증하기 위한 값. + */ + secret: string; + + /** + * 가상 계좌 정보. + */ + virtualAccount: ITossVirtualAccountPayment.IVirtualAccount; +} +export namespace ITossVirtualAccountPayment { + /** + * 가상 계좌를 이용한 결제 신청 정보. + */ + export interface IStore { /** - * 가상 계좌로 결제할 때 전달되는 입금 콜백을 검증하기 위한 값. + * 결제 수단이 가상 계좌임을 의미. */ - secret: string; + method: "virtual-account"; /** - * 가상 계좌 정보. + * 주문 식별자 번호. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ - virtualAccount: ITossVirtualAccountPayment.IVirtualAccount; -} -export namespace ITossVirtualAccountPayment { + orderId: string; + + /** + * 주문 이름. + * + * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. + */ + orderName: string; + + /** + * 은행명. + */ + bank: string; + + /** + * 고객 이름. + */ + customerName: string; + + /** + * 결제 총액. + */ + amount: number; + + /** + * 결제 승인 여부. + * + * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, + * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 + * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 + * 상황을 시뮬레이션할 수 있다. + * + * 본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고, + * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, + * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 + * 정식 결제로 인청치 아니한다. + * + * 반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는 + * 이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가 + * 필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것. + */ + __approved?: boolean; + } + + /** + * 가상 계좌 정보. + */ + export interface IVirtualAccount { + /** + * 계좌 번호. + */ + accountNumber: string; + + /** + * 가상 계좌 타입. + */ + accountType: "일반" | "고정"; + + /** + * 은행명. + */ + bank: string; + + /** + * 고객 이름. + */ + customerName: string; + + /** + * 입금 기한. + */ + dueDate: string & tags.Format<"date">; + + /** + * 가상 계좌 만료 여부. + */ + expired: boolean; + /** - * 가상 계좌를 이용한 결제 신청 정보. + * 정산 상태. */ - export interface IStore { - /** - * 결제 수단이 가상 계좌임을 의미. - */ - method: "virtual-account"; - - /** - * 주문 식별자 번호. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. - */ - orderId: string; - - /** - * 주문 이름. - * - * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. - */ - orderName: string; - - /** - * 은행명. - */ - bank: string; - - /** - * 고객 이름. - */ - customerName: string; - - /** - * 결제 총액. - */ - amount: number; - - /** - * 결제 승인 여부. - * - * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, - * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 - * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 - * 상황을 시뮬레이션할 수 있다. - * - * 본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고, - * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, - * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 - * 정식 결제로 인청치 아니한다. - * - * 반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는 - * 이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가 - * 필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것. - */ - __approved?: boolean; - } + settlementStatus: "INCOMPLETED" | "COMPLETED"; /** - * 가상 계좌 정보. + * 환불 처리 상태. + * + * - NONE: 해당 없음 + * - FAILED: 환불 실패 + * - PENDING: 환불 처리중 + * - PARTIAL_FAILED: 부분 환불 실패 + * - COMPLETED: 환불 완료 */ - export interface IVirtualAccount { - /** - * 계좌 번호. - */ - accountNumber: string; - - /** - * 가상 계좌 타입. - */ - accountType: "일반" | "고정"; - - /** - * 은행명. - */ - bank: string; - - /** - * 고객 이름. - */ - customerName: string; - - /** - * 입금 기한. - */ - dueDate: string & tags.Format<"date">; - - /** - * 가상 계좌 만료 여부. - */ - expired: boolean; - - /** - * 정산 상태. - */ - settlementStatus: "INCOMPLETED" | "COMPLETED"; - - /** - * 환불 처리 상태. - * - * - NONE: 해당 없음 - * - FAILED: 환불 실패 - * - PENDING: 환불 처리중 - * - PARTIAL_FAILED: 부분 환불 실패 - * - COMPLETED: 환불 완료 - */ - refundStatus: - | "NONE" - | "FAILED" - | "PENDING" - | "PARTIAL_FAILED" - | "COMPLETED"; - } + refundStatus: + | "NONE" + | "FAILED" + | "PENDING" + | "PARTIAL_FAILED" + | "COMPLETED"; + } } diff --git a/packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts index 75e16ad..69722b4 100644 --- a/packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts @@ -1,145 +1,137 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller, ForbiddenException } from "@nestjs/common"; import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossCardPayment"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { v4 } from "uuid"; +import { FakeTossUserAuth } from "../decorators/FakeTossUserAuth"; import { FakeTossPaymentProvider } from "../providers/FakeTossPaymentProvider"; import { FakeTossStorage } from "../providers/FakeTossStorage"; -import { FakeTossUserAuth } from "../providers/FakeTossUserAuth"; -@nest.Controller("v1/billing") +@Controller("v1/billing") export class FakeTossBillingController { - /** - * 간편 결제 카드 등록하기. - * - * `billing.authorizations.card.store` 는 고객이 자신의 신록 카드를 서버에 등록해두고, - * 매번 결제가 필요할 때마다 카드 정보를 반복 입력하는 일 없이 간편하게 결제를 - * 진행하고자 할 때, 호출되는 API 함수이다. - * - * 참고로 `billing.authorizations.card.store` 는 클라이언트 어플리케이션이 토스 - * 페이먼츠가 제공하는 간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 - * 실 서비스에서 호출하는 일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 - * 상황을 시뮬레이션하기 위하여, 테스트 자동화 프로그램 수준에서 사용될 수는 있다. - * - * @param input 간편 결제 카드 등록 정보 - * @returns 간편 결제 카드 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post("authorizations/card") - public store( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: ITossBilling.IStore, - ): ITossBilling { - FakeTossUserAuth.authorize(request); + /** + * 간편 결제 카드 등록하기. + * + * `billing.authorizations.card.store` 는 고객이 자신의 신록 카드를 서버에 등록해두고, + * 매번 결제가 필요할 때마다 카드 정보를 반복 입력하는 일 없이 간편하게 결제를 + * 진행하고자 할 때, 호출되는 API 함수이다. + * + * 참고로 `billing.authorizations.card.store` 는 클라이언트 어플리케이션이 토스 + * 페이먼츠가 제공하는 간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 + * 실 서비스에서 호출하는 일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 + * 상황을 시뮬레이션하기 위하여, 테스트 자동화 프로그램 수준에서 사용될 수는 있다. + * + * @param input 간편 결제 카드 등록 정보 + * @returns 간편 결제 카드 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post("authorizations/card") + public store( + @FakeTossUserAuth() _0: void, + @core.TypedBody() input: ITossBilling.IStore, + ): ITossBilling { + const billing: ITossBilling = { + mId: "tosspyaments", + method: "카드", + billingKey: v4(), + customerKey: input.customerKey, + cardCompany: "신한", + cardNumber: input.cardNumber, + authenticatedAt: new Date().toISOString(), + }; + FakeTossStorage.billings.set(billing.billingKey, [billing, input]); + return billing; + } - const billing: ITossBilling = { - mId: "tosspyaments", - method: "카드", - billingKey: v4(), - customerKey: input.customerKey, - cardCompany: "신한", - cardNumber: input.cardNumber, - authenticatedAt: new Date().toISOString(), - }; - FakeTossStorage.billings.set(billing.billingKey, [billing, input]); - return billing; - } + /** + * 간편 결제로 등록한 수단 조회하기. + * + * `billing.authorizations.at` 은 고객이 간편 결제를 위하여 토스 페이먼츠 서버에 + * 등록한 결제 수단을 조회하는 함수이다. + * + * 주로 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 제공하는 결제 창을 사용하는 + * 경우, 그래서 프론트 어플리케이션이 귀하의 백엔드 서버에 `billingKey` 와` customerKey` + * 만을 전달해주어, 상세 간편 결제 수단 정보가 필요할 때 사용한다. + * + * @param billingKey 대상 정보의 {@link ITossBilling.billingKey} + * @param input 고객 식별자 키 + * @returns 간편 결제 수단 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post("authorizations/:billingKey") + public at( + @FakeTossUserAuth() _0: void, + @core.TypedParam("billingKey") billingKey: string, + @core.TypedBody() input: ITossBilling.ICustomerKey, + ): ITossBilling { + const tuple = FakeTossStorage.billings.get(billingKey); + if (tuple[0].customerKey !== input.customerKey) + throw new ForbiddenException("Different customer."); - /** - * 간편 결제로 등록한 수단 조회하기. - * - * `billing.authorizations.at` 은 고객이 간편 결제를 위하여 토스 페이먼츠 서버에 - * 등록한 결제 수단을 조회하는 함수이다. - * - * 주로 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 제공하는 결제 창을 사용하는 - * 경우, 그래서 프론트 어플리케이션이 귀하의 백엔드 서버에 `billingKey` 와` customerKey` - * 만을 전달해주어, 상세 간편 결제 수단 정보가 필요할 때 사용한다. - * - * @param billingKey 대상 정보의 {@link ITossBilling.billingKey} - * @param input 고객 식별자 키 - * @returns 간편 결제 수단 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post("authorizations/:billingKey") - public at( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("billingKey") billingKey: string, - @core.TypedBody() input: ITossBilling.ICustomerKey, - ): ITossBilling { - FakeTossUserAuth.authorize(request); + return tuple[0]; + } - const tuple = FakeTossStorage.billings.get(billingKey); - if (tuple[0].customerKey !== input.customerKey) - throw new nest.ForbiddenException("Different customer."); + /** + * 간편 결제에 등록한 수단으로 결제하기. + * + * `billing.pay` 는 간편 결제에 등록한 수단으로 결제를 진행하고자 할 때 호출하는 API + * 함수이다. + * + * 그리고 `billing.pay` 는 결제 수단 중 유일하게, 클라이언트 어플리케이션이 토스 + * 페이먼츠가 제공하는 결제 창을 사용할 수 없어, 귀하의 백엔드 서버가 토스 페이먼츠의 + * API 함수를 직접 호출해야 하는 경우에 해당한다. 따라서 간편 결제에 관련하여 토스 + * 페이먼츠와 연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 + * 대한 별도의 설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다. + * + * 더하여 `billing.pay` 는 철저히 귀사 백엔드 서버의 판단 아래 호출되는 API 함수인지라, + * 이를 통하여 이루어지는 결제는 일절 {@link payments.approve} 가 필요 없다. 다만 + * `billing.pay` 는 이처럼 부차적인 승인 과정 필요없이 그 즉시로 결제가 완성되니, 이를 + * 호출하는 상황에 대하여 세심히 주의를 기울일 필요가 있다 + * + * @param billingKey 간편 결제에 등록한 수단의 {@link ITossBilling.billingKey} + * @param input 주문 정보 + * @returns 결제 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post(":billingKey") + public pay( + @FakeTossUserAuth() _0: void, + @core.TypedParam("billingKey") billingKey: string, + @core.TypedBody() input: ITossBilling.IPaymentStore, + ): ITossPayment { + const tuple = FakeTossStorage.billings.get(billingKey); + const card: ITossBilling.IStore = tuple[1]; - return tuple[0]; - } - - /** - * 간편 결제에 등록한 수단으로 결제하기. - * - * `billing.pay` 는 간편 결제에 등록한 수단으로 결제를 진행하고자 할 때 호출하는 API - * 함수이다. - * - * 그리고 `billing.pay` 는 결제 수단 중 유일하게, 클라이언트 어플리케이션이 토스 - * 페이먼츠가 제공하는 결제 창을 사용할 수 없어, 귀하의 백엔드 서버가 토스 페이먼츠의 - * API 함수를 직접 호출해야 하는 경우에 해당한다. 따라서 간편 결제에 관련하여 토스 - * 페이먼츠와 연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 - * 대한 별도의 설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다. - * - * 더하여 `billing.pay` 는 철저히 귀사 백엔드 서버의 판단 아래 호출되는 API 함수인지라, - * 이를 통하여 이루어지는 결제는 일절 {@link payments.approve} 가 필요 없다. 다만 - * `billing.pay` 는 이처럼 부차적인 승인 과정 필요없이 그 즉시로 결제가 완성되니, 이를 - * 호출하는 상황에 대하여 세심히 주의를 기울일 필요가 있다 - * - * @param billingKey 간편 결제에 등록한 수단의 {@link ITossBilling.billingKey} - * @param input 주문 정보 - * @returns 결제 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post(":billingKey") - public pay( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("billingKey") billingKey: string, - @core.TypedBody() input: ITossBilling.IPaymentStore, - ): ITossPayment { - FakeTossUserAuth.authorize(request); - - const tuple = FakeTossStorage.billings.get(billingKey); - const card: ITossBilling.IStore = tuple[1]; - - const payment: ITossCardPayment = { - ...FakeTossPaymentProvider.get_common_props(input), - method: "카드", - type: "NORMAL", - status: "DONE", - approvedAt: new Date().toISOString(), - discount: null, - card: { - company: "신한카드", - number: card.cardNumber, - installmentPlanMonths: 0, - isInterestFree: true, - approveNo: "temporary-card-approval-number", - useCardPoint: false, - cardType: "신용", - ownerType: "개인", - acquireStatus: "READY", - receiptUrl: - "https://github.com/samchon/fake-toss-payments-server", - }, - easyPay: null, - }; - FakeTossStorage.payments.set(payment.paymentKey, payment); - return payment; - } + const payment: ITossCardPayment = { + ...FakeTossPaymentProvider.get_common_props(input), + method: "카드", + type: "NORMAL", + status: "DONE", + approvedAt: new Date().toISOString(), + discount: null, + card: { + company: "신한카드", + number: card.cardNumber, + installmentPlanMonths: 0, + isInterestFree: true, + approveNo: "temporary-card-approval-number", + useCardPoint: false, + cardType: "신용", + ownerType: "개인", + acquireStatus: "READY", + receiptUrl: "https://github.com/samchon/fake-toss-payments-server", + }, + easyPay: null, + }; + FakeTossStorage.payments.set(payment.paymentKey, payment); + return payment; + } } diff --git a/packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts index 446c01b..3e50dff 100644 --- a/packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts @@ -1,103 +1,99 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { + Controller, + NotFoundException, + UnprocessableEntityException, +} from "@nestjs/common"; import { ITossCashReceipt } from "toss-payments-server-api/lib/structures/ITossCashReceipt"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { v4 } from "uuid"; +import { FakeTossUserAuth } from "../decorators/FakeTossUserAuth"; import { FakeTossStorage } from "../providers/FakeTossStorage"; -import { FakeTossUserAuth } from "../providers/FakeTossUserAuth"; -@nest.Controller("v1/cash-receipts") +@Controller("v1/cash-receipts") export class FakeTossCashReceiptsController { - /** - * 현금 영수증 발행하기. - * - * @param input 입력 정보 - * @returns 현금 영수증 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post() - public store( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: ITossCashReceipt.IStore, - ): ITossCashReceipt { - // VALIADTE - FakeTossUserAuth.authorize(request); + /** + * 현금 영수증 발행하기. + * + * @param input 입력 정보 + * @returns 현금 영수증 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post() + public store( + @FakeTossUserAuth() _0: void, + @core.TypedBody() input: ITossCashReceipt.IStore, + ): ITossCashReceipt { + // CHECK PAYMENT + const payment: ITossPayment = FakeTossStorage.payments.get( + input.paymentKey, + ); + if (payment.orderId !== input.orderId) + throw new NotFoundException("Wrong orderId"); + else if (payment.cashReceipt !== null) + throw new UnprocessableEntityException("Duplicated cash receipt exists."); + else if (payment.totalAmount < input.amount) + throw new UnprocessableEntityException( + "Input amount is greater than its payment.", + ); - // CHECK PAYMENT - const payment: ITossPayment = FakeTossStorage.payments.get( - input.paymentKey, - ); - if (payment.orderId !== input.orderId) - throw new nest.NotFoundException("Wrong orderId"); - else if (payment.cashReceipt !== null) - throw new nest.UnprocessableEntityException( - "Duplicated cash receipt exists.", - ); - else if (payment.totalAmount < input.amount) - throw new nest.UnprocessableEntityException( - "Input amount is greater than its payment.", - ); + // CONSTRUCT + const receipt: ITossCashReceipt = { + orderId: input.orderId, + orderName: input.orderName, + type: input.type, + receiptKey: v4(), + approvalNumber: v4(), + approvedAt: new Date().toISOString(), + canceledAt: null, + receiptUrl: "https://github.com/samchon/fake-toss-payments-server", + __paymentKey: payment.paymentKey, + }; + FakeTossStorage.cash_receipts.set(receipt.receiptKey, receipt); + payment.cashReceipt = { + type: receipt.type, + amount: input.amount, + taxFreeAmount: input.taxFreeAmount || 0, + issueNumber: receipt.approvalNumber, + receiptUrl: receipt.receiptUrl, + }; - // CONSTRUCT - const receipt: ITossCashReceipt = { - orderId: input.orderId, - orderName: input.orderName, - type: input.type, - receiptKey: v4(), - approvalNumber: v4(), - approvedAt: new Date().toISOString(), - canceledAt: null, - receiptUrl: "https://github.com/samchon/fake-toss-payments-server", - __paymentKey: payment.paymentKey, - }; - FakeTossStorage.cash_receipts.set(receipt.receiptKey, receipt); - payment.cashReceipt = { - type: receipt.type, - amount: input.amount, - taxFreeAmount: input.taxFreeAmount || 0, - issueNumber: receipt.approvalNumber, - receiptUrl: receipt.receiptUrl, - }; + // RETURNS + return receipt; + } - // RETURNS - return receipt; - } + /** + * 현금 영수증 취소하기. + * + * @param receiptKey 현금 영수증의 {@link ITossCashReceipt.receiptKey} + * @param input 취소 입력 정보 + * @returns 취소된 현금 영수증 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post(":receiptKey/cancel") + public cancel( + @FakeTossUserAuth() _0: void, + @core.TypedParam("receiptKey") receiptKey: string, + @core.TypedBody() input: ITossCashReceipt.ICancel, + ): ITossCashReceipt { + input; - /** - * 현금 영수증 취소하기. - * - * @param receiptKey 현금 영수증의 {@link ITossCashReceipt.receiptKey} - * @param input 취소 입력 정보 - * @returns 취소된 현금 영수증 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post(":receiptKey/cancel") - public cancel( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("receiptKey") receiptKey: string, - @core.TypedBody() input: ITossCashReceipt.ICancel, - ): ITossCashReceipt { - // VALIADTE - FakeTossUserAuth.authorize(request); - input; + // GET RECORDS + const receipt: ITossCashReceipt = + FakeTossStorage.cash_receipts.get(receiptKey); + const payment: ITossPayment = FakeTossStorage.payments.get( + receipt.__paymentKey, + ); - // GET RECORDS - const receipt: ITossCashReceipt = - FakeTossStorage.cash_receipts.get(receiptKey); - const payment: ITossPayment = FakeTossStorage.payments.get( - receipt.__paymentKey, - ); + // CHANGE + receipt.canceledAt = new Date().toISOString(); + payment.cashReceipt = null; - // CHANGE - receipt.canceledAt = new Date().toISOString(); - payment.cashReceipt = null; - - return receipt; - } + return receipt; + } } diff --git a/packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts index 4d88466..318d0d8 100644 --- a/packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts @@ -1,79 +1,76 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller, UnprocessableEntityException } from "@nestjs/common"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossPaymentWebhook } from "toss-payments-server-api/lib/structures/ITossPaymentWebhook"; +import { FakeTossUserAuth } from "../decorators/FakeTossUserAuth"; import { FakeTossStorage } from "../providers/FakeTossStorage"; -import { FakeTossUserAuth } from "../providers/FakeTossUserAuth"; import { FakeTossWebhookProvider } from "../providers/FakeTossWebhookProvider"; -@nest.Controller("internal") +@Controller("internal") export class FakeTossInternalController { - /** - * 웹훅 이벤트 더미 리스너. - * - * `internal.webhook` 은 실제 토스 페이먼츠의 결제 서버에는 존재하지 않는 API 로써, - * `fake-toss-payments-server` 의 {@link Configuration.WEBHOOK_URL} 에 아무런 URL 을 - * 설정하지 않으면, `fake-toss-payments-server` 로부터 발생하는 모든 종류의 웹훅 - * 이벤트는 이 곳으로 전달되어 무의미하게 사라진다. - * - * 따라서 `fake-toss-payments-server` 를 사용하여 토스 페이먼츠 서버와의 연동을 미리 - * 검증코자 할 때는, 반드시 {@link Configuration.WEBHOOK_URL} 를 설정하여 웹훅 - * 이벤트가 귀하의 백엔드 서버로 제대로 전달되도록 하자. - * - * @param input 웹훅 이벤트 정보 - * @author Samchon - */ - @core.TypedRoute.Post("webhook") - public webhook(@core.TypedBody() input: ITossPaymentWebhook): void { - const payment = FakeTossStorage.payments.get(input.data.paymentKey); - payment.status = input.data.status; + /** + * 웹훅 이벤트 더미 리스너. + * + * `internal.webhook` 은 실제 토스 페이먼츠의 결제 서버에는 존재하지 않는 API 로써, + * `fake-toss-payments-server` 의 {@link Configuration.WEBHOOK_URL} 에 아무런 URL 을 + * 설정하지 않으면, `fake-toss-payments-server` 로부터 발생하는 모든 종류의 웹훅 + * 이벤트는 이 곳으로 전달되어 무의미하게 사라진다. + * + * 따라서 `fake-toss-payments-server` 를 사용하여 토스 페이먼츠 서버와의 연동을 미리 + * 검증코자 할 때는, 반드시 {@link Configuration.WEBHOOK_URL} 를 설정하여 웹훅 + * 이벤트가 귀하의 백엔드 서버로 제대로 전달되도록 하자. + * + * @param input 웹훅 이벤트 정보 + * @author Samchon + */ + @core.TypedRoute.Post("webhook") + public webhook(@core.TypedBody() input: ITossPaymentWebhook): void { + const payment = FakeTossStorage.payments.get(input.data.paymentKey); + payment.status = input.data.status; - FakeTossStorage.webhooks.set(input.data.paymentKey, input); - } + FakeTossStorage.webhooks.set(input.data.paymentKey, input); + } - /** - * 가상 계좌에 입금하기. - * - * `internal.virtual_accounts.deposit` 은 실제 토스 페이먼츠의 결제 서버에는 존재하지 - * 않는 API 로써, 가상 계좌 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 - * 상황을 시뮬레이션할 수 있는 함수이다. - * - * 즉 `internal.virtual_accounts.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 - * 입금을 하고, 그에 따라 토스 페이먼츠 서버에서 webhook 이벤트가 발생하여 이를 귀하의 - * 백엔드 서버로 전송하는 일련의 상황을 테스트하기 위한 함수인 셈이다. - * - * @param paymentKey 대상 가상 계좌 결제 정보의 {@link ITossPayment.paymentKey} - * @returns 입금 완료된 가상 꼐좌 결제 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Put(":paymentKey/deposit") - public deposit( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("paymentKey") paymentKey: string, - ): ITossPayment { - FakeTossUserAuth.authorize(request); + /** + * 가상 계좌에 입금하기. + * + * `internal.virtual_accounts.deposit` 은 실제 토스 페이먼츠의 결제 서버에는 존재하지 + * 않는 API 로써, 가상 계좌 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 + * 상황을 시뮬레이션할 수 있는 함수이다. + * + * 즉 `internal.virtual_accounts.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 + * 입금을 하고, 그에 따라 토스 페이먼츠 서버에서 webhook 이벤트가 발생하여 이를 귀하의 + * 백엔드 서버로 전송하는 일련의 상황을 테스트하기 위한 함수인 셈이다. + * + * @param paymentKey 대상 가상 계좌 결제 정보의 {@link ITossPayment.paymentKey} + * @returns 입금 완료된 가상 꼐좌 결제 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Put(":paymentKey/deposit") + public deposit( + @FakeTossUserAuth() _0: void, + @core.TypedParam("paymentKey") paymentKey: string, + ): ITossPayment { + const payment: ITossPayment = FakeTossStorage.payments.get(paymentKey); + if (payment.method !== "가상계좌") + throw new UnprocessableEntityException("Invalid target."); - const payment: ITossPayment = FakeTossStorage.payments.get(paymentKey); - if (payment.method !== "가상계좌") - throw new nest.UnprocessableEntityException("Invalid target."); + payment.virtualAccount.settlementStatus = "COMPLETED"; + payment.approvedAt = new Date().toString(); + payment.status = "DONE"; - payment.virtualAccount.settlementStatus = "COMPLETED"; - payment.approvedAt = new Date().toString(); - payment.status = "DONE"; + FakeTossWebhookProvider.webhook({ + eventType: "PAYMENT_STATUS_CHANGED", + data: { + paymentKey: payment.paymentKey, + orderId: payment.orderId, + status: "DONE", + }, + }).catch(() => {}); - FakeTossWebhookProvider.webhook({ - eventType: "PAYMENT_STATUS_CHANGED", - data: { - paymentKey: payment.paymentKey, - orderId: payment.orderId, - status: "DONE", - }, - }).catch(() => {}); - - return payment; - } + return payment; + } } diff --git a/packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts index f8d1b0a..7eaa5e9 100644 --- a/packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts @@ -1,201 +1,186 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller, UnprocessableEntityException } from "@nestjs/common"; import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossCardPayment"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossPaymentCancel } from "toss-payments-server-api/lib/structures/ITossPaymentCancel"; +import { FakeTossUserAuth } from "../decorators/FakeTossUserAuth"; import { FakeTossPaymentProvider } from "../providers/FakeTossPaymentProvider"; import { FakeTossStorage } from "../providers/FakeTossStorage"; -import { FakeTossUserAuth } from "../providers/FakeTossUserAuth"; import { FakeTossWebhookProvider } from "../providers/FakeTossWebhookProvider"; -@nest.Controller("v1/payments") +@Controller("v1/payments") export class FakeTossPaymentsController { - /** - * 결제 정보 조회하기. - * - * `payments.at` 은 결제 정보를 조회하는 함수이다. - * - * 주로 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 제공하는 결제 창을 사용하는 - * 경우, 그래서 프론트 어플리케이션이 귀하의 백엔드 서버에 `paymentKey` 등 극히 일부의 - * 식별자 정보만을 전달해주어, 상세 결제 정보가 필요할 때 사용한다. - * - * 참고로 토스 페이먼츠는 다른 결제 PG 사들과 다르게, 클라이언트 어플리케이션에서 토스 - * 페이먼츠의 결제 창을 이용하여 진행한 결제가 바로 확정되는 것은 아니다. 귀사의 백엔드 - * 서버가 현재의 `payments.at` 을 통하여 해당 결제 정보를 확인하고, {@link approve} 를 - * 호출하여 직접 승인하기 전까지, 해당 결제는 확정되지 않으니, 이 점에 유의하기 바란다. - * - * @param paymentKey 결제 정보의 {@link ITossPayment.paymentKey} - * @returns 결제 정보 - * - * @author Samchon - */ - @core.TypedRoute.Get(":paymentKey") - public at( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("paymentKey") paymentKey: string, - ): ITossPayment { - FakeTossUserAuth.authorize(request); - return FakeTossStorage.payments.get(paymentKey); - } + /** + * 결제 정보 조회하기. + * + * `payments.at` 은 결제 정보를 조회하는 함수이다. + * + * 주로 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 제공하는 결제 창을 사용하는 + * 경우, 그래서 프론트 어플리케이션이 귀하의 백엔드 서버에 `paymentKey` 등 극히 일부의 + * 식별자 정보만을 전달해주어, 상세 결제 정보가 필요할 때 사용한다. + * + * 참고로 토스 페이먼츠는 다른 결제 PG 사들과 다르게, 클라이언트 어플리케이션에서 토스 + * 페이먼츠의 결제 창을 이용하여 진행한 결제가 바로 확정되는 것은 아니다. 귀사의 백엔드 + * 서버가 현재의 `payments.at` 을 통하여 해당 결제 정보를 확인하고, {@link approve} 를 + * 호출하여 직접 승인하기 전까지, 해당 결제는 확정되지 않으니, 이 점에 유의하기 바란다. + * + * @param paymentKey 결제 정보의 {@link ITossPayment.paymentKey} + * @returns 결제 정보 + * + * @author Samchon + */ + @core.TypedRoute.Get(":paymentKey") + public at( + @FakeTossUserAuth() _0: void, + @core.TypedParam("paymentKey") paymentKey: string, + ): ITossPayment { + return FakeTossStorage.payments.get(paymentKey); + } - /** - * 카드로 결제하기. - * - * `payments.key_in` 은 카드를 이용한 결제를 할 때 호출되는 API 함수이다. - * - * 참고로 `payments.key_in` 는 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 - * 제공하는 결제 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 - * 일은 없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여, - * 테스트 자동화 프로그램 수준에서 사용될 수는 있다. - * - * 그리고 귀하의 백엔드 서버가 `payments.key-in` 을 직접 호출하는 경우, 토스 페이먼츠 - * 서버는 이를 완전히 승인된 결제로 보고 바로 확정한다. 때문에 `payments.key-in` 을 - * 직접 호출하는 경우, 토스 페이먼츠의 결제 창을 이용하여 별도의 {@link approve} 가 - * 필요한 때 대비, 훨씬 더 세심한 주의가 요구된다. - * - * 더하여 만약 귀하의 백엔드 서버가 `fake-toss-payments-server` 를 이용하여 고객의 - * 카드 결제를 시뮬레이션하는 경우, {@link ITossCardPayment.IStore.__approved} 값을 - * `false` 로 하여 카드 결제의 확정을 고의로 회피할 수 있다. 이를 통하여 토스 - * 페이먼츠의 결제 창을 이용한 카드 결제의 경우, 별도의 {@link approve} 가 필요한 - * 상황을 시뮬레이션 할 수 있다. - * - * @param input 카드 결제 입력 정보 - * @returns 카드 결제 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post("key-in") - public key_in( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: ITossCardPayment.IStore, - ): ITossCardPayment { - FakeTossUserAuth.authorize(request); + /** + * 카드로 결제하기. + * + * `payments.key_in` 은 카드를 이용한 결제를 할 때 호출되는 API 함수이다. + * + * 참고로 `payments.key_in` 는 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 + * 제공하는 결제 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 + * 일은 없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여, + * 테스트 자동화 프로그램 수준에서 사용될 수는 있다. + * + * 그리고 귀하의 백엔드 서버가 `payments.key-in` 을 직접 호출하는 경우, 토스 페이먼츠 + * 서버는 이를 완전히 승인된 결제로 보고 바로 확정한다. 때문에 `payments.key-in` 을 + * 직접 호출하는 경우, 토스 페이먼츠의 결제 창을 이용하여 별도의 {@link approve} 가 + * 필요한 때 대비, 훨씬 더 세심한 주의가 요구된다. + * + * 더하여 만약 귀하의 백엔드 서버가 `fake-toss-payments-server` 를 이용하여 고객의 + * 카드 결제를 시뮬레이션하는 경우, {@link ITossCardPayment.IStore.__approved} 값을 + * `false` 로 하여 카드 결제의 확정을 고의로 회피할 수 있다. 이를 통하여 토스 + * 페이먼츠의 결제 창을 이용한 카드 결제의 경우, 별도의 {@link approve} 가 필요한 + * 상황을 시뮬레이션 할 수 있다. + * + * @param input 카드 결제 입력 정보 + * @returns 카드 결제 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post("key-in") + public key_in( + @FakeTossUserAuth() _0: void, + @core.TypedBody() input: ITossCardPayment.IStore, + ): ITossCardPayment { + const payment: ITossCardPayment = { + ...FakeTossPaymentProvider.get_common_props(input), + method: "카드", + type: "NORMAL", + status: input.__approved !== false ? "DONE" : "IN_PROGRESS", + approvedAt: input.__approved !== false ? new Date().toISOString() : null, + discount: null, + card: { + company: "신한카드", + number: input.cardNumber, + installmentPlanMonths: input.cardInstallmentPlan || 0, + isInterestFree: true, + approveNo: "temporary-card-approval-number", + useCardPoint: false, + cardType: "신용", + ownerType: "개인", + acquireStatus: "READY", + receiptUrl: "https://github.com/samchon/fake-toss-payments-server", + }, + easyPay: null, + }; + FakeTossStorage.payments.set(payment.paymentKey, payment); + return payment; + } - const payment: ITossCardPayment = { - ...FakeTossPaymentProvider.get_common_props(input), - method: "카드", - type: "NORMAL", - status: input.__approved !== false ? "DONE" : "IN_PROGRESS", - approvedAt: - input.__approved !== false ? new Date().toISOString() : null, - discount: null, - card: { - company: "신한카드", - number: input.cardNumber, - installmentPlanMonths: input.cardInstallmentPlan || 0, - isInterestFree: true, - approveNo: "temporary-card-approval-number", - useCardPoint: false, - cardType: "신용", - ownerType: "개인", - acquireStatus: "READY", - receiptUrl: - "https://github.com/samchon/fake-toss-payments-server", - }, - easyPay: null, - }; - FakeTossStorage.payments.set(payment.paymentKey, payment); - return payment; - } + /** + * 결제 승인하기. + * + * 토스 페이먼츠는 귀사의 백엔드에서 일어난 결제가 아닌 프론트 어플리케이션의 결제 창에서 + * 이루어진 결제의 경우, 해당 서비스으 백엔드 서버로부터 결제를 승인받기 전까지, 이를 + * 확정하지 않는다. `payments.approve` 는 바로 이러한 상황에서, 해당 결제를 승인해주는 + * 함수이다. + * + * 만일 귀하가 `fake-toss-payments-server` 를 이용하여 결제를 시뮬레이션하는 경우라면, + * 결제 관련 API 를 호출함에 있어 {@link ITossCardPayment.IStore.__approved} 내지 + * {@link ITossVirtualAccountPayment.IStore.__approved} 를 `false` 로 함으로써, 별도 + * 승인이 필요한 이러한 상황을 시뮬레이션 할 수 있다. + * + * @param paymentKey 대상 결제의 {@link ITossPayment.paymentKey} + * @param input 주문 정보 확인 + * @returns 승인된 결제 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post(":paymentKey") + public approve( + @FakeTossUserAuth() _0: void, + @core.TypedParam("paymentKey") paymentKey: string, + @core.TypedBody() input: ITossPayment.IApproval, + ): ITossPayment { + const payment: ITossPayment = FakeTossStorage.payments.get(paymentKey); + if (payment.orderId !== input.orderId) + throw new UnprocessableEntityException("Wrong orderId"); + else if (payment.totalAmount !== input.amount) + throw new UnprocessableEntityException("Wrong price."); - /** - * 결제 승인하기. - * - * 토스 페이먼츠는 귀사의 백엔드에서 일어난 결제가 아닌 프론트 어플리케이션의 결제 창에서 - * 이루어진 결제의 경우, 해당 서비스으 백엔드 서버로부터 결제를 승인받기 전까지, 이를 - * 확정하지 않는다. `payments.approve` 는 바로 이러한 상황에서, 해당 결제를 승인해주는 - * 함수이다. - * - * 만일 귀하가 `fake-toss-payments-server` 를 이용하여 결제를 시뮬레이션하는 경우라면, - * 결제 관련 API 를 호출함에 있어 {@link ITossCardPayment.IStore.__approved} 내지 - * {@link ITossVirtualAccountPayment.IStore.__approved} 를 `false` 로 함으로써, 별도 - * 승인이 필요한 이러한 상황을 시뮬레이션 할 수 있다. - * - * @param paymentKey 대상 결제의 {@link ITossPayment.paymentKey} - * @param input 주문 정보 확인 - * @returns 승인된 결제 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post(":paymentKey") - public approve( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("paymentKey") paymentKey: string, - @core.TypedBody() input: ITossPayment.IApproval, - ): ITossPayment { - FakeTossUserAuth.authorize(request); + payment.approvedAt = new Date().toISOString(); + if (payment.method !== "가상계좌" && payment.method !== "계좌이체") + payment.status = "DONE"; + return payment; + } - const payment: ITossPayment = FakeTossStorage.payments.get(paymentKey); - if (payment.orderId !== input.orderId) - throw new nest.UnprocessableEntityException("Wrong orderId"); - else if (payment.totalAmount !== input.amount) - throw new nest.UnprocessableEntityException("Wrong price."); + /** + * 결제 취소하기. + * + * `payments.cancel` 은 결제를 취소하는 API 이다. + * + * 결제 취소 입력 정보 {@link ITossPaymentCancel.IStore} 에는 취소 사유를 비롯하여, + * 고객에게 환불해 줄 금액과 부가세 및 필요시 환불 계좌 정보 등을 입력하게 되어있다. + * + * @param paymentKey 결제 정보의 {@link ITossPayment.paymentKey} + * @param input 취소 입력 정보 + * @returns 취소된 결제 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post(":paymentKey/cancel") + public cancel( + @FakeTossUserAuth() _0: void, + @core.TypedParam("paymentKey") paymentKey: string, + @core.TypedBody() input: ITossPaymentCancel.IStore, + ): ITossPayment { + const payment: ITossPayment = FakeTossStorage.payments.get(paymentKey); + const amount: number = input.cancelAmount ?? payment.totalAmount; - payment.approvedAt = new Date().toISOString(); - if (payment.method !== "가상계좌" && payment.method !== "계좌이체") - payment.status = "DONE"; - return payment; - } + if (payment.balanceAmount < amount) + throw new UnprocessableEntityException("Balance amount is not enough."); - /** - * 결제 취소하기. - * - * `payments.cancel` 은 결제를 취소하는 API 이다. - * - * 결제 취소 입력 정보 {@link ITossPaymentCancel.IStore} 에는 취소 사유를 비롯하여, - * 고객에게 환불해 줄 금액과 부가세 및 필요시 환불 계좌 정보 등을 입력하게 되어있다. - * - * @param paymentKey 결제 정보의 {@link ITossPayment.paymentKey} - * @param input 취소 입력 정보 - * @returns 취소된 결제 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post(":paymentKey/cancel") - public cancel( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedParam("paymentKey") paymentKey: string, - @core.TypedBody() input: ITossPaymentCancel.IStore, - ): ITossPayment { - FakeTossUserAuth.authorize(request); + payment.status = "CANCELED"; + payment.cancels ??= []; + payment.cancels.push({ + cancelAmount: amount, + cancelReason: input.cancelReason, + taxFreeAmount: input.taxFreeAmount ?? 0, + taxAmount: input.taxAmount ?? 0, + refundableAmount: input.refundableAmount ?? payment.totalAmount, + canceledAt: new Date().toISOString(), + }); + payment.balanceAmount -= amount; - const payment: ITossPayment = FakeTossStorage.payments.get(paymentKey); - const amount: number = input.cancelAmount ?? payment.totalAmount; + FakeTossWebhookProvider.webhook({ + eventType: "PAYMENT_STATUS_CHANGED", + data: { + paymentKey: payment.paymentKey, + orderId: payment.orderId, + status: payment.balanceAmount === 0 ? "CANCELED" : "PARTIAL_CANCELED", + }, + }).catch(() => {}); - if (payment.balanceAmount < amount) - throw new nest.UnprocessableEntityException( - "Balance amount is not enough.", - ); - - payment.status = "CANCELED"; - payment.cancels ??= []; - payment.cancels.push({ - cancelAmount: amount, - cancelReason: input.cancelReason, - taxFreeAmount: input.taxFreeAmount ?? 0, - taxAmount: input.taxAmount ?? 0, - refundableAmount: input.refundableAmount ?? payment.totalAmount, - canceledAt: new Date().toISOString(), - }); - payment.balanceAmount -= amount; - - FakeTossWebhookProvider.webhook({ - eventType: "PAYMENT_STATUS_CHANGED", - data: { - paymentKey: payment.paymentKey, - orderId: payment.orderId, - status: - payment.balanceAmount === 0 - ? "CANCELED" - : "PARTIAL_CANCELED", - }, - }).catch(() => {}); - - return payment; - } + return payment; + } } diff --git a/packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts index 0c3c804..4ccb737 100644 --- a/packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts @@ -1,81 +1,75 @@ import core from "@nestia/core"; -import * as nest from "@nestjs/common"; -import * as fastify from "fastify"; +import { Controller } from "@nestjs/common"; import { ITossVirtualAccountPayment } from "toss-payments-server-api/lib/structures/ITossVirtualAccountPayment"; import { v4 } from "uuid"; +import { FakeTossUserAuth } from "../decorators/FakeTossUserAuth"; import { FakeTossPaymentProvider } from "../providers/FakeTossPaymentProvider"; import { FakeTossStorage } from "../providers/FakeTossStorage"; -import { FakeTossUserAuth } from "../providers/FakeTossUserAuth"; import { FakeTossWebhookProvider } from "../providers/FakeTossWebhookProvider"; import { DateUtil } from "../utils/DateUtil"; -@nest.Controller("v1/virtual-accounts") +@Controller("v1/virtual-accounts") export class FakeTossVirtualAccountsController { - /** - * 가상 계좌로 결제 신청하기. - * - * `virtual_accounts.store` 는 고객이 결제 수단을 가상 계좌로 선택하는 경우에 호출되는 - * API 함수이다. 물론 고객이 이처럼 가상 계좌를 선택한 경우, 고객이 지정된 계좌에 돈을 - * 입금하기 전까지는 결제가 마무리된 것이 아니기에, {@link ITossPayment.status} 값은 - * `WAITING_FOR_DEPOSIT` 이 된다. - * - * 참고로 `virtual_accounts.store` 는 클라이언트 어플리케이션이 토스 페이먼츠가 - * 자체적으로 제공하는 결제 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 - * 호출하는 일은 없을 것이다. 다만, 고객이 가상 계좌로 결제를 진행하는 상황을 - * 시뮬레이션하기 위하여, 테스트 자동화 프로그램 수준에서 사용될 수는 있다. - * - * 그리고 `virtual_accounts.store` 이후에 고객이 지정된 계좌에 금액을 입금하거든, 토스 - * 페이먼츠 서버로부터 웹훅 이벤트가 발생되어 귀하의 백엔드 서버로 전송된다. 만약 연동 - * 대상 토스 페이먼츠 서버가 실제가 아닌 `fake-toss-payments-server` 라면, - * {@link internal.virtual_accounts.deposit} 를 호출하여, 고객이 가상 계좌에 입금하는 - * 상황을 시뮬레이션 할 수 있다. - * - * @param input 가상 결제 신청 정보. - * @returns 가상 계좌 결제 정보 - * - * @security basic - * @author Samchon - */ - @core.TypedRoute.Post() - public store( - @nest.Request() request: fastify.FastifyRequest, - @core.TypedBody() input: ITossVirtualAccountPayment.IStore, - ): ITossVirtualAccountPayment { - FakeTossUserAuth.authorize(request); + /** + * 가상 계좌로 결제 신청하기. + * + * `virtual_accounts.store` 는 고객이 결제 수단을 가상 계좌로 선택하는 경우에 호출되는 + * API 함수이다. 물론 고객이 이처럼 가상 계좌를 선택한 경우, 고객이 지정된 계좌에 돈을 + * 입금하기 전까지는 결제가 마무리된 것이 아니기에, {@link ITossPayment.status} 값은 + * `WAITING_FOR_DEPOSIT` 이 된다. + * + * 참고로 `virtual_accounts.store` 는 클라이언트 어플리케이션이 토스 페이먼츠가 + * 자체적으로 제공하는 결제 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 + * 호출하는 일은 없을 것이다. 다만, 고객이 가상 계좌로 결제를 진행하는 상황을 + * 시뮬레이션하기 위하여, 테스트 자동화 프로그램 수준에서 사용될 수는 있다. + * + * 그리고 `virtual_accounts.store` 이후에 고객이 지정된 계좌에 금액을 입금하거든, 토스 + * 페이먼츠 서버로부터 웹훅 이벤트가 발생되어 귀하의 백엔드 서버로 전송된다. 만약 연동 + * 대상 토스 페이먼츠 서버가 실제가 아닌 `fake-toss-payments-server` 라면, + * {@link internal.virtual_accounts.deposit} 를 호출하여, 고객이 가상 계좌에 입금하는 + * 상황을 시뮬레이션 할 수 있다. + * + * @param input 가상 결제 신청 정보. + * @returns 가상 계좌 결제 정보 + * + * @security basic + * @author Samchon + */ + @core.TypedRoute.Post() + public store( + @FakeTossUserAuth() _0: void, + @core.TypedBody() input: ITossVirtualAccountPayment.IStore, + ): ITossVirtualAccountPayment { + const payment: ITossVirtualAccountPayment = { + ...FakeTossPaymentProvider.get_common_props(input), + method: "가상계좌", + type: "NORMAL", + status: "WAITING_FOR_DEPOSIT", + approvedAt: null, + secret: v4(), + virtualAccount: { + accountNumber: "110417532896", + accountType: "일반", + bank: input.bank, + customerName: input.customerName, + dueDate: DateUtil.to_string(DateUtil.add_days(new Date(), 3), false), + expired: false, + settlementStatus: "INCOMPLETED", + refundStatus: "NONE", + }, + }; + FakeTossStorage.payments.set(payment.paymentKey, payment); - const payment: ITossVirtualAccountPayment = { - ...FakeTossPaymentProvider.get_common_props(input), - method: "가상계좌", - type: "NORMAL", - status: "WAITING_FOR_DEPOSIT", - approvedAt: null, - secret: v4(), - virtualAccount: { - accountNumber: "110417532896", - accountType: "일반", - bank: input.bank, - customerName: input.customerName, - dueDate: DateUtil.to_string( - DateUtil.add_days(new Date(), 3), - false, - ), - expired: false, - settlementStatus: "INCOMPLETED", - refundStatus: "NONE", - }, - }; - FakeTossStorage.payments.set(payment.paymentKey, payment); + FakeTossWebhookProvider.webhook({ + eventType: "PAYMENT_STATUS_CHANGED", + data: { + paymentKey: payment.paymentKey, + orderId: payment.orderId, + status: "WAITING_FOR_DEPOSIT", + }, + }).catch(() => {}); - FakeTossWebhookProvider.webhook({ - eventType: "PAYMENT_STATUS_CHANGED", - data: { - paymentKey: payment.paymentKey, - orderId: payment.orderId, - status: "WAITING_FOR_DEPOSIT", - }, - }).catch(() => {}); - - return payment; - } + return payment; + } } diff --git a/packages/fake-toss-payments-server/src/decorators/FakeTossUserAuth.ts b/packages/fake-toss-payments-server/src/decorators/FakeTossUserAuth.ts new file mode 100644 index 0000000..cef938f --- /dev/null +++ b/packages/fake-toss-payments-server/src/decorators/FakeTossUserAuth.ts @@ -0,0 +1,38 @@ +import { + ExecutionContext, + ForbiddenException, + createParamDecorator, +} from "@nestjs/common"; +import atob from "atob"; +import { FastifyRequest } from "fastify"; +import { Singleton } from "tstl"; + +import { FakeTossConfiguration } from "../FakeTossConfiguration"; + +export function FakeTossUserAuth() { + return singleton.get()(); +} +export namespace FakeTossUserAuth { + export function authorize(request: FastifyRequest): void { + let token: string | undefined = request.headers.authorization; + if (token === undefined) + throw new ForbiddenException("No authorization token exists."); + else if (token.indexOf("Basic ") !== 0) + throw new ForbiddenException("Invalid authorization token."); + + token = token.substring("Basic ".length); + token = atob(token); + if ( + FakeTossConfiguration.authorize(token.substring(0, token.length - 1)) === + false + ) + throw new ForbiddenException("Wrong authorization token."); + } +} + +const singleton = new Singleton(() => + createParamDecorator(async (_0: any, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + return FakeTossUserAuth.authorize(request); + }), +); diff --git a/packages/fake-toss-payments-server/src/executable/server.ts b/packages/fake-toss-payments-server/src/executable/server.ts index c44062c..65022f7 100644 --- a/packages/fake-toss-payments-server/src/executable/server.ts +++ b/packages/fake-toss-payments-server/src/executable/server.ts @@ -9,51 +9,51 @@ const EXTENSION = __filename.substr(-2); if (EXTENSION === "js") require("source-map-support/register"); const directory = new Singleton(async () => { - await mkdir(`${__dirname}/../../assets`); - await mkdir(`${__dirname}/../../assets/logs`); - await mkdir(`${__dirname}/../../assets/logs/errors`); + await mkdir(`${__dirname}/../../assets`); + await mkdir(`${__dirname}/../../assets/logs`); + await mkdir(`${__dirname}/../../assets/logs/errors`); }); function cipher(val: number): string { - if (val < 10) return "0" + val; - else return String(val); + if (val < 10) return "0" + val; + else return String(val); } async function mkdir(path: string): Promise { - try { - await fs.promises.mkdir(path); - } catch {} + try { + await fs.promises.mkdir(path); + } catch {} } async function handle_error(exp: any): Promise { - try { - const date: Date = new Date(); - const fileName: string = `${date.getFullYear()}${cipher( - date.getMonth() + 1, - )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( - date.getMinutes(), - )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; - const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); - - await directory.get(); - await fs.promises.writeFile( - `${__dirname}/../../assets/logs/errors/${fileName}.log`, - content, - "utf8", - ); - } catch {} + try { + const date: Date = new Date(); + const fileName: string = `${date.getFullYear()}${cipher( + date.getMonth() + 1, + )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( + date.getMinutes(), + )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; + const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); + + await directory.get(); + await fs.promises.writeFile( + `${__dirname}/../../assets/logs/errors/${fileName}.log`, + content, + "utf8", + ); + } catch {} } async function main(): Promise { - // BACKEND SEVER LATER - const backend: FakeTossBackend = new FakeTossBackend(); - await backend.open(); + // BACKEND SEVER LATER + const backend: FakeTossBackend = new FakeTossBackend(); + await backend.open(); - // UNEXPECTED ERRORS - global.process.on("uncaughtException", handle_error); - global.process.on("unhandledRejection", handle_error); + // UNEXPECTED ERRORS + global.process.on("uncaughtException", handle_error); + global.process.on("unhandledRejection", handle_error); } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts b/packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts index 83b43f2..2cb7a58 100644 --- a/packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts +++ b/packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts @@ -2,25 +2,25 @@ import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayme import { v4 } from "uuid"; export namespace FakeTossPaymentProvider { - export function get_common_props(input: ITossPayment.IStore) { - return { - mId: "tosspayments", - version: "1.3", - paymentKey: v4(), - transactionKey: v4(), - orderId: input.orderId, - orderName: "test", - currency: "KRW" as const, - totalAmount: input.amount, - balanceAmount: input.amount, - suppliedAmount: input.amount, - taxFreeAmount: 0, - vat: 0, - useEscrow: false, - cultureExpense: false, - requestedAt: new Date().toISOString(), - cancels: null, - cashReceipt: null, - }; - } + export function get_common_props(input: ITossPayment.IStore) { + return { + mId: "tosspayments", + version: "1.3", + paymentKey: v4(), + transactionKey: v4(), + orderId: input.orderId, + orderName: "test", + currency: "KRW" as const, + totalAmount: input.amount, + balanceAmount: input.amount, + suppliedAmount: input.amount, + taxFreeAmount: 0, + vat: 0, + useEscrow: false, + cultureExpense: false, + requestedAt: new Date().toISOString(), + cancels: null, + cashReceipt: null, + }; + } } diff --git a/packages/fake-toss-payments-server/src/providers/FakeTossStorage.ts b/packages/fake-toss-payments-server/src/providers/FakeTossStorage.ts index 884cf97..5c472fe 100644 --- a/packages/fake-toss-payments-server/src/providers/FakeTossStorage.ts +++ b/packages/fake-toss-payments-server/src/providers/FakeTossStorage.ts @@ -7,15 +7,15 @@ import { FakeTossConfiguration } from "../FakeTossConfiguration"; import { VolatileMap } from "../utils/VolatileMap"; export namespace FakeTossStorage { - export const payments: VolatileMap = new VolatileMap( - FakeTossConfiguration.EXPIRATION, - ); - export const billings: VolatileMap< - string, - [ITossBilling, ITossBilling.IStore] - > = new VolatileMap(FakeTossConfiguration.EXPIRATION); - export const cash_receipts: VolatileMap = - new VolatileMap(FakeTossConfiguration.EXPIRATION); - export const webhooks: VolatileMap = - new VolatileMap(FakeTossConfiguration.EXPIRATION); + export const payments: VolatileMap = new VolatileMap( + FakeTossConfiguration.EXPIRATION, + ); + export const billings: VolatileMap< + string, + [ITossBilling, ITossBilling.IStore] + > = new VolatileMap(FakeTossConfiguration.EXPIRATION); + export const cash_receipts: VolatileMap = + new VolatileMap(FakeTossConfiguration.EXPIRATION); + export const webhooks: VolatileMap = + new VolatileMap(FakeTossConfiguration.EXPIRATION); } diff --git a/packages/fake-toss-payments-server/src/providers/FakeTossUserAuth.ts b/packages/fake-toss-payments-server/src/providers/FakeTossUserAuth.ts deleted file mode 100644 index c781896..0000000 --- a/packages/fake-toss-payments-server/src/providers/FakeTossUserAuth.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as nest from "@nestjs/common"; -import atob from "atob"; -import * as fastify from "fastify"; - -import { FakeTossConfiguration } from "../FakeTossConfiguration"; - -export namespace FakeTossUserAuth { - export function authorize(request: fastify.FastifyRequest): void { - let token: string | undefined = request.headers.authorization; - if (token === undefined) - throw new nest.ForbiddenException("No authorization token exists."); - else if (token.indexOf("Basic ") !== 0) - throw new nest.ForbiddenException("Invalid authorization token."); - - token = token.substr("Basic ".length); - token = atob(token); - - if ( - FakeTossConfiguration.authorize( - token.substr(0, token.length - 1), - ) === false - ) - throw new nest.ForbiddenException("Wrong authorization token."); - } -} diff --git a/packages/fake-toss-payments-server/src/providers/FakeTossWebhookProvider.ts b/packages/fake-toss-payments-server/src/providers/FakeTossWebhookProvider.ts index 11cde2b..7a81426 100644 --- a/packages/fake-toss-payments-server/src/providers/FakeTossWebhookProvider.ts +++ b/packages/fake-toss-payments-server/src/providers/FakeTossWebhookProvider.ts @@ -4,21 +4,21 @@ import { FakeTossConfiguration } from "../FakeTossConfiguration"; // POLYFILL FOR NODE if ( - typeof global === "object" && - typeof global.process === "object" && - typeof global.process.versions === "object" && - typeof global.process.versions.node !== undefined + typeof global === "object" && + typeof global.process === "object" && + typeof global.process.versions === "object" && + typeof global.process.versions.node !== undefined ) - (global as any).fetch = require("node-fetch"); + (global as any).fetch = require("node-fetch"); export namespace FakeTossWebhookProvider { - export async function webhook(input: ITossPaymentWebhook): Promise { - await fetch(FakeTossConfiguration.WEBHOOK_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(input), - }); - } + export async function webhook(input: ITossPaymentWebhook): Promise { + await fetch(FakeTossConfiguration.WEBHOOK_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + } } diff --git a/packages/fake-toss-payments-server/src/utils/DateUtil.ts b/packages/fake-toss-payments-server/src/utils/DateUtil.ts index e9577e4..477344f 100644 --- a/packages/fake-toss-payments-server/src/utils/DateUtil.ts +++ b/packages/fake-toss-payments-server/src/utils/DateUtil.ts @@ -1,129 +1,123 @@ export namespace DateUtil { - export const SECOND = 1_000; - export const MINUTE = 60 * SECOND; - export const HOUR = 60 * MINUTE; - export const DAY = 24 * HOUR; - export const WEEK = 7 * DAY; - export const MONTH = 30 * DAY; - - export function to_string(date: Date, hms: boolean = false): string { - const ymd: string = [ - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - ] - .map((value) => _To_cipher_string(value)) - .join("-"); - if (hms === false) return ymd; - - return ( - `${ymd} ` + - [date.getHours(), date.getMinutes(), date.getSeconds()] - .map((value) => _To_cipher_string(value)) - .join(":") - ); - } - - export function to_uuid(date: Date = new Date()): string { - const elements: number[] = [ - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - date.getHours(), - date.getMinutes(), - date.getSeconds(), - ]; - return ( - elements.map((value) => _To_cipher_string(value)).join("") + - "-" + - Math.random().toString().substring(4) - ); - } - - export interface IDifference { - year: number; - month: number; - date: number; - } - - export function diff(x: Date | string, y: Date | string): IDifference { - x = _To_date(x); - y = _To_date(y); - - // FIRST DIFFERENCES - const ret: IDifference = { - year: x.getFullYear() - y.getFullYear(), - month: x.getMonth() - y.getMonth(), - date: x.getDate() - y.getDate(), - }; - - //---- - // HANDLE NEGATIVE ELEMENTS - //---- - // DATE - if (ret.date < 0) { - const last: number = last_date(y.getFullYear(), y.getMonth()); - - --ret.month; - ret.date = x.getDate() + (last - y.getDate()); - } - - // MONTH - if (ret.month < 0) { - --ret.year; - ret.month = 12 + ret.month; - } - return ret; - } - - export function last_date(year: number, month: number): number { - // LEAP MONTH - if ( - month == 1 && - year % 4 == 0 && - !(year % 100 == 0 && year % 400 != 0) - ) - return 29; - else return LAST_DATES[month]; - } - - export function add_years(date: Date, value: number): Date { - date = new Date(date); - date.setFullYear(date.getFullYear() + value); - - return date; - } - - export function add_months(date: Date, value: number): Date { - date = new Date(date); - - const newYear: number = - date.getFullYear() + Math.floor((date.getMonth() + value) / 12); - const newMonth: number = (date.getMonth() + value) % 12; - const lastDate: number = last_date(newYear, newMonth - 1); - - if (lastDate < date.getDate()) date.setDate(lastDate); - - date.setMonth(value - 1); - return date; - } - - export function add_days(date: Date, value: number): Date { - date = new Date(); - date.setDate(date.getDate() + value); - - return date; + export const SECOND = 1_000; + export const MINUTE = 60 * SECOND; + export const HOUR = 60 * MINUTE; + export const DAY = 24 * HOUR; + export const WEEK = 7 * DAY; + export const MONTH = 30 * DAY; + + export function to_string(date: Date, hms: boolean = false): string { + const ymd: string = [ + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + ] + .map((value) => _To_cipher_string(value)) + .join("-"); + if (hms === false) return ymd; + + return ( + `${ymd} ` + + [date.getHours(), date.getMinutes(), date.getSeconds()] + .map((value) => _To_cipher_string(value)) + .join(":") + ); + } + + export function to_uuid(date: Date = new Date()): string { + const elements: number[] = [ + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes(), + date.getSeconds(), + ]; + return ( + elements.map((value) => _To_cipher_string(value)).join("") + + "-" + + Math.random().toString().substring(4) + ); + } + + export interface IDifference { + year: number; + month: number; + date: number; + } + + export function diff(x: Date | string, y: Date | string): IDifference { + x = _To_date(x); + y = _To_date(y); + + // FIRST DIFFERENCES + const ret: IDifference = { + year: x.getFullYear() - y.getFullYear(), + month: x.getMonth() - y.getMonth(), + date: x.getDate() - y.getDate(), + }; + + //---- + // HANDLE NEGATIVE ELEMENTS + //---- + // DATE + if (ret.date < 0) { + const last: number = last_date(y.getFullYear(), y.getMonth()); + + --ret.month; + ret.date = x.getDate() + (last - y.getDate()); } - function _To_date(date: string | Date): Date { - if (date instanceof Date) return date; - else return new Date(date); + // MONTH + if (ret.month < 0) { + --ret.year; + ret.month = 12 + ret.month; } - function _To_cipher_string(val: number): string { - if (val < 10) return "0" + val; - else return String(val); - } - const LAST_DATES: number[] = [ - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - ]; + return ret; + } + + export function last_date(year: number, month: number): number { + // LEAP MONTH + if (month == 1 && year % 4 == 0 && !(year % 100 == 0 && year % 400 != 0)) + return 29; + else return LAST_DATES[month]; + } + + export function add_years(date: Date, value: number): Date { + date = new Date(date); + date.setFullYear(date.getFullYear() + value); + + return date; + } + + export function add_months(date: Date, value: number): Date { + date = new Date(date); + + const newYear: number = + date.getFullYear() + Math.floor((date.getMonth() + value) / 12); + const newMonth: number = (date.getMonth() + value) % 12; + const lastDate: number = last_date(newYear, newMonth - 1); + + if (lastDate < date.getDate()) date.setDate(lastDate); + + date.setMonth(value - 1); + return date; + } + + export function add_days(date: Date, value: number): Date { + date = new Date(); + date.setDate(date.getDate() + value); + + return date; + } + + function _To_date(date: string | Date): Date { + if (date instanceof Date) return date; + else return new Date(date); + } + function _To_cipher_string(val: number): string { + if (val < 10) return "0" + val; + else return String(val); + } + const LAST_DATES: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; } diff --git a/packages/fake-toss-payments-server/src/utils/ErrorUtil.ts b/packages/fake-toss-payments-server/src/utils/ErrorUtil.ts index 9501fd0..12bd0df 100644 --- a/packages/fake-toss-payments-server/src/utils/ErrorUtil.ts +++ b/packages/fake-toss-payments-server/src/utils/ErrorUtil.ts @@ -6,51 +6,48 @@ import { Singleton } from "tstl/thread/Singleton"; import { FakeTossConfiguration } from "../FakeTossConfiguration"; export namespace ErrorUtil { - export function toJSON(err: any): object { - return err instanceof Object && err.toJSON instanceof Function - ? err.toJSON() - : serializeError(err); - } - - export async function log( - prefix: string, - data: string | object | Error, - ): Promise { - try { - if (data instanceof Error) data = toJSON(data); - - const date: Date = new Date(); - const fileName: string = `${date.getFullYear()}${cipher( - date.getMonth() + 1, - )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( - date.getMinutes(), - )}${cipher(date.getSeconds())}.${randint( - 0, - Number.MAX_SAFE_INTEGER, - )}`; - const content: string = JSON.stringify(data, null, 4); - - await directory.get(); - await fs.promises.writeFile( - `${FakeTossConfiguration.ASSETS}/logs/errors/${prefix}_${fileName}.log`, - content, - "utf8", - ); - } catch {} - } + export function toJSON(err: any): object { + return err instanceof Object && err.toJSON instanceof Function + ? err.toJSON() + : serializeError(err); + } + + export async function log( + prefix: string, + data: string | object | Error, + ): Promise { + try { + if (data instanceof Error) data = toJSON(data); + + const date: Date = new Date(); + const fileName: string = `${date.getFullYear()}${cipher( + date.getMonth() + 1, + )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( + date.getMinutes(), + )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; + const content: string = JSON.stringify(data, null, 4); + + await directory.get(); + await fs.promises.writeFile( + `${FakeTossConfiguration.ASSETS}/logs/errors/${prefix}_${fileName}.log`, + content, + "utf8", + ); + } catch {} + } } function cipher(val: number): string { - if (val < 10) return "0" + val; - else return String(val); + if (val < 10) return "0" + val; + else return String(val); } const directory = new Singleton(async () => { - try { - await fs.promises.mkdir(`${FakeTossConfiguration.ASSETS}/logs`); - } catch {} + try { + await fs.promises.mkdir(`${FakeTossConfiguration.ASSETS}/logs`); + } catch {} - try { - await fs.promises.mkdir(`${FakeTossConfiguration.ASSETS}/logs/errors`); - } catch {} + try { + await fs.promises.mkdir(`${FakeTossConfiguration.ASSETS}/logs/errors`); + } catch {} }); diff --git a/packages/fake-toss-payments-server/src/utils/Terminal.ts b/packages/fake-toss-payments-server/src/utils/Terminal.ts index c7174aa..c7fe808 100644 --- a/packages/fake-toss-payments-server/src/utils/Terminal.ts +++ b/packages/fake-toss-payments-server/src/utils/Terminal.ts @@ -2,17 +2,17 @@ import cp from "child_process"; import { Pair } from "tstl/utility/Pair"; export namespace Terminal { - export function execute( - ...commands: string[] - ): Promise> { - return new Promise((resolve, reject) => { - cp.exec( - commands.join(" && "), - (error: Error | null, stdout: string, stderr: string) => { - if (error) reject(error); - else resolve(new Pair(stdout, stderr)); - }, - ); - }); - } + export function execute( + ...commands: string[] + ): Promise> { + return new Promise((resolve, reject) => { + cp.exec( + commands.join(" && "), + (error: Error | null, stdout: string, stderr: string) => { + if (error) reject(error); + else resolve(new Pair(stdout, stderr)); + }, + ); + }); + } } diff --git a/packages/fake-toss-payments-server/src/utils/VolatileMap.ts b/packages/fake-toss-payments-server/src/utils/VolatileMap.ts index 97e2d2b..022c979 100644 --- a/packages/fake-toss-payments-server/src/utils/VolatileMap.ts +++ b/packages/fake-toss-payments-server/src/utils/VolatileMap.ts @@ -4,60 +4,60 @@ import { equal_to } from "tstl/functional/comparators"; import { hash } from "tstl/functional/hash"; export class VolatileMap { - private readonly dict_: HashMap; - private readonly timepoints_: TreeMap; - - public constructor( - public readonly expiration: VolatileMap.IExpiration, - hasher: (key: Key) => number = hash, - pred: (x: Key, y: Key) => boolean = equal_to, - ) { - this.dict_ = new HashMap(hasher, pred); - this.timepoints_ = new TreeMap(); + private readonly dict_: HashMap; + private readonly timepoints_: TreeMap; + + public constructor( + public readonly expiration: VolatileMap.IExpiration, + hasher: (key: Key) => number = hash, + pred: (x: Key, y: Key) => boolean = equal_to, + ) { + this.dict_ = new HashMap(hasher, pred); + this.timepoints_ = new TreeMap(); + } + + public size(): number { + return this.dict_.size(); + } + + public get(key: Key): T { + return this.dict_.get(key); + } + + public clear(): void { + this.dict_.clear(); + this.timepoints_.clear(); + } + + public set(key: Key, value: T): void { + this._Clean_up(); + + this.dict_.set(key, value); + this.timepoints_.set(Date.now(), key); + } + + private _Clean_up(): void { + const bound: number = Date.now() - this.expiration.time; + const last: TreeMap.Iterator = + this.timepoints_.upper_bound(bound); + + for (let it = this.timepoints_.begin(); it.equals(last) === false; ) { + this.dict_.erase(it.second); + it = this.timepoints_.erase(it); } + if (this.timepoints_.size() < this.expiration.capacity) return; - public size(): number { - return this.dict_.size(); - } - - public get(key: Key): T { - return this.dict_.get(key); - } - - public clear(): void { - this.dict_.clear(); - this.timepoints_.clear(); - } - - public set(key: Key, value: T): void { - this._Clean_up(); - - this.dict_.set(key, value); - this.timepoints_.set(Date.now(), key); - } - - private _Clean_up(): void { - const bound: number = Date.now() - this.expiration.time; - const last: TreeMap.Iterator = - this.timepoints_.upper_bound(bound); - - for (let it = this.timepoints_.begin(); it.equals(last) === false; ) { - this.dict_.erase(it.second); - it = this.timepoints_.erase(it); - } - if (this.timepoints_.size() < this.expiration.capacity) return; - - let left: number = this.timepoints_.size() - this.expiration.capacity; - while (left-- === 0) { - const it: TreeMap.Iterator = this.timepoints_.begin(); - this.dict_.erase(it.second); - this.timepoints_.erase(it); - } + let left: number = this.timepoints_.size() - this.expiration.capacity; + while (left-- === 0) { + const it: TreeMap.Iterator = this.timepoints_.begin(); + this.dict_.erase(it.second); + this.timepoints_.erase(it); } + } } export namespace VolatileMap { - export interface IExpiration { - time: number; - capacity: number; - } + export interface IExpiration { + time: number; + capacity: number; + } } diff --git a/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts index 1a6dfaf..75ecc24 100644 --- a/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts +++ b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts @@ -9,68 +9,65 @@ import { FakeTossStorage } from "../../../src/providers/FakeTossStorage"; import { TestConnection } from "../../internal/TestConnection"; export const validate_fake_payment_cancel = - (generator: () => Promise, virtual: boolean) => async () => { - // 결제 완료 - const init: ITossPayment = await generator(); + (generator: () => Promise, virtual: boolean) => async () => { + // 결제 완료 + const init: ITossPayment = await generator(); - // 검증 로직 준비 - const validate = (cancelled: boolean) => (p: ITossPayment) => { - TestValidator.equals("balanceAmount")( - cancelled ? 0 : p.totalAmount, - )(p.balanceAmount); - TestValidator.equals("cancelAmount")( - (p.cancels ?? []) - .map((c) => c.cancelAmount) - .reduce((a, b) => a + b, 0), - )(cancelled ? p.totalAmount : 0); - TestValidator.equals("cancels?.length")(cancelled ? 1 : 0)( - p.cancels?.length ?? 0, - ); - TestValidator.predicate("cancel_history")( - cancelled - ? () => - p.cancels?.length === 1 && - p.cancels?.[0].cancelAmount === p.totalAmount - : () => !p.cancels?.length, - ); - }; + // 검증 로직 준비 + const validate = (cancelled: boolean) => (p: ITossPayment) => { + TestValidator.equals("balanceAmount")(cancelled ? 0 : p.totalAmount)( + p.balanceAmount, + ); + TestValidator.equals("cancelAmount")( + (p.cancels ?? []).map((c) => c.cancelAmount).reduce((a, b) => a + b, 0), + )(cancelled ? p.totalAmount : 0); + TestValidator.equals("cancels?.length")(cancelled ? 1 : 0)( + p.cancels?.length ?? 0, + ); + TestValidator.predicate("cancel_history")( + cancelled + ? () => + p.cancels?.length === 1 && + p.cancels?.[0].cancelAmount === p.totalAmount + : () => !p.cancels?.length, + ); + }; - // 결제 취소 전 검증 - validate(false)(init); + // 결제 취소 전 검증 + validate(false)(init); - // 결제 취소하기 (전액) - const cancelled: ITossPayment = - await toss.functional.v1.payments.cancel( - TestConnection.FAKE, - init.paymentKey, - { - paymentKey: init.paymentKey, - cancelReason: "테스트 결제 취소", - cancelAmount: init.totalAmount, - refundReceiveAccount: virtual - ? { - bank: "국민은행", - accountNumber: "12345678901234", - holderName: "홍길동", - } - : undefined, - }, - ); - typia.assert(cancelled); - validate(true)(cancelled); + // 결제 취소하기 (전액) + const cancelled: ITossPayment = await toss.functional.v1.payments.cancel( + TestConnection.FAKE, + init.paymentKey, + { + paymentKey: init.paymentKey, + cancelReason: "테스트 결제 취소", + cancelAmount: init.totalAmount, + refundReceiveAccount: virtual + ? { + bank: "국민은행", + accountNumber: "12345678901234", + holderName: "홍길동", + } + : undefined, + }, + ); + typia.assert(cancelled); + validate(true)(cancelled); - // 웹훅 검증 - await sleep_for(50); - const webhook: ITossPaymentWebhook = FakeTossStorage.webhooks.get( - init.paymentKey, - ); - TestValidator.equals("status")(webhook.data.status)("CANCELED"); + // 웹훅 검증 + await sleep_for(50); + const webhook: ITossPaymentWebhook = FakeTossStorage.webhooks.get( + init.paymentKey, + ); + TestValidator.equals("status")(webhook.data.status)("CANCELED"); - // 결제 내역 재 조회하여 검증 - const reloaded: ITossPayment = await toss.functional.v1.payments.at( - TestConnection.FAKE, - init.paymentKey, - ); - typia.assert(reloaded); - validate(true)(reloaded); - }; + // 결제 내역 재 조회하여 검증 + const reloaded: ITossPayment = await toss.functional.v1.payments.at( + TestConnection.FAKE, + init.paymentKey, + ); + typia.assert(reloaded); + validate(true)(reloaded); + }; diff --git a/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts index 369ad64..7825d5a 100644 --- a/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts +++ b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts @@ -6,38 +6,36 @@ import typia from "typia"; import { TestConnection } from "../../internal/TestConnection"; export const validate_fake_payment_cancel_over = - (generator: () => Promise, virtual: boolean) => async () => { - // 결제 완료 - const payment: ITossPayment = await generator(); + (generator: () => Promise, virtual: boolean) => async () => { + // 결제 완료 + const payment: ITossPayment = await generator(); - // 결제 취소하기 (전액 + 100) - await TestValidator.error("over")(() => - toss.functional.v1.payments.cancel( - TestConnection.FAKE, - payment.paymentKey, - { - paymentKey: payment.paymentKey, - cancelReason: "테스트 결제 취소", - cancelAmount: payment.totalAmount + 100, - refundReceiveAccount: virtual - ? { - bank: "국민은행", - accountNumber: "12345678901234", - holderName: "홍길동", - } - : undefined, - }, - ), - ); + // 결제 취소하기 (전액 + 100) + await TestValidator.error("over")(() => + toss.functional.v1.payments.cancel( + TestConnection.FAKE, + payment.paymentKey, + { + paymentKey: payment.paymentKey, + cancelReason: "테스트 결제 취소", + cancelAmount: payment.totalAmount + 100, + refundReceiveAccount: virtual + ? { + bank: "국민은행", + accountNumber: "12345678901234", + holderName: "홍길동", + } + : undefined, + }, + ), + ); - // 결제 내역 재 조회하여 다시 검증 - const reloaded: ITossPayment = await toss.functional.v1.payments.at( - TestConnection.FAKE, - payment.paymentKey, - ); - typia.assert(reloaded); - TestValidator.equals("balance")(payment.balanceAmount)( - payment.totalAmount, - ); - TestValidator.equals("cancels.length")(payment.cancels?.length ?? 0)(0); - }; + // 결제 내역 재 조회하여 다시 검증 + const reloaded: ITossPayment = await toss.functional.v1.payments.at( + TestConnection.FAKE, + payment.paymentKey, + ); + typia.assert(reloaded); + TestValidator.equals("balance")(payment.balanceAmount)(payment.totalAmount); + TestValidator.equals("cancels.length")(payment.cancels?.length ?? 0)(0); + }; diff --git a/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts index 30712de..8b42c8e 100644 --- a/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts +++ b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts @@ -8,61 +8,56 @@ import { FakeTossStorage } from "../../../src/providers/FakeTossStorage"; import { TestConnection } from "../../internal/TestConnection"; export const validate_fake_payment_cancel_partial = - (generator: () => Promise, virtual: boolean) => async () => { - // 결제 완료 - const init: ITossPayment = await generator(); + (generator: () => Promise, virtual: boolean) => async () => { + // 결제 완료 + const init: ITossPayment = await generator(); - // 검증 로직 준비 - const validate = (count: number) => (p: ITossPayment) => { - const expected: number = (p.totalAmount / 5) * count; + // 검증 로직 준비 + const validate = (count: number) => (p: ITossPayment) => { + const expected: number = (p.totalAmount / 5) * count; - // VALIDATE CANCELLED AMOUNT - TestValidator.equals("balanceAmount")(p.totalAmount - expected)( - p.balanceAmount, - ); - TestValidator.equals("cancelAmount")( - (p.cancels ?? []) - .map((c) => c.cancelAmount) - .reduce((a, b) => a + b, 0), - )(expected); - TestValidator.equals("cancels?.length")(count)( - p.cancels?.length ?? 0, - ); + // VALIDATE CANCELLED AMOUNT + TestValidator.equals("balanceAmount")(p.totalAmount - expected)( + p.balanceAmount, + ); + TestValidator.equals("cancelAmount")( + (p.cancels ?? []).map((c) => c.cancelAmount).reduce((a, b) => a + b, 0), + )(expected); + TestValidator.equals("cancels?.length")(count)(p.cancels?.length ?? 0); - // VALIDATE WEBHOOOK - if (count !== 0) { - const webhook = FakeTossStorage.webhooks.get(p.paymentKey); - TestValidator.equals("status")(webhook.data.status)( - count !== 5 ? "PARTIAL_CANCELED" : "CANCELED", - ); - } - }; + // VALIDATE WEBHOOOK + if (count !== 0) { + const webhook = FakeTossStorage.webhooks.get(p.paymentKey); + TestValidator.equals("status")(webhook.data.status)( + count !== 5 ? "PARTIAL_CANCELED" : "CANCELED", + ); + } + }; - // 결제 취소 전 검증 - await sleep_for(100); - validate(0)(init); + // 결제 취소 전 검증 + await sleep_for(100); + validate(0)(init); - // 5 회에 걸쳐 분할 취소 하기 - await ArrayUtil.asyncRepeat(5)(async (i) => { - const cancelled: ITossPayment = - await toss.functional.v1.payments.cancel( - TestConnection.FAKE, - init.paymentKey, - { - paymentKey: init.paymentKey, - cancelReason: "테스트 결제 취소", - cancelAmount: init.totalAmount / 5, - refundReceiveAccount: virtual - ? { - bank: "국민은행", - accountNumber: "12345678901234", - holderName: "홍길동", - } - : undefined, - }, - ); - typia.assert(cancelled); - await sleep_for(100); - validate(i + 1)(cancelled); - }); - }; + // 5 회에 걸쳐 분할 취소 하기 + await ArrayUtil.asyncRepeat(5)(async (i) => { + const cancelled: ITossPayment = await toss.functional.v1.payments.cancel( + TestConnection.FAKE, + init.paymentKey, + { + paymentKey: init.paymentKey, + cancelReason: "테스트 결제 취소", + cancelAmount: init.totalAmount / 5, + refundReceiveAccount: virtual + ? { + bank: "국민은행", + accountNumber: "12345678901234", + holderName: "홍길동", + } + : undefined, + }, + ); + typia.assert(cancelled); + await sleep_for(100); + validate(i + 1)(cancelled); + }); + }; diff --git a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts index 8b90b60..1464466 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts @@ -9,38 +9,38 @@ import { v4 } from "uuid"; import { TestConnection } from "../internal/TestConnection"; export async function test_fake_billing_payment(): Promise { - // 간편 결제 카드 등록하기 - const billing: ITossBilling = - await toss.functional.v1.billing.authorizations.card.store( - TestConnection.FAKE, - { - customerKey: "some-consumer-key", - cardNumber: "1111222233334444", - cardExpirationYear: "28", - cardExpirationMonth: "03", - cardPassword: "99", - customerBirthday: "880311", - consumerName: "남정호", - }, - ); - typia.assert(billing); - - // 간편 결제 카드로 결제하기 - const payment: ITossPayment = await toss.functional.v1.billing.pay( - TestConnection.FAKE, - billing.billingKey, - { - method: "billing", - billingKey: billing.billingKey, - customerKey: v4(), - orderId: v4(), - amount: 10_000, - }, + // 간편 결제 카드 등록하기 + const billing: ITossBilling = + await toss.functional.v1.billing.authorizations.card.store( + TestConnection.FAKE, + { + customerKey: "some-consumer-key", + cardNumber: "1111222233334444", + cardExpirationYear: "28", + cardExpirationMonth: "03", + cardPassword: "99", + customerBirthday: "880311", + consumerName: "남정호", + }, ); - typia.assert(payment); + typia.assert(billing); + + // 간편 결제 카드로 결제하기 + const payment: ITossPayment = await toss.functional.v1.billing.pay( + TestConnection.FAKE, + billing.billingKey, + { + method: "billing", + billingKey: billing.billingKey, + customerKey: v4(), + orderId: v4(), + amount: 10_000, + }, + ); + typia.assert(payment); - // 간편 결제 카드로 결제시, 별도 승인 처리가 필요 없음 - TestValidator.equals("approvedAt")(!!payment.approvedAt)(true); - TestValidator.equals("status")(payment.status)("DONE"); - return payment; + // 간편 결제 카드로 결제시, 별도 승인 처리가 필요 없음 + TestValidator.equals("approvedAt")(!!payment.approvedAt)(true); + TestValidator.equals("status")(payment.status)("DONE"); + return payment; } diff --git a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel.ts index 01e2aa3..34198ae 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel.ts @@ -2,6 +2,6 @@ import { validate_fake_payment_cancel } from "./internal/validate_fake_payment_c import { test_fake_billing_payment } from "./test_fake_billing_payment"; export const test_fake_billing_payment_cancel = validate_fake_payment_cancel( - () => test_fake_billing_payment(), - false, + () => test_fake_billing_payment(), + false, ); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_over.ts index efb6e9c..190bfae 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_over.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_over.ts @@ -2,4 +2,4 @@ import { validate_fake_payment_cancel_over } from "./internal/validate_fake_paym import { test_fake_billing_payment } from "./test_fake_billing_payment"; export const test_fake_billing_payment_cancel_over = - validate_fake_payment_cancel_over(() => test_fake_billing_payment(), false); + validate_fake_payment_cancel_over(() => test_fake_billing_payment(), false); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_partial.ts index 2741308..641b18b 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_partial.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_partial.ts @@ -2,7 +2,7 @@ import { validate_fake_payment_cancel_partial } from "./internal/validate_fake_p import { test_fake_billing_payment } from "./test_fake_billing_payment"; export const test_fake_billing_payment_cancel_partial = - validate_fake_payment_cancel_partial( - () => test_fake_billing_payment(), - false, - ); + validate_fake_payment_cancel_partial( + () => test_fake_billing_payment(), + false, + ); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts index a9e7e00..4721841 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts @@ -8,79 +8,79 @@ import { v4 } from "uuid"; import { TestConnection } from "../internal/TestConnection"; export async function test_fake_card_payment(): Promise { - //---- - // 결제하기 - //---- - // 결제 요청 레코드 발행하기 - // - // 백엔드 서버에서 토스 페이먼츠 서버의 API 를 직접 호출하여, 즉시 승인되는 카드 결제를 - // 진행하는 게 아닌, 프론트 앱이 토스 페이먼츠가 제공해주는 결제 창을 이용한다는 가정. - // - // 토스 페이먼츠는 이처럼 프론트 앱에서 백엔드 서버를 거치지 않고, 토스 페이먼츠 고유의 - // 결제 창을 이용하여 직접 결제를 진행하는 경우, 백엔드 서버에서 이를 별도 승인 - // 처리해주기 전까지 정식 결제로 인정치 아니함. - // - // 때문에 {@link ITossCardPayment.IStore.__approved} 값을 `false` 로 하여, - // 백엔드에서 해당 결제 요청 건에 대하여 별도의 승인 처리가 필요한 상황을 고의로 만듦. - const payment: ITossCardPayment = await toss.functional.v1.payments.key_in( - TestConnection.FAKE, - { - // 카드 정보 - method: "card", - cardNumber: "1111222233334444", - cardExpirationYear: "24", - cardExpirationMonth: "03", + //---- + // 결제하기 + //---- + // 결제 요청 레코드 발행하기 + // + // 백엔드 서버에서 토스 페이먼츠 서버의 API 를 직접 호출하여, 즉시 승인되는 카드 결제를 + // 진행하는 게 아닌, 프론트 앱이 토스 페이먼츠가 제공해주는 결제 창을 이용한다는 가정. + // + // 토스 페이먼츠는 이처럼 프론트 앱에서 백엔드 서버를 거치지 않고, 토스 페이먼츠 고유의 + // 결제 창을 이용하여 직접 결제를 진행하는 경우, 백엔드 서버에서 이를 별도 승인 + // 처리해주기 전까지 정식 결제로 인정치 아니함. + // + // 때문에 {@link ITossCardPayment.IStore.__approved} 값을 `false` 로 하여, + // 백엔드에서 해당 결제 요청 건에 대하여 별도의 승인 처리가 필요한 상황을 고의로 만듦. + const payment: ITossCardPayment = await toss.functional.v1.payments.key_in( + TestConnection.FAKE, + { + // 카드 정보 + method: "card", + cardNumber: "1111222233334444", + cardExpirationYear: "24", + cardExpirationMonth: "03", - // 주문 정보 - orderId: v4(), - amount: 30_000, + // 주문 정보 + orderId: v4(), + amount: 30_000, - // FAKE PROPERTY - __approved: false, - }, - ); - typia.assert(payment); + // FAKE PROPERTY + __approved: false, + }, + ); + typia.assert(payment); - // 잘못된 `orderId` 로 승인 처리시, 불발됨 - await TestValidator.error( - "VirtualTossPaymentsController.approve() with wrong orderId", - )(() => - toss.functional.v1.payments.approve( - TestConnection.FAKE, - payment.paymentKey, - { - orderId: "wrong-order-id", - amount: payment.totalAmount, - }, - ), - ); + // 잘못된 `orderId` 로 승인 처리시, 불발됨 + await TestValidator.error( + "VirtualTossPaymentsController.approve() with wrong orderId", + )(() => + toss.functional.v1.payments.approve( + TestConnection.FAKE, + payment.paymentKey, + { + orderId: "wrong-order-id", + amount: payment.totalAmount, + }, + ), + ); - // 잘못된 결제 금액으로 승인 처리시, 마찬가지로 불발됨 - await TestValidator.error( - "VirtualTossPaymentsController.approve() with wrong amount", - )(() => - toss.functional.v1.payments.approve( - TestConnection.FAKE, - payment.paymentKey, - { - orderId: payment.orderId, - amount: payment.totalAmount - 100, - }, - ), - ); + // 잘못된 결제 금액으로 승인 처리시, 마찬가지로 불발됨 + await TestValidator.error( + "VirtualTossPaymentsController.approve() with wrong amount", + )(() => + toss.functional.v1.payments.approve( + TestConnection.FAKE, + payment.paymentKey, + { + orderId: payment.orderId, + amount: payment.totalAmount - 100, + }, + ), + ); - // 정확한 `orderId` 와 주문 금액을 입력해야 비로소 승인 처리된다. - const approved: ITossPayment = await toss.functional.v1.payments.approve( - TestConnection.FAKE, - payment.paymentKey, - { - orderId: payment.orderId, - amount: payment.totalAmount, - }, - ); - const card: ITossCardPayment = typia.assert(approved); - TestValidator.equals("approvedAt")(!!card.approvedAt)(true); - TestValidator.equals("status")(card.status)("DONE"); + // 정확한 `orderId` 와 주문 금액을 입력해야 비로소 승인 처리된다. + const approved: ITossPayment = await toss.functional.v1.payments.approve( + TestConnection.FAKE, + payment.paymentKey, + { + orderId: payment.orderId, + amount: payment.totalAmount, + }, + ); + const card: ITossCardPayment = typia.assert(approved); + TestValidator.equals("approvedAt")(!!card.approvedAt)(true); + TestValidator.equals("status")(card.status)("DONE"); - return card; + return card; } diff --git a/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel.ts index 784d5d7..b4ea200 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel.ts @@ -2,6 +2,6 @@ import { validate_fake_payment_cancel } from "./internal/validate_fake_payment_c import { test_fake_card_payment } from "./test_fake_card_payment"; export const test_fake_card_payment_cancel = validate_fake_payment_cancel( - () => test_fake_card_payment(), - false, + () => test_fake_card_payment(), + false, ); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_over.ts index f8427aa..abc19fc 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_over.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_over.ts @@ -2,4 +2,4 @@ import { validate_fake_payment_cancel_over } from "./internal/validate_fake_paym import { test_fake_card_payment } from "./test_fake_card_payment"; export const test_fake_card_payment_cancel_over = - validate_fake_payment_cancel_over(() => test_fake_card_payment(), false); + validate_fake_payment_cancel_over(() => test_fake_card_payment(), false); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_partial.ts index 4e4a7de..cdcc788 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_partial.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_partial.ts @@ -2,4 +2,4 @@ import { validate_fake_payment_cancel_partial } from "./internal/validate_fake_p import { test_fake_card_payment } from "./test_fake_card_payment"; export const test_fake_card_payment_cancel_partial = - validate_fake_payment_cancel_partial(() => test_fake_card_payment(), false); + validate_fake_payment_cancel_partial(() => test_fake_card_payment(), false); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts b/packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts index cd8511a..10e6e2d 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts @@ -9,25 +9,25 @@ import { TestConnection } from "../internal/TestConnection"; import { test_fake_virtual_account_payment } from "./test_fake_virtual_account_payment"; export async function test_fake_cash_receipt(): Promise { - const payment: ITossVirtualAccountPayment = - await test_fake_virtual_account_payment(); - const receipt: ITossCashReceipt = - await toss.functional.v1.cash_receipts.store(TestConnection.FAKE, { - type: "소득공제", - paymentKey: payment.paymentKey, - orderId: payment.orderId, - orderName: payment.orderName, - registrationNumber: "8803111******", - amount: payment.totalAmount, - }); - typia.assert(receipt); + const payment: ITossVirtualAccountPayment = + await test_fake_virtual_account_payment(); + const receipt: ITossCashReceipt = + await toss.functional.v1.cash_receipts.store(TestConnection.FAKE, { + type: "소득공제", + paymentKey: payment.paymentKey, + orderId: payment.orderId, + orderName: payment.orderName, + registrationNumber: "8803111******", + amount: payment.totalAmount, + }); + typia.assert(receipt); - const reloaded: ITossPayment = await toss.functional.v1.payments.at( - TestConnection.FAKE, - payment.paymentKey, - ); - typia.assert(reloaded); - TestValidator.equals("receipt")(reloaded.cashReceipt?.receiptUrl)( - receipt.receiptUrl, - ); + const reloaded: ITossPayment = await toss.functional.v1.payments.at( + TestConnection.FAKE, + payment.paymentKey, + ); + typia.assert(reloaded); + TestValidator.equals("receipt")(reloaded.cashReceipt?.receiptUrl)( + receipt.receiptUrl, + ); } diff --git a/packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts b/packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts index 284a0cb..8d87468 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts @@ -13,75 +13,70 @@ import { AdvancedRandomGenerator } from "../internal/AdvancedRandomGenerator"; import { TestConnection } from "../internal/TestConnection"; export async function test_fake_storage_capacity(): Promise { - const capacity: number = FakeTossConfiguration.EXPIRATION.capacity; + const capacity: number = FakeTossConfiguration.EXPIRATION.capacity; - FakeTossStorage.payments.clear(); - FakeTossStorage.billings.clear(); - FakeTossConfiguration.EXPIRATION.capacity = 1; + FakeTossStorage.payments.clear(); + FakeTossStorage.billings.clear(); + FakeTossConfiguration.EXPIRATION.capacity = 1; - const previous: IPointer = { value: null }; - for (let i: number = 0; i < 10; ++i) { - // GENERATE RANDOM BILLING - const customerKey: string = v4(); - const billing: ITossBilling = - await toss.functional.v1.billing.authorizations.card.store( - TestConnection.FAKE, - { - customerKey, - customerBirthday: "880311", - cardNumber: AdvancedRandomGenerator.cardNumber(), - cardExpirationYear: randint(22, 28).toString(), - cardExpirationMonth: randint(1, 12) - .toString() - .padStart(2, "0"), - cardPassword: randint(0, 9999).toString().padStart(4, "0"), - }, - ); - assert(billing); + const previous: IPointer = { value: null }; + for (let i: number = 0; i < 10; ++i) { + // GENERATE RANDOM BILLING + const customerKey: string = v4(); + const billing: ITossBilling = + await toss.functional.v1.billing.authorizations.card.store( + TestConnection.FAKE, + { + customerKey, + customerBirthday: "880311", + cardNumber: AdvancedRandomGenerator.cardNumber(), + cardExpirationYear: randint(22, 28).toString(), + cardExpirationMonth: randint(1, 12).toString().padStart(2, "0"), + cardPassword: randint(0, 9999).toString().padStart(4, "0"), + }, + ); + assert(billing); - // GENERATE RANDOM PAYMENT BY THE BILLING - const orderId: string = v4(); - const amount: number = 100; + // GENERATE RANDOM PAYMENT BY THE BILLING + const orderId: string = v4(); + const amount: number = 100; - const payment: ITossPayment = await toss.functional.v1.billing.pay( - TestConnection.FAKE, - billing.billingKey, - { - method: "billing", - customerKey, - billingKey: billing.billingKey, - orderId, - amount, - }, - ); - assert(payment); + const payment: ITossPayment = await toss.functional.v1.billing.pay( + TestConnection.FAKE, + billing.billingKey, + { + method: "billing", + customerKey, + billingKey: billing.billingKey, + orderId, + amount, + }, + ); + assert(payment); - // APPROVE THE PAYMENT - await toss.functional.v1.payments.approve( - TestConnection.FAKE, - payment.paymentKey, - { - orderId, - amount, - }, - ); + // APPROVE THE PAYMENT + await toss.functional.v1.payments.approve( + TestConnection.FAKE, + payment.paymentKey, + { + orderId, + amount, + }, + ); - // TEST THE EXPIRATION - if (previous.value !== null) - await TestValidator.error( - "VirtualTossStorage.payments.get() for expired record", - )(() => - toss.functional.v1.payments.at( - TestConnection.FAKE, - previous.value!, - ), - ); - await toss.functional.v1.payments.at( - TestConnection.FAKE, - payment.paymentKey, - ); - previous.value = payment.paymentKey; - } + // TEST THE EXPIRATION + if (previous.value !== null) + await TestValidator.error( + "VirtualTossStorage.payments.get() for expired record", + )(() => + toss.functional.v1.payments.at(TestConnection.FAKE, previous.value!), + ); + await toss.functional.v1.payments.at( + TestConnection.FAKE, + payment.paymentKey, + ); + previous.value = payment.paymentKey; + } - FakeTossConfiguration.EXPIRATION.capacity = capacity; + FakeTossConfiguration.EXPIRATION.capacity = capacity; } diff --git a/packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts b/packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts index 12d4e99..addb746 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts @@ -14,42 +14,39 @@ import { AdvancedRandomGenerator } from "../internal/AdvancedRandomGenerator"; import { TestConnection } from "../internal/TestConnection"; export async function test_fake_storage_expiration_time(): Promise { - const time: number = FakeTossConfiguration.EXPIRATION.time; - FakeTossStorage.payments.clear(); - FakeTossConfiguration.EXPIRATION.time = 1; + const time: number = FakeTossConfiguration.EXPIRATION.time; + FakeTossStorage.payments.clear(); + FakeTossConfiguration.EXPIRATION.time = 1; - const previous: IPointer = { value: null }; - await ArrayUtil.asyncRepeat(10)(async () => { - const payment: ITossPayment = await toss.functional.v1.payments.key_in( - TestConnection.FAKE, - { - method: "card", + const previous: IPointer = { value: null }; + await ArrayUtil.asyncRepeat(10)(async () => { + const payment: ITossPayment = await toss.functional.v1.payments.key_in( + TestConnection.FAKE, + { + method: "card", - cardNumber: AdvancedRandomGenerator.cardNumber(), - cardExpirationYear: randint(22, 28).toString(), - cardExpirationMonth: randint(1, 12).toString().padStart(2, "0"), + cardNumber: AdvancedRandomGenerator.cardNumber(), + cardExpirationYear: randint(22, 28).toString(), + cardExpirationMonth: randint(1, 12).toString().padStart(2, "0"), - orderId: v4(), - amount: 1000, - }, - ); - assert(payment); + orderId: v4(), + amount: 1000, + }, + ); + assert(payment); - await sleep_for(1); - if (previous.value !== null) - await TestValidator.error( - "VirtualTossStorageProvider.payments.get() for expired record", - )(() => - toss.functional.v1.payments.at( - TestConnection.FAKE, - previous.value!, - ), - ); - await toss.functional.v1.payments.at( - TestConnection.FAKE, - payment.paymentKey, - ); - previous.value = payment.paymentKey; - }); - FakeTossConfiguration.EXPIRATION.time = time; + await sleep_for(1); + if (previous.value !== null) + await TestValidator.error( + "VirtualTossStorageProvider.payments.get() for expired record", + )(() => + toss.functional.v1.payments.at(TestConnection.FAKE, previous.value!), + ); + await toss.functional.v1.payments.at( + TestConnection.FAKE, + payment.paymentKey, + ); + previous.value = payment.paymentKey; + }); + FakeTossConfiguration.EXPIRATION.time = time; } diff --git a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts index e4e2a8a..7253949 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts @@ -11,80 +11,80 @@ import { AdvancedRandomGenerator } from "../internal/AdvancedRandomGenerator"; import { TestConnection } from "../internal/TestConnection"; export async function test_fake_virtual_account_payment(): Promise { - //---- - // 결제하기 - //---- - // 결제 요청 레코드 발행하기 - // - // 백엔드 서버에서 토스 페이먼츠 서버의 API 를 직접 호출하여, 즉시 승인되는 가상 계좌 - // 결제를 진행하는 게 아닌, 프론트 앱이 토스 페이먼츠가 제공해주는 결제 창을 이용한다는 - // 가정. - // - // 토스 페이먼츠는 이처럼 프론트 앱에서 백엔드 서버를 거치지 않고, 토스 페이먼츠 고유의 - // 결제 창을 이용하여 직접 결제를 진행하는 경우, 백엔드 서버에서 이를 별도 승인 - // 처리해주기 전까지 정식 결제로 인정치 아니함. - // - // 때문에 {@link ITossVirtualAccountPayment.IStore.__approved} 값을 `false` 로 하여, - // 백엔드에서 해당 결제 요청 건에 대하여 별도의 승인 처리가 필요한 상황을 고의로 만듦. - const payment: ITossVirtualAccountPayment = - await toss.functional.v1.virtual_accounts.store(TestConnection.FAKE, { - // 가싱 계좌 정보 - method: "virtual-account", - bank: "신한", - customerName: AdvancedRandomGenerator.name(3), + //---- + // 결제하기 + //---- + // 결제 요청 레코드 발행하기 + // + // 백엔드 서버에서 토스 페이먼츠 서버의 API 를 직접 호출하여, 즉시 승인되는 가상 계좌 + // 결제를 진행하는 게 아닌, 프론트 앱이 토스 페이먼츠가 제공해주는 결제 창을 이용한다는 + // 가정. + // + // 토스 페이먼츠는 이처럼 프론트 앱에서 백엔드 서버를 거치지 않고, 토스 페이먼츠 고유의 + // 결제 창을 이용하여 직접 결제를 진행하는 경우, 백엔드 서버에서 이를 별도 승인 + // 처리해주기 전까지 정식 결제로 인정치 아니함. + // + // 때문에 {@link ITossVirtualAccountPayment.IStore.__approved} 값을 `false` 로 하여, + // 백엔드에서 해당 결제 요청 건에 대하여 별도의 승인 처리가 필요한 상황을 고의로 만듦. + const payment: ITossVirtualAccountPayment = + await toss.functional.v1.virtual_accounts.store(TestConnection.FAKE, { + // 가싱 계좌 정보 + method: "virtual-account", + bank: "신한", + customerName: AdvancedRandomGenerator.name(3), - // 주문 정보 - orderId: v4(), - orderName: AdvancedRandomGenerator.name(8), - amount: 25_000, + // 주문 정보 + orderId: v4(), + orderName: AdvancedRandomGenerator.name(8), + amount: 25_000, - // 고의 미승인 처리 - __approved: false, - }); - typia.assert(payment); + // 고의 미승인 처리 + __approved: false, + }); + typia.assert(payment); - // 결제 요청 승인하기 - // - // 백엔드 서버에서 해당 건을 승인함으로써, 비로소 해당 결제가 완성된다. - const approved: ITossPayment = await toss.functional.v1.payments.approve( - TestConnection.FAKE, - payment.paymentKey, - { - orderId: payment.orderId, - amount: payment.totalAmount, - }, - ); - typia.assert(approved); + // 결제 요청 승인하기 + // + // 백엔드 서버에서 해당 건을 승인함으로써, 비로소 해당 결제가 완성된다. + const approved: ITossPayment = await toss.functional.v1.payments.approve( + TestConnection.FAKE, + payment.paymentKey, + { + orderId: payment.orderId, + amount: payment.totalAmount, + }, + ); + typia.assert(approved); - //---- - // 입금하기 - //---- - // 고객이 자신 앞으로 발급된 가상 계좌에 - // 결제 금액을 입금하는 상황 시뮬레이션 - await toss.functional.internal.deposit( - TestConnection.FAKE, - payment.paymentKey, - ); + //---- + // 입금하기 + //---- + // 고객이 자신 앞으로 발급된 가상 계좌에 + // 결제 금액을 입금하는 상황 시뮬레이션 + await toss.functional.internal.deposit( + TestConnection.FAKE, + payment.paymentKey, + ); - // 결제 레코드를 다시 불러보면 - const reloaded: ITossPayment = await toss.functional.v1.payments.at( - TestConnection.FAKE, - payment.paymentKey, - ); - typia.assert(reloaded); + // 결제 레코드를 다시 불러보면 + const reloaded: ITossPayment = await toss.functional.v1.payments.at( + TestConnection.FAKE, + payment.paymentKey, + ); + typia.assert(reloaded); - // 결제 완료 처리되었음을 알 수 있다 - TestValidator.equals("status")(reloaded.status)("DONE"); + // 결제 완료 처리되었음을 알 수 있다 + TestValidator.equals("status")(reloaded.status)("DONE"); - // 실제로 웹훅 이벤트 발생 내역을 보면, - // 고객이 가상 계좌에 결제 금액을 입금하였을 때, - // 결제 완료 처리 또한 함께 이루어졌음을 알 수 있다. - const webhook: ITossPaymentWebhook = FakeTossStorage.webhooks.get( - payment.paymentKey, - ); - TestValidator.equals("status")(webhook.data.status)("DONE"); + // 실제로 웹훅 이벤트 발생 내역을 보면, + // 고객이 가상 계좌에 결제 금액을 입금하였을 때, + // 결제 완료 처리 또한 함께 이루어졌음을 알 수 있다. + const webhook: ITossPaymentWebhook = FakeTossStorage.webhooks.get( + payment.paymentKey, + ); + TestValidator.equals("status")(webhook.data.status)("DONE"); - // if condition 에 의하여 자동 다운 캐스팅 됨. - payment.virtualAccount.accountNumber; - return payment; + // if condition 에 의하여 자동 다운 캐스팅 됨. + payment.virtualAccount.accountNumber; + return payment; } diff --git a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel.ts index e34cc68..9e421df 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel.ts @@ -2,7 +2,4 @@ import { validate_fake_payment_cancel } from "./internal/validate_fake_payment_c import { test_fake_virtual_account_payment } from "./test_fake_virtual_account_payment"; export const test_fake_virtual_account_payment_cancel = - validate_fake_payment_cancel( - () => test_fake_virtual_account_payment(), - true, - ); + validate_fake_payment_cancel(() => test_fake_virtual_account_payment(), true); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_over.ts index ed42c82..3edcf88 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_over.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_over.ts @@ -2,7 +2,7 @@ import { validate_fake_payment_cancel_over } from "./internal/validate_fake_paym import { test_fake_virtual_account_payment } from "./test_fake_virtual_account_payment"; export const test_fake_virtual_account_payment_cancel_over = - validate_fake_payment_cancel_over( - () => test_fake_virtual_account_payment(), - true, - ); + validate_fake_payment_cancel_over( + () => test_fake_virtual_account_payment(), + true, + ); diff --git a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_partial.ts index cc74f56..9258902 100644 --- a/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_partial.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_partial.ts @@ -2,7 +2,7 @@ import { validate_fake_payment_cancel_partial } from "./internal/validate_fake_p import { test_fake_virtual_account_payment } from "./test_fake_virtual_account_payment"; export const test_fake_virtual_account_payment_cancel_partial = - validate_fake_payment_cancel_partial( - () => test_fake_virtual_account_payment(), - true, - ); + validate_fake_payment_cancel_partial( + () => test_fake_virtual_account_payment(), + true, + ); diff --git a/packages/fake-toss-payments-server/test/index.ts b/packages/fake-toss-payments-server/test/index.ts index ef58d94..3348c16 100644 --- a/packages/fake-toss-payments-server/test/index.ts +++ b/packages/fake-toss-payments-server/test/index.ts @@ -5,37 +5,37 @@ import { FakeTossConfiguration } from "../src/FakeTossConfiguration"; import toss from "../src/api"; async function main(): Promise { - // BACKEND SERVER - const backend: FakeTossBackend = new FakeTossBackend(); - await backend.open(); + // BACKEND SERVER + const backend: FakeTossBackend = new FakeTossBackend(); + await backend.open(); - // CONNECTION INFO - const connection: toss.IConnection = { - host: `http://127.0.0.1:${FakeTossConfiguration.API_PORT}`, - }; + // CONNECTION INFO + const connection: toss.IConnection = { + host: `http://127.0.0.1:${FakeTossConfiguration.API_PORT}`, + }; - // DO TEST - const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ - extension: __filename.substr(-2), - prefix: "test", - parameters: () => [connection], - })(__dirname + "/features"); + // DO TEST + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + extension: __filename.substr(-2), + prefix: "test", + parameters: () => [connection], + })(__dirname + "/features"); - // TERMINATE - await backend.close(); + // TERMINATE + await backend.close(); - const exceptions: Error[] = report.executions - .filter((exec) => exec.error !== null) - .map((exec) => exec.error!); - if (exceptions.length === 0) { - console.log(`Total elapsed time: ${report.time.toLocaleString()} ms`); - console.log("Success"); - } else { - for (const exp of exceptions) console.log(exp); - process.exit(-1); - } + const exceptions: Error[] = report.executions + .filter((exec) => exec.error !== null) + .map((exec) => exec.error!); + if (exceptions.length === 0) { + console.log(`Total elapsed time: ${report.time.toLocaleString()} ms`); + console.log("Success"); + } else { + for (const exp of exceptions) console.log(exp); + process.exit(-1); + } } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/fake-toss-payments-server/test/internal/AdvancedRandomGenerator.ts b/packages/fake-toss-payments-server/test/internal/AdvancedRandomGenerator.ts index 7fe2976..57f9590 100644 --- a/packages/fake-toss-payments-server/test/internal/AdvancedRandomGenerator.ts +++ b/packages/fake-toss-payments-server/test/internal/AdvancedRandomGenerator.ts @@ -2,15 +2,15 @@ import { RandomGenerator } from "@nestia/e2e"; import { randint } from "tstl/algorithm/random"; export const AdvancedRandomGenerator = { - ...RandomGenerator, - name: (length: number = 3) => - new Array(length) - .fill("") - .map(() => String.fromCharCode(randint(44031, 55203))) - .join(""), - cardNumber: () => - new Array(4) - .fill("") - .map(() => randint(0, 9999).toString().padStart(4, "0")) - .join(""), + ...RandomGenerator, + name: (length: number = 3) => + new Array(length) + .fill("") + .map(() => String.fromCharCode(randint(44031, 55203))) + .join(""), + cardNumber: () => + new Array(4) + .fill("") + .map(() => randint(0, 9999).toString().padStart(4, "0")) + .join(""), }; diff --git a/packages/fake-toss-payments-server/test/internal/TestConnection.ts b/packages/fake-toss-payments-server/test/internal/TestConnection.ts index ecaf94f..cb2c313 100644 --- a/packages/fake-toss-payments-server/test/internal/TestConnection.ts +++ b/packages/fake-toss-payments-server/test/internal/TestConnection.ts @@ -4,21 +4,17 @@ import { FakeTossConfiguration } from "../../src/FakeTossConfiguration"; import toss from "../../src/api"; export namespace TestConnection { - export const FAKE: toss.IConnection = { - host: `http://127.0.0.1:${FakeTossConfiguration.API_PORT}`, - headers: { - Authorization: `Basic ${btoa( - "test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:", - )}`, - }, - }; + export const FAKE: toss.IConnection = { + host: `http://127.0.0.1:${FakeTossConfiguration.API_PORT}`, + headers: { + Authorization: `Basic ${btoa("test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:")}`, + }, + }; - export const REAL: toss.IConnection = { - host: `https://api.tosspayments.com`, - headers: { - Authorization: `Basic ${btoa( - "test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:", - )}`, - }, - }; + export const REAL: toss.IConnection = { + host: `https://api.tosspayments.com`, + headers: { + Authorization: `Basic ${btoa("test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:")}`, + }, + }; } diff --git a/packages/fake-toss-payments-server/test/tsconfig.json b/packages/fake-toss-payments-server/test/tsconfig.json index 25ae87e..ba053f3 100644 --- a/packages/fake-toss-payments-server/test/tsconfig.json +++ b/packages/fake-toss-payments-server/test/tsconfig.json @@ -3,5 +3,8 @@ "compilerOptions": { "outDir": "../bin", }, - "include": [".", "../src"] + "include": [ + ".", + "../src", + ], } \ No newline at end of file diff --git a/packages/iamport-server-api/package.json b/packages/iamport-server-api/package.json index c7f6a8d..50cb499 100644 --- a/packages/iamport-server-api/package.json +++ b/packages/iamport-server-api/package.json @@ -1,6 +1,6 @@ { "name": "iamport-server-api", - "version": "5.0.6", + "version": "5.1.0", "description": "API for Iamport Server", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -34,7 +34,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "@nestia/fetcher": "^2.3.4", - "typia": "^5.2.4" + "@nestia/fetcher": "^2.3.9", + "typia": "^5.2.6" } } \ No newline at end of file diff --git a/packages/iamport-server-api/swagger.json b/packages/iamport-server-api/swagger.json index 08188b7..cca77f1 100644 --- a/packages/iamport-server-api/swagger.json +++ b/packages/iamport-server-api/swagger.json @@ -13,53 +13,53 @@ "info": { "title": "Iamport API", "description": "Built by [fake-iamport-server](https://github.com/samchon/payments/tree/master/packages/fake-iamport-server) with [nestia](https://github.com/samchon/nestia)", - "version": "5.0.6", + "version": "5.1.0", "license": { "name": "MIT" } }, "paths": { - "/certifications/{imp_uid}": { + "/subscribe/customers/{customer_uid}": { "get": { "tags": [], "parameters": [ { - "name": "imp_uid", + "name": "customer_uid", "in": "path", "schema": { "type": "string" }, - "description": "대상 본인인증 정보의 ", + "description": "고객 (간편 결제 카드) 식별자 키", "required": true } ], "responses": { "200": { - "description": "본인인증 정보", + "description": "간편 결제 카드 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportCertification" + "$ref": "#/components/schemas/IIamportResponseIIamportSubscription" } } }, "x-nestia-encrypted": false } }, - "summary": "본인인증 정보 열람하기", - "description": "본인인증 정보 열람하기.\n\n`certiciations.at` 은 본인인증 정보를 열람할 때 사용하는 API 함수이다.\n\n다만 이 API 함수를 통하여 열람한 본인인증 정보 {@link IIamportCertification } 이\n곧 OTP 인증까지 마쳐 본인인증을 모두 마친 레코드라는 보장은 없다. 본인인증의 완결\n여부는 오직, {@link IIamportCertification.certified } 값을 직접 검사해봐야만 알\n수 있기 때문이다.", + "summary": "간편 결제 카드 정보 조회하기", + "description": "간편 결제 카드 정보 조회하기.\n\n`subscribe.customers.at` 은 고객이 {@link store } 나 혹은 아임포트가 제공하는\n간편 결제 카드 등록 창을 이용하여 저장한 간편 결제 카드 정보를 조회하는 API\n함수이다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "certifications.at", + "x-nestia-namespace": "subscribe.customers.at", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "imp_uid", + "text": "customer_uid", "kind": "parameterName" }, { @@ -67,27 +67,8 @@ "kind": "space" }, { - "text": "대상 본인인증 정보의 ", + "text": "고객 (간편 결제 카드) 식별자 키", "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "IIamportCertification.imp_uid", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts", - "textSpan": { - "start": 455, - "length": 16 - } - } - }, - { - "text": "}", - "kind": "link" } ] }, @@ -95,7 +76,7 @@ "name": "returns", "text": [ { - "text": "본인인증 정보", + "text": "간편 결제 카드 정보", "kind": "text" } ] @@ -121,46 +102,58 @@ ], "x-nestia-method": "GET" }, - "delete": { + "post": { "tags": [], "parameters": [ { - "name": "imp_uid", + "name": "customer_uid", "in": "path", "schema": { "type": "string" }, - "description": "대상 본인인증 정보의 ", + "description": "고객 (간편 결제 카드) 식별자 키", "required": true } ], + "requestBody": { + "description": "카드 입력 정보", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IIamportSubscription.IStore" + } + } + }, + "required": true, + "x-nestia-encrypted": false + }, "responses": { - "200": { - "description": "삭제된 본인인증 정보", + "201": { + "description": "간편 결제 카드 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportCertification" + "$ref": "#/components/schemas/IIamportResponseIIamportSubscription" } } }, "x-nestia-encrypted": false } }, - "summary": "본인인증 정보 삭제하기", - "description": "본인인증 정보 삭제하기.", + "summary": "간편 결제 카드 등록하기", + "description": "간편 결제 카드 등록하기.\n\n`subscribe.customers.stoer` 는 고객이 자신의 카드를 서버에 등록해두고, 매번 결제가\n필요할 때마다 카드 정보를 반복 입력하는 일 없이, 간편하게 결제를 진행하고자 할 때\n사용하는 API 함수이다.\n\n참고로 `subscribe.customers.store` 는 클라이언트 어플리케이션이 아임포트가 제공하는\n간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는\n일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 상황을 시뮬레이션하기 위하여,\n테스트 자동화 프로그램 수준에서 사용될 수는 있다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "certifications.erase", + "x-nestia-namespace": "subscribe.customers.store", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "imp_uid", + "text": "customer_uid", "kind": "parameterName" }, { @@ -168,27 +161,25 @@ "kind": "space" }, { - "text": "대상 본인인증 정보의 ", + "text": "고객 (간편 결제 카드) 식별자 키", "kind": "text" - }, + } + ] + }, + { + "name": "param", + "text": [ { - "text": "{@link ", - "kind": "link" + "text": "input", + "kind": "parameterName" }, { - "text": "IIamportCertification.imp_uid", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts", - "textSpan": { - "start": 455, - "length": 16 - } - } + "text": " ", + "kind": "space" }, { - "text": "}", - "kind": "link" + "text": "카드 입력 정보", + "kind": "text" } ] }, @@ -196,7 +187,7 @@ "name": "returns", "text": [ { - "text": "삭제된 본인인증 정보", + "text": "간편 결제 카드 정보", "kind": "text" } ] @@ -220,52 +211,48 @@ ] } ], - "x-nestia-method": "DELETE" - } - }, - "/certifications/otp/request": { - "post": { + "x-nestia-method": "POST" + }, + "delete": { "tags": [], - "parameters": [], - "requestBody": { - "description": "본인인증 요청 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IIamportCertification.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, + "parameters": [ + { + "name": "customer_uid", + "in": "path", + "schema": { + "type": "string" + }, + "description": "고객 (간편 결제 카드) 식별자 키", + "required": true + } + ], "responses": { - "201": { - "description": "진행 중인 본인인증의 식별자 정보", + "200": { + "description": "삭제된 간편 결제 카드 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportCertification.IAccessor" + "$ref": "#/components/schemas/IIamportResponseIIamportSubscription" } } }, "x-nestia-encrypted": false } }, - "summary": "본인인증 요청하기", - "description": "본인인증 요청하기.\n\n`certifications.otp.request` 는 아임포트 서버에 본인인증을 요청하는 API 함수이다.\n이 API 를 호출하면 본인인증 대상자의 핸드폰으로 OTP 문자가 전송되며, 본인인증\n대상자가 {@link certifications.otp.confirm } 을 통하여 이 OTP 번호를 정확히\n입력함으로써, 본인인증이 완결된다.\n\n또한 본인인증 대상자가 자신의 핸드폰으로 전송된 OTP 문자를 입력하기 전에도,\n여전히해당 본인인증 내역은 {@link certifications.at } 함수를 통하여 조회할 수 있다.\n다만, 이 때 리턴되는 {@link IIamportCertification } 에서 인증의 완결 여부를\n지칭하는 {@link IIamportCertification.certified } 값은 `false` 이다.", + "summary": "간편 결제 카드 삭제하기", + "description": "간편 결제 카드 삭제하기.\n\n간편 결제를 위하여 등록한 카드를 제거한다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "certifications.otp.request.request", + "x-nestia-namespace": "subscribe.customers.erase", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "input", + "text": "customer_uid", "kind": "parameterName" }, { @@ -273,7 +260,7 @@ "kind": "space" }, { - "text": "본인인증 요청 정보", + "text": "고객 (간편 결제 카드) 식별자 키", "kind": "text" } ] @@ -282,7 +269,7 @@ "name": "returns", "text": [ { - "text": "진행 중인 본인인증의 식별자 정보", + "text": "삭제된 간편 결제 카드 정보", "kind": "text" } ] @@ -306,29 +293,19 @@ ] } ], - "x-nestia-method": "POST" + "x-nestia-method": "DELETE" } }, - "/certifications/otp/confirm/{imp_uid}": { + "/subscribe/payments/onetime": { "post": { "tags": [], - "parameters": [ - { - "name": "imp_uid", - "in": "path", - "schema": { - "type": "string" - }, - "description": "대상 본인인증 정보의 ", - "required": true - } - ], + "parameters": [], "requestBody": { - "description": "OTP 코드", + "description": "카드 결제 신청 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportCertification.IConfirm" + "$ref": "#/components/schemas/IIamportSubscription.IOnetime" } } }, @@ -337,62 +314,26 @@ }, "responses": { "201": { - "description": "인증 완료된 본인인증 정보", + "description": "카드 결제 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportCertification" + "$ref": "#/components/schemas/IIamportResponseIIamportCardPayment" } } }, "x-nestia-encrypted": false } }, - "summary": "본인인증 시 발급된 OTP 코드 입력하기", - "description": "본인인증 시 발급된 OTP 코드 입력하기.\n\n`certifications.otp.confirm` 는 {@link certifications.otp.request } 를 통하여\n발급된 본인인증 건에 대하여, 본인인증 대상자의 휴대폰으로 전송된 OTP 번호를\n검증하고, 입력한 OTP 번호가 맞거든 해당 본인인증 건을 승인하여 완료 처리해주는\nAPI 함수이다.\n\n이처럼 본인인증을 완료하거든, 해당 본인인증 건 {@link IIamportCertification } 의\n{@link IIamportCertification.certified } 값이 비로소 `true` 로 변경되어,\n비로소 완결된다.", + "summary": "카드로 결제하기, 더불어 간편 결제용으로 등록 가능", + "description": "카드로 결제하기, 더불어 간편 결제용으로 등록 가능.\n\n`subscribe.payments.onetime` 은 카드를 매개로 한 결제를 하고자 할 때 호출하는 API\n함수이다. 더하여 입력 값에 {@link IIamportSubscription.IOnetime.customer_uid } 를\n기입하는 경우, 결제에 사용한 카드를 그대로 간편 결제용 카드\n{@link IIamportSubscription } 로 등록해버린다.\n\n다만, 정히 간편 카드 등록과 결제를 동시에 하고 싶다면,\n`subscribe.payments.onetime` 에 {@link IIamportSubscription.IOnetime.customer_uid }\n를 더하기보다, {@link subscribe.customers.store } 와 {@link subscribe.payments.again }\n을 각각 호출하는 것을 권장한다. 그것이 예외적인 상황에 보다 안전하게 대처할 수 있기\n때문이다.\n\n더하여 `subscribe.payments.onetime` 은 클라이언트 어플리케이션이 아임포트가 제공하는\n결제 창을 그대로 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 일은\n없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여, 테스트\n자동화 프로그램 수준에서 사용될 수는 있다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "certifications.otp.confirm.confirm", + "x-nestia-namespace": "subscribe.payments.onetime.onetime", "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "imp_uid", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "대상 본인인증 정보의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "IIamportCertification.imp_uid", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts", - "textSpan": { - "start": 455, - "length": 16 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, { "name": "param", "text": [ @@ -405,7 +346,7 @@ "kind": "space" }, { - "text": "OTP 코드", + "text": "카드 결제 신청 정보", "kind": "text" } ] @@ -414,7 +355,7 @@ "name": "returns", "text": [ { - "text": "인증 완료된 본인인증 정보", + "text": "카드 결제 정보", "kind": "text" } ] @@ -441,16 +382,16 @@ "x-nestia-method": "POST" } }, - "/internal/webhook": { + "/subscribe/payments/again": { "post": { "tags": [], "parameters": [], "requestBody": { - "description": "웹훅 이벤트 정보", + "description": "미리 등록한 카드를 이용한 결제 신청 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportPayment.IWebhook" + "$ref": "#/components/schemas/IIamportSubscription.IAgain" } } }, @@ -459,13 +400,25 @@ }, "responses": { "201": { - "description": "", + "description": "카드 결제 정보", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IIamportResponseIIamportCardPayment" + } + } + }, "x-nestia-encrypted": false } }, - "summary": "웹훅 이벤트 더미 리스너", - "description": "웹훅 이벤트 더미 리스너.\n\n`internal.webhook` 은 실제 아임포트의 서버에는 존재하지 않는 API 로써,\n`fake-impoart-server` 의 {@link Configuration.WEBHOOK_URL } 에 아무런 URL 을 설정하지\n않으면, `fake-iamport-server` 로부터 발생하는 모든 종류의 웹훅 이벤트는 이 곳으로 전달되어\n무의미하게 사라진다.\n\n따라서 `fake-iamport-server` 를 사용하여 아임포트 서버와의 연동을 미리 검증코자 할 때는,\n반드시 {@link Configuration.WEBHOOK_URL } 를 설정하여 웹훅 이벤트가 귀하의 백엔드 서버로\n제대로 전달되도록 하자.", - "x-nestia-namespace": "internal.webhook.webhook", + "summary": "간편 결제에 등록된 카드로 결제하기", + "description": "간편 결제에 등록된 카드로 결제하기.\n\n`subscribe.payments.again` 은 고객이 간편 결제에 등록한 카드로 결제를 진행하고자 할 때\n호출하는 API 함수이다. 이는 간편하고 불편하고를 떠나, 본질적으로 카드 결제의 일환이기에,\n리턴값은 일반적인 카드 결제 때와 동일한 {@link IIamportCardPayment } 이다.\n\n그리고 `subscribe.payments.again` 은 결제 수단 중 유일하게, 클라이언트 어플리케이션이\n아임포트가 제공하는 결체 창을 사용할 수 없어, 오직 귀하의 백엔드 서버가 아임포트의 API\n함수를 직접 호출해야하는 경우에 해당한다. 따라서 간편 결제에 관하여 아임포트 서버와\n연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 대한 별도의\n설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다.", + "security": [ + { + "bearer": [] + } + ], + "x-nestia-namespace": "subscribe.payments.again.again", "x-nestia-jsDocTags": [ { "name": "param", @@ -479,26 +432,44 @@ "kind": "space" }, { - "text": "웹훅 이벤트 정보", + "text": "미리 등록한 카드를 이용한 결제 신청 정보", "kind": "text" } ] }, { - "name": "author", + "name": "returns", "text": [ { - "text": "Samchon", + "text": "카드 결제 정보", "kind": "text" } ] - } - ], - "x-nestia-method": "POST" - } + }, + { + "name": "security", + "text": [ + { + "text": "bearer", + "kind": "text" + } + ] + }, + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ], + "x-nestia-method": "POST" + } }, - "/internal/deposit/{imp_uid}": { - "put": { + "/certifications/{imp_uid}": { + "get": { "tags": [], "parameters": [ { @@ -507,24 +478,31 @@ "schema": { "type": "string" }, - "description": "대상 결제의 ", + "description": "대상 본인인증 정보의 ", "required": true } ], "responses": { - "201": { - "description": "", + "200": { + "description": "본인인증 정보", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IIamportResponseIIamportCertification" + } + } + }, "x-nestia-encrypted": false } }, - "summary": "가상 계좌에 입금하기", - "description": "가상 계좌에 입금하기.\n\n`internal.deposit` 은 실제 아임포트 결제 서버에는 존재하지 않는 API 로써, 가상 계좌\n결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 상황을 시뮬레이션 할 수 있는\n함수이다.\n\n즉, `internal.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 입금을 하고, 그에 따라\n아임포트 서버에서 webhook 이벤트가 발생, 이를 귀하의 백엔드 서버로 전송하는 일련의 상황을\n시뮬레이션하기 위하여 설계된 테스트 함수다.", + "summary": "본인인증 정보 열람하기", + "description": "본인인증 정보 열람하기.\n\n`certiciations.at` 은 본인인증 정보를 열람할 때 사용하는 API 함수이다.\n\n다만 이 API 함수를 통하여 열람한 본인인증 정보 {@link IIamportCertification } 이\n곧 OTP 인증까지 마쳐 본인인증을 모두 마친 레코드라는 보장은 없다. 본인인증의 완결\n여부는 오직, {@link IIamportCertification.certified } 값을 직접 검사해봐야만 알\n수 있기 때문이다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "internal.deposit.deposit", + "x-nestia-namespace": "certifications.at", "x-nestia-jsDocTags": [ { "name": "param", @@ -538,7 +516,7 @@ "kind": "space" }, { - "text": "대상 결제의 ", + "text": "대상 본인인증 정보의 ", "kind": "text" }, { @@ -546,8 +524,15 @@ "kind": "link" }, { - "text": "IIamportVBankPayment.imp_uid ", - "kind": "linkText" + "text": "IIamportCertification.imp_uid", + "kind": "linkName", + "target": { + "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts", + "textSpan": { + "start": 429, + "length": 16 + } + } }, { "text": "}", @@ -555,6 +540,15 @@ } ] }, + { + "name": "returns", + "text": [ + { + "text": "본인인증 정보", + "kind": "text" + } + ] + }, { "name": "security", "text": [ @@ -574,11 +568,9 @@ ] } ], - "x-nestia-method": "PUT" - } - }, - "/payments/{imp_uid}": { - "get": { + "x-nestia-method": "GET" + }, + "delete": { "tags": [], "parameters": [ { @@ -587,40 +579,31 @@ "schema": { "type": "string" }, - "description": "대상 결제 기록의 ", - "required": true - }, - { - "name": "query", - "in": "query", - "schema": { - "$ref": "#/components/schemas/IIamportPayment.IQuery" - }, - "description": "결제 수단이 페이팔인 경우에 사용", + "description": "대상 본인인증 정보의 ", "required": true } ], "responses": { "200": { - "description": "결제 정보", + "description": "삭제된 본인인증 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportPayment" + "$ref": "#/components/schemas/IIamportResponseIIamportCertification" } } }, "x-nestia-encrypted": false } }, - "summary": "결제 기록 열람하기", - "description": "결제 기록 열람하기.\n\n아임포트를 통하여 발생한 결제 기록을 열람한다.", + "summary": "본인인증 정보 삭제하기", + "description": "본인인증 정보 삭제하기.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "payments.at", + "x-nestia-namespace": "certifications.erase", "x-nestia-jsDocTags": [ { "name": "param", @@ -634,7 +617,7 @@ "kind": "space" }, { - "text": "대상 결제 기록의 ", + "text": "대상 본인인증 정보의 ", "kind": "text" }, { @@ -642,12 +625,12 @@ "kind": "link" }, { - "text": "IIamportPayment.imp_uid", + "text": "IIamportCertification.imp_uid", "kind": "linkName", "target": { - "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", + "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts", "textSpan": { - "start": 2306, + "start": 429, "length": 16 } } @@ -658,28 +641,11 @@ } ] }, - { - "name": "param", - "text": [ - { - "text": "query", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "결제 수단이 페이팔인 경우에 사용", - "kind": "text" - } - ] - }, { "name": "returns", "text": [ { - "text": "결제 정보", + "text": "삭제된 본인인증 정보", "kind": "text" } ] @@ -703,19 +669,19 @@ ] } ], - "x-nestia-method": "GET" + "x-nestia-method": "DELETE" } }, - "/payments/cancel": { + "/certifications/otp/request": { "post": { "tags": [], "parameters": [], "requestBody": { - "description": "결제 취소 입력 정보", + "description": "본인인증 요청 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportPaymentCancel.IStore" + "$ref": "#/components/schemas/IIamportCertification.IStore" } } }, @@ -724,25 +690,25 @@ }, "responses": { "201": { - "description": "취소된 결제 정보", + "description": "진행 중인 본인인증의 식별자 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportPayment" + "$ref": "#/components/schemas/IIamportResponseIIamportCertification.IAccessor" } } }, "x-nestia-encrypted": false } }, - "summary": "결제 취소하기", - "description": "결제 취소하기.\n\n만약 가상 계좌를 통한 결제였다면, 반드시 환불 계좌 정보를 입력해줘야 한다.", + "summary": "본인인증 요청하기", + "description": "본인인증 요청하기.\n\n`certifications.otp.request` 는 아임포트 서버에 본인인증을 요청하는 API 함수이다.\n이 API 를 호출하면 본인인증 대상자의 핸드폰으로 OTP 문자가 전송되며, 본인인증\n대상자가 {@link certifications.otp.confirm } 을 통하여 이 OTP 번호를 정확히\n입력함으로써, 본인인증이 완결된다.\n\n또한 본인인증 대상자가 자신의 핸드폰으로 전송된 OTP 문자를 입력하기 전에도,\n여전히해당 본인인증 내역은 {@link certifications.at } 함수를 통하여 조회할 수 있다.\n다만, 이 때 리턴되는 {@link IIamportCertification } 에서 인증의 완결 여부를\n지칭하는 {@link IIamportCertification.certified } 값은 `false` 이다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "payments.cancel.cancel", + "x-nestia-namespace": "certifications.otp.request.request", "x-nestia-jsDocTags": [ { "name": "param", @@ -756,7 +722,7 @@ "kind": "space" }, { - "text": "결제 취소 입력 정보", + "text": "본인인증 요청 정보", "kind": "text" } ] @@ -765,7 +731,7 @@ "name": "returns", "text": [ { - "text": "취소된 결제 정보", + "text": "진행 중인 본인인증의 식별자 정보", "kind": "text" } ] @@ -792,8 +758,8 @@ "x-nestia-method": "POST" } }, - "/receipts/{imp_uid}": { - "get": { + "/certifications/otp/confirm/{imp_uid}": { + "post": { "tags": [], "parameters": [ { @@ -802,31 +768,43 @@ "schema": { "type": "string" }, - "description": "귀속 결제의 ", + "description": "대상 본인인증 정보의 ", "required": true } ], + "requestBody": { + "description": "OTP 코드", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IIamportCertification.IConfirm" + } + } + }, + "required": true, + "x-nestia-encrypted": false + }, "responses": { - "200": { - "description": "현금 영수증 정보", + "201": { + "description": "인증 완료된 본인인증 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportReceipt" + "$ref": "#/components/schemas/IIamportResponseIIamportCertification" } } }, "x-nestia-encrypted": false } }, - "summary": "현금 영수증 조회하기", - "description": "현금 영수증 조회하기.", + "summary": "본인인증 시 발급된 OTP 코드 입력하기", + "description": "본인인증 시 발급된 OTP 코드 입력하기.\n\n`certifications.otp.confirm` 는 {@link certifications.otp.request } 를 통하여\n발급된 본인인증 건에 대하여, 본인인증 대상자의 휴대폰으로 전송된 OTP 번호를\n검증하고, 입력한 OTP 번호가 맞거든 해당 본인인증 건을 승인하여 완료 처리해주는\nAPI 함수이다.\n\n이처럼 본인인증을 완료하거든, 해당 본인인증 건 {@link IIamportCertification } 의\n{@link IIamportCertification.certified } 값이 비로소 `true` 로 변경되어,\n비로소 완결된다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "receipts.at", + "x-nestia-namespace": "certifications.otp.confirm.confirm", "x-nestia-jsDocTags": [ { "name": "param", @@ -840,7 +818,7 @@ "kind": "space" }, { - "text": "귀속 결제의 ", + "text": "대상 본인인증 정보의 ", "kind": "text" }, { @@ -848,12 +826,12 @@ "kind": "link" }, { - "text": "IIamportPayment.imp_uid", + "text": "IIamportCertification.imp_uid", "kind": "linkName", "target": { - "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", + "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts", "textSpan": { - "start": 2306, + "start": 429, "length": 16 } } @@ -864,11 +842,28 @@ } ] }, + { + "name": "param", + "text": [ + { + "text": "input", + "kind": "parameterName" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "OTP 코드", + "kind": "text" + } + ] + }, { "name": "returns", "text": [ { - "text": "현금 영수증 정보", + "text": "인증 완료된 본인인증 정보", "kind": "text" } ] @@ -892,27 +887,19 @@ ] } ], - "x-nestia-method": "GET" - }, + "x-nestia-method": "POST" + } + }, + "/internal/webhook": { "post": { "tags": [], - "parameters": [ - { - "name": "imp_uid", - "in": "path", - "schema": { - "type": "string" - }, - "description": "귀속 결제의 ", - "required": true - } - ], + "parameters": [], "requestBody": { - "description": "현금 영수증 입력 정보", + "description": "웹훅 이벤트 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportReceipt.IStore" + "$ref": "#/components/schemas/IIamportPayment.IWebhook" } } }, @@ -921,31 +908,19 @@ }, "responses": { "201": { - "description": "현금 영수증 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportReceipt" - } - } - }, + "description": "", "x-nestia-encrypted": false } }, - "summary": "현금 영수증 발행하기", - "description": "현금 영수증 발행하기.", - "security": [ - { - "bearer": [] - } - ], - "x-nestia-namespace": "receipts.store", + "summary": "웹훅 이벤트 더미 리스너", + "description": "웹훅 이벤트 더미 리스너.\n\n`internal.webhook` 은 실제 아임포트의 서버에는 존재하지 않는 API 로써,\n`fake-impoart-server` 의 {@link Configuration.WEBHOOK_URL } 에 아무런 URL 을 설정하지\n않으면, `fake-iamport-server` 로부터 발생하는 모든 종류의 웹훅 이벤트는 이 곳으로 전달되어\n무의미하게 사라진다.\n\n따라서 `fake-iamport-server` 를 사용하여 아임포트 서버와의 연동을 미리 검증코자 할 때는,\n반드시 {@link Configuration.WEBHOOK_URL } 를 설정하여 웹훅 이벤트가 귀하의 백엔드 서버로\n제대로 전달되도록 하자.", + "x-nestia-namespace": "internal.webhook.webhook", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "imp_uid", + "text": "input", "kind": "parameterName" }, { @@ -953,35 +928,58 @@ "kind": "space" }, { - "text": "귀속 결제의 ", + "text": "웹훅 이벤트 정보", "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, + } + ] + }, + { + "name": "author", + "text": [ { - "text": "IIamportPayment.imp_uid", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", - "textSpan": { - "start": 2306, - "length": 16 - } - } - }, - { - "text": "}", - "kind": "link" + "text": "Samchon", + "kind": "text" } ] - }, + } + ], + "x-nestia-method": "POST" + } + }, + "/internal/deposit/{imp_uid}": { + "put": { + "tags": [], + "parameters": [ + { + "name": "imp_uid", + "in": "path", + "schema": { + "type": "string" + }, + "description": "대상 결제의 ", + "required": true + } + ], + "responses": { + "201": { + "description": "", + "x-nestia-encrypted": false + } + }, + "summary": "가상 계좌에 입금하기", + "description": "가상 계좌에 입금하기.\n\n`internal.deposit` 은 실제 아임포트 결제 서버에는 존재하지 않는 API 로써, 가상 계좌\n결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 상황을 시뮬레이션 할 수 있는\n함수이다.\n\n즉, `internal.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 입금을 하고, 그에 따라\n아임포트 서버에서 webhook 이벤트가 발생, 이를 귀하의 백엔드 서버로 전송하는 일련의 상황을\n시뮬레이션하기 위하여 설계된 테스트 함수다.", + "security": [ + { + "bearer": [] + } + ], + "x-nestia-namespace": "internal.deposit.deposit", + "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "input", + "text": "imp_uid", "kind": "parameterName" }, { @@ -989,17 +987,20 @@ "kind": "space" }, { - "text": "현금 영수증 입력 정보", + "text": "대상 결제의 ", "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ + }, { - "text": "현금 영수증 정보", - "kind": "text" + "text": "{@link ", + "kind": "link" + }, + { + "text": "IIamportVBankPayment.imp_uid ", + "kind": "linkText" + }, + { + "text": "}", + "kind": "link" } ] }, @@ -1022,9 +1023,11 @@ ] } ], - "x-nestia-method": "POST" - }, - "delete": { + "x-nestia-method": "PUT" + } + }, + "/payments/{imp_uid}": { + "get": { "tags": [], "parameters": [ { @@ -1033,31 +1036,40 @@ "schema": { "type": "string" }, - "description": "귀속 결제의 ", + "description": "대상 결제 기록의 ", + "required": true + }, + { + "name": "query", + "in": "query", + "schema": { + "$ref": "#/components/schemas/IIamportPayment.IQuery" + }, + "description": "결제 수단이 페이팔인 경우에 사용", "required": true } ], "responses": { "200": { - "description": "취소된 현금 영수증 정보", + "description": "결제 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportReceipt" + "$ref": "#/components/schemas/IIamportResponseIIamportPayment" } } }, "x-nestia-encrypted": false } }, - "summary": "현금 영수증 취소하기", - "description": "현금 영수증 취소하기.", + "summary": "결제 기록 열람하기", + "description": "결제 기록 열람하기.\n\n아임포트를 통하여 발생한 결제 기록을 열람한다.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "receipts.erase", + "x-nestia-namespace": "payments.at", "x-nestia-jsDocTags": [ { "name": "param", @@ -1071,7 +1083,7 @@ "kind": "space" }, { - "text": "귀속 결제의 ", + "text": "대상 결제 기록의 ", "kind": "text" }, { @@ -1084,7 +1096,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", "textSpan": { - "start": 2306, + "start": 2017, "length": 16 } } @@ -1095,11 +1107,28 @@ } ] }, + { + "name": "param", + "text": [ + { + "text": "query", + "kind": "parameterName" + }, + { + "text": " ", + "kind": "space" + }, + { + "text": "결제 수단이 페이팔인 경우에 사용", + "kind": "text" + } + ] + }, { "name": "returns", "text": [ { - "text": "취소된 현금 영수증 정보", + "text": "결제 정보", "kind": "text" } ] @@ -1123,19 +1152,19 @@ ] } ], - "x-nestia-method": "DELETE" + "x-nestia-method": "GET" } }, - "/users/getToken": { + "/payments/cancel": { "post": { "tags": [], "parameters": [], "requestBody": { - "description": "아임포트의 API 및 secret 키 정보", + "description": "결제 취소 입력 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportUser.IAccessor" + "$ref": "#/components/schemas/IIamportPaymentCancel.IStore" } } }, @@ -1144,20 +1173,25 @@ }, "responses": { "201": { - "description": "유저 인증 토큰 정보", + "description": "취소된 결제 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportUser" + "$ref": "#/components/schemas/IIamportResponseIIamportPayment" } } }, "x-nestia-encrypted": false } }, - "summary": "유저 인증 토큰 발행하기", - "description": "유저 인증 토큰 발행하기.\n\n아임포트에 가입하여 부여받은 API 및 secret 키를 토대로, 유저 인증 토큰을 발행한다.\n\n단, 아임포트가 발급해주는 유저 인증 토큰에는 유효 시간 {@link IIamportUser.expired_at }\n이 있어, 해당 시간이 지나거든 기 발급 토큰이 만료되어 더 이상 쓸 수 없게 된다. 때문에\n아임포트의 이러한 시간 제한에 구애받지 않고 자유로이 아임포트의 API 를 이용하고 싶다면,\n`iamport-server-api` 에서 제공해주는 {@link IamportConnector } 를 활용하도록 하자.", - "x-nestia-namespace": "users.getToken.getToken", + "summary": "결제 취소하기", + "description": "결제 취소하기.\n\n만약 가상 계좌를 통한 결제였다면, 반드시 환불 계좌 정보를 입력해줘야 한다.", + "security": [ + { + "bearer": [] + } + ], + "x-nestia-namespace": "payments.cancel.cancel", "x-nestia-jsDocTags": [ { "name": "param", @@ -1171,7 +1205,7 @@ "kind": "space" }, { - "text": "아임포트의 API 및 secret 키 정보", + "text": "결제 취소 입력 정보", "kind": "text" } ] @@ -1180,7 +1214,16 @@ "name": "returns", "text": [ { - "text": "유저 인증 토큰 정보", + "text": "취소된 결제 정보", + "kind": "text" + } + ] + }, + { + "name": "security", + "text": [ + { + "text": "bearer", "kind": "text" } ] @@ -1198,49 +1241,47 @@ "x-nestia-method": "POST" } }, - "/vbanks": { - "post": { + "/receipts/{imp_uid}": { + "get": { "tags": [], - "parameters": [], - "requestBody": { - "description": "가상 계좌 입력 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IIamportVBankPayment.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, + "parameters": [ + { + "name": "imp_uid", + "in": "path", + "schema": { + "type": "string" + }, + "description": "귀속 결제의 ", + "required": true + } + ], "responses": { - "201": { - "description": "가상 계좌 결제 정보", + "200": { + "description": "현금 영수증 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportVBankPayment" + "$ref": "#/components/schemas/IIamportResponseIIamportReceipt" } } }, "x-nestia-encrypted": false } }, - "summary": "가상 계좌 발급하기", - "description": "가상 계좌 발급하기.", + "summary": "현금 영수증 조회하기", + "description": "현금 영수증 조회하기.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "vbanks.store", + "x-nestia-namespace": "receipts.at", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "input", + "text": "imp_uid", "kind": "parameterName" }, { @@ -1248,8 +1289,27 @@ "kind": "space" }, { - "text": "가상 계좌 입력 정보", + "text": "귀속 결제의 ", "kind": "text" + }, + { + "text": "{@link ", + "kind": "link" + }, + { + "text": "IIamportPayment.imp_uid", + "kind": "linkName", + "target": { + "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", + "textSpan": { + "start": 2017, + "length": 16 + } + } + }, + { + "text": "}", + "kind": "link" } ] }, @@ -1257,7 +1317,7 @@ "name": "returns", "text": [ { - "text": "가상 계좌 결제 정보", + "text": "현금 영수증 정보", "kind": "text" } ] @@ -1281,17 +1341,27 @@ ] } ], - "x-nestia-method": "POST" + "x-nestia-method": "GET" }, - "put": { + "post": { "tags": [], - "parameters": [], + "parameters": [ + { + "name": "imp_uid", + "in": "path", + "schema": { + "type": "string" + }, + "description": "귀속 결제의 ", + "required": true + } + ], "requestBody": { - "description": "가상 계좌 편집 입력 정보", + "description": "현금 영수증 입력 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportVBankPayment.IUpdate" + "$ref": "#/components/schemas/IIamportReceipt.IStore" } } }, @@ -1300,31 +1370,31 @@ }, "responses": { "201": { - "description": "편집된 가상 계좌 결제 정보", + "description": "현금 영수증 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportVBankPayment" + "$ref": "#/components/schemas/IIamportResponseIIamportReceipt" } } }, "x-nestia-encrypted": false } }, - "summary": "가상 계좌 편집하기", - "description": "가상 계좌 편집하기.", + "summary": "현금 영수증 발행하기", + "description": "현금 영수증 발행하기.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "vbanks.update", + "x-nestia-namespace": "receipts.store", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "input", + "text": "imp_uid", "kind": "parameterName" }, { @@ -1332,83 +1402,35 @@ "kind": "space" }, { - "text": "가상 계좌 편집 입력 정보", + "text": "귀속 결제의 ", "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ + }, { - "text": "편집된 가상 계좌 결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ + "text": "{@link ", + "kind": "link" + }, { - "text": "bearer", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ + "text": "IIamportPayment.imp_uid", + "kind": "linkName", + "target": { + "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", + "textSpan": { + "start": 2017, + "length": 16 + } + } + }, { - "text": "Samchon", - "kind": "text" + "text": "}", + "kind": "link" } ] - } - ], - "x-nestia-method": "PUT" - } - }, - "/subscribe/customers/{customer_uid}": { - "get": { - "tags": [], - "parameters": [ - { - "name": "customer_uid", - "in": "path", - "schema": { - "type": "string" - }, - "description": "고객 (간편 결제 카드) 식별자 키", - "required": true - } - ], - "responses": { - "200": { - "description": "간편 결제 카드 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportSubscription" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "간편 결제 카드 정보 조회하기", - "description": "간편 결제 카드 정보 조회하기.\n\n`subscribe.customers.at` 은 고객이 {@link store } 나 혹은 아임포트가 제공하는\n간편 결제 카드 등록 창을 이용하여 저장한 간편 결제 카드 정보를 조회하는 API\n함수이다.", - "security": [ - { - "bearer": [] - } - ], - "x-nestia-namespace": "subscribe.customers.at", - "x-nestia-jsDocTags": [ + }, { "name": "param", "text": [ { - "text": "customer_uid", + "text": "input", "kind": "parameterName" }, { @@ -1416,7 +1438,7 @@ "kind": "space" }, { - "text": "고객 (간편 결제 카드) 식별자 키", + "text": "현금 영수증 입력 정보", "kind": "text" } ] @@ -1425,7 +1447,7 @@ "name": "returns", "text": [ { - "text": "간편 결제 카드 정보", + "text": "현금 영수증 정보", "kind": "text" } ] @@ -1449,60 +1471,48 @@ ] } ], - "x-nestia-method": "GET" + "x-nestia-method": "POST" }, - "post": { + "delete": { "tags": [], "parameters": [ { - "name": "customer_uid", + "name": "imp_uid", "in": "path", "schema": { "type": "string" }, - "description": "고객 (간편 결제 카드) 식별자 키", + "description": "귀속 결제의 ", "required": true } ], - "requestBody": { - "description": "카드 입력 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IIamportSubscription.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, "responses": { - "201": { - "description": "간편 결제 카드 정보", + "200": { + "description": "취소된 현금 영수증 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportSubscription" + "$ref": "#/components/schemas/IIamportResponseIIamportReceipt" } } }, "x-nestia-encrypted": false } }, - "summary": "간편 결제 카드 등록하기", - "description": "간편 결제 카드 등록하기.\n\n`subscribe.customers.stoer` 는 고객이 자신의 카드를 서버에 등록해두고, 매번 결제가\n필요할 때마다 카드 정보를 반복 입력하는 일 없이, 간편하게 결제를 진행하고자 할 때\n사용하는 API 함수이다.\n\n참고로 `subscribe.customers.store` 는 클라이언트 어플리케이션이 아임포트가 제공하는\n간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는\n일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 상황을 시뮬레이션하기 위하여,\n테스트 자동화 프로그램 수준에서 사용될 수는 있다.", + "summary": "현금 영수증 취소하기", + "description": "현금 영수증 취소하기.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "subscribe.customers.store", + "x-nestia-namespace": "receipts.erase", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "customer_uid", + "text": "imp_uid", "kind": "parameterName" }, { @@ -1510,25 +1520,27 @@ "kind": "space" }, { - "text": "고객 (간편 결제 카드) 식별자 키", + "text": "귀속 결제의 ", "kind": "text" - } - ] - }, - { - "name": "param", - "text": [ + }, { - "text": "input", - "kind": "parameterName" + "text": "{@link ", + "kind": "link" }, { - "text": " ", - "kind": "space" + "text": "IIamportPayment.imp_uid", + "kind": "linkName", + "target": { + "fileName": "D:/github/samchon/payments/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts", + "textSpan": { + "start": 2017, + "length": 16 + } + } }, { - "text": "카드 입력 정보", - "kind": "text" + "text": "}", + "kind": "link" } ] }, @@ -1536,7 +1548,7 @@ "name": "returns", "text": [ { - "text": "간편 결제 카드 정보", + "text": "취소된 현금 영수증 정보", "kind": "text" } ] @@ -1560,48 +1572,47 @@ ] } ], - "x-nestia-method": "POST" - }, - "delete": { + "x-nestia-method": "DELETE" + } + }, + "/users/getToken": { + "post": { "tags": [], - "parameters": [ - { - "name": "customer_uid", - "in": "path", - "schema": { - "type": "string" - }, - "description": "고객 (간편 결제 카드) 식별자 키", - "required": true - } - ], + "parameters": [], + "requestBody": { + "description": "아임포트의 API 및 secret 키 정보", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IIamportUser.IAccessor" + } + } + }, + "required": true, + "x-nestia-encrypted": false + }, "responses": { - "200": { - "description": "삭제된 간편 결제 카드 정보", + "201": { + "description": "유저 인증 토큰 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportSubscription" + "$ref": "#/components/schemas/IIamportResponseIIamportUser" } } }, "x-nestia-encrypted": false } }, - "summary": "간편 결제 카드 삭제하기", - "description": "간편 결제 카드 삭제하기.\n\n간편 결제를 위하여 등록한 카드를 제거한다.", - "security": [ - { - "bearer": [] - } - ], - "x-nestia-namespace": "subscribe.customers.erase", + "summary": "유저 인증 토큰 발행하기", + "description": "유저 인증 토큰 발행하기.\n\n아임포트에 가입하여 부여받은 API 및 secret 키를 토대로, 유저 인증 토큰을 발행한다.\n\n단, 아임포트가 발급해주는 유저 인증 토큰에는 유효 시간 {@link IIamportUser.expired_at }\n이 있어, 해당 시간이 지나거든 기 발급 토큰이 만료되어 더 이상 쓸 수 없게 된다. 때문에\n아임포트의 이러한 시간 제한에 구애받지 않고 자유로이 아임포트의 API 를 이용하고 싶다면,\n`iamport-server-api` 에서 제공해주는 {@link IamportConnector } 를 활용하도록 하자.", + "x-nestia-namespace": "users.getToken.getToken", "x-nestia-jsDocTags": [ { "name": "param", "text": [ { - "text": "customer_uid", + "text": "input", "kind": "parameterName" }, { @@ -1609,7 +1620,7 @@ "kind": "space" }, { - "text": "고객 (간편 결제 카드) 식별자 키", + "text": "아임포트의 API 및 secret 키 정보", "kind": "text" } ] @@ -1618,16 +1629,7 @@ "name": "returns", "text": [ { - "text": "삭제된 간편 결제 카드 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "bearer", + "text": "유저 인증 토큰 정보", "kind": "text" } ] @@ -1642,19 +1644,19 @@ ] } ], - "x-nestia-method": "DELETE" + "x-nestia-method": "POST" } }, - "/subscribe/payments/onetime": { + "/vbanks": { "post": { "tags": [], "parameters": [], "requestBody": { - "description": "카드 결제 신청 정보", + "description": "가상 계좌 입력 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportSubscription.IOnetime" + "$ref": "#/components/schemas/IIamportVBankPayment.IStore" } } }, @@ -1663,25 +1665,25 @@ }, "responses": { "201": { - "description": "카드 결제 정보", + "description": "가상 계좌 결제 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportCardPayment" + "$ref": "#/components/schemas/IIamportResponseIIamportVBankPayment" } } }, "x-nestia-encrypted": false } }, - "summary": "카드로 결제하기, 더불어 간편 결제용으로 등록 가능", - "description": "카드로 결제하기, 더불어 간편 결제용으로 등록 가능.\n\n`subscribe.payments.onetime` 은 카드를 매개로 한 결제를 하고자 할 때 호출하는 API\n함수이다. 더하여 입력 값에 {@link IIamportSubscription.IOnetime.customer_uid } 를\n기입하는 경우, 결제에 사용한 카드를 그대로 간편 결제용 카드\n{@link IIamportSubscription } 로 등록해버린다.\n\n다만, 정히 간편 카드 등록과 결제를 동시에 하고 싶다면,\n`subscribe.payments.onetime` 에 {@link IIamportSubscription.IOnetime.customer_uid }\n를 더하기보다, {@link subscribe.customers.store } 와 {@link subscribe.payments.again }\n을 각각 호출하는 것을 권장한다. 그것이 예외적인 상황에 보다 안전하게 대처할 수 있기\n때문이다.\n\n더하여 `subscribe.payments.onetime` 은 클라이언트 어플리케이션이 아임포트가 제공하는\n결제 창을 그대로 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 일은\n없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여, 테스트\n자동화 프로그램 수준에서 사용될 수는 있다.", + "summary": "가상 계좌 발급하기", + "description": "가상 계좌 발급하기.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "subscribe.payments.onetime.onetime", + "x-nestia-namespace": "vbanks.store", "x-nestia-jsDocTags": [ { "name": "param", @@ -1695,7 +1697,7 @@ "kind": "space" }, { - "text": "카드 결제 신청 정보", + "text": "가상 계좌 입력 정보", "kind": "text" } ] @@ -1704,7 +1706,7 @@ "name": "returns", "text": [ { - "text": "카드 결제 정보", + "text": "가상 계좌 결제 정보", "kind": "text" } ] @@ -1729,18 +1731,16 @@ } ], "x-nestia-method": "POST" - } - }, - "/subscribe/payments/again": { - "post": { + }, + "put": { "tags": [], "parameters": [], "requestBody": { - "description": "미리 등록한 카드를 이용한 결제 신청 정보", + "description": "가상 계좌 편집 입력 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportSubscription.IAgain" + "$ref": "#/components/schemas/IIamportVBankPayment.IUpdate" } } }, @@ -1749,25 +1749,25 @@ }, "responses": { "201": { - "description": "카드 결제 정보", + "description": "편집된 가상 계좌 결제 정보", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IIamportResponseIIamportCardPayment" + "$ref": "#/components/schemas/IIamportResponseIIamportVBankPayment" } } }, "x-nestia-encrypted": false } }, - "summary": "간편 결제에 등록된 카드로 결제하기", - "description": "간편 결제에 등록된 카드로 결제하기.\n\n`subscribe.payments.again` 은 고객이 간편 결제에 등록한 카드로 결제를 진행하고자 할 때\n호출하는 API 함수이다. 이는 간편하고 불편하고를 떠나, 본질적으로 카드 결제의 일환이기에,\n리턴값은 일반적인 카드 결제 때와 동일한 {@link IIamportCardPayment } 이다.\n\n그리고 `subscribe.payments.again` 은 결제 수단 중 유일하게, 클라이언트 어플리케이션이\n아임포트가 제공하는 결체 창을 사용할 수 없어, 오직 귀하의 백엔드 서버가 아임포트의 API\n함수를 직접 호출해야하는 경우에 해당한다. 따라서 간편 결제에 관하여 아임포트 서버와\n연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 대한 별도의\n설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다.", + "summary": "가상 계좌 편집하기", + "description": "가상 계좌 편집하기.", "security": [ { "bearer": [] } ], - "x-nestia-namespace": "subscribe.payments.again.again", + "x-nestia-namespace": "vbanks.update", "x-nestia-jsDocTags": [ { "name": "param", @@ -1781,7 +1781,7 @@ "kind": "space" }, { - "text": "미리 등록한 카드를 이용한 결제 신청 정보", + "text": "가상 계좌 편집 입력 정보", "kind": "text" } ] @@ -1790,7 +1790,7 @@ "name": "returns", "text": [ { - "text": "카드 결제 정보", + "text": "편집된 가상 계좌 결제 정보", "kind": "text" } ] @@ -1814,13 +1814,13 @@ ] } ], - "x-nestia-method": "POST" + "x-nestia-method": "PUT" } } }, "components": { "schemas": { - "IIamportResponseIIamportCertification": { + "IIamportResponseIIamportSubscription": { "type": "object", "properties": { "code": { @@ -1836,7 +1836,7 @@ "type": "string" }, "response": { - "$ref": "#/components/schemas/IIamportCertification" + "$ref": "#/components/schemas/IIamportSubscription" } }, "nullable": false, @@ -1858,152 +1858,104 @@ } ] }, - "IIamportCertification": { + "IIamportSubscription": { "type": "object", "properties": { - "imp_uid": { - "description": "아임포트가 발급해 준 식별자 번호.", + "pg_provider": { "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "merchant_uid": { - "description": "서비스로부터의 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true - }, - "name": { - "description": "본인인증대상자 성명.", + "pg_id": { "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "gender": { - "description": "성별.", + "card_name": { "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "birth": { - "description": "생년월일.\n\n리눅스 타임이 쓰인다.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "birthday": { - "description": "생년월일, YYYYMMDD 형식.", + "card_code": { "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "pattern": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$\">", - "kind": "pattern", - "value": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", - "validate": "/^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "string" }, - "foreigner": { - "description": "외국인 여부.", + "card_number": { "x-typia-required": true, "x-typia-optional": false, - "type": "boolean" + "type": "string" }, - "phone": { - "description": "본인인증 대상자 핸드폰 번호.", + "card_type": { "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "carrier": { - "description": "본인인증 대상자 통신사 코드.", + "customer_name": { "x-typia-required": true, "x-typia-optional": false, "type": "string", - "enum": [ - "SKT", - "KT", - "LGT" - ] + "nullable": true }, - "certified": { - "description": "OTP 인증 여부.", + "customer_tel": { "x-typia-required": true, "x-typia-optional": false, - "type": "boolean" + "type": "string", + "nullable": true }, - "certified_at": { - "description": "OTP 인증 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "customer_email": { "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string", + "nullable": true }, - "unique_key": { - "description": "뭔지 잘 모름, 용도 아시는 분?", + "customer_addr": { "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "string", + "nullable": true }, - "unique_in_site": { - "description": "뭔지 잘 모름, 용도 아시는 분?", + "customer_postcode": { "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "string", + "nullable": true }, - "pg_tid": { - "description": "뭔지 잘 모름, 용도 아시는 분?", + "inserted": { "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "number" }, - "pg_provider": { - "description": "PG 제공자.", + "updated": { "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "number" }, - "origin": { - "description": "뭔지 잘 모름, 용도 아시는 분?", + "customer_uid": { + "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.", "x-typia-required": true, "x-typia-optional": false, "type": "string" - }, - "__otp": { - "description": "(테스트 전용) OTP 코드.\n\n오직 `fake-iamport-server` 에서만 쓰이는 속성으로써, 본인인증을 시뮬레이션할 때,\n어떠한 OTP 코드가 발급되었는 지를 확인하기 위하여 사용된다. 이를 이용하여\n{@link functional.certifications.otp.confirm } 함수를 호출하면, 본인인증을 완료할\n수 있다.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" } }, "nullable": false, "required": [ - "imp_uid", - "merchant_uid", - "name", - "gender", - "birth", - "birthday", - "foreigner", - "phone", - "carrier", - "certified", - "certified_at", - "unique_key", - "unique_in_site", - "pg_tid", "pg_provider", - "origin" + "pg_id", + "card_name", + "card_code", + "card_number", + "card_type", + "customer_name", + "customer_tel", + "customer_email", + "customer_addr", + "customer_postcode", + "inserted", + "updated", + "customer_uid" ], - "description": "본인 인증 내역.\n\n`IIamportCertification` 은 아임포트의 본인인증 정보를 형상화한 자료구조 인터페이스이다.\n\n단, `IIamportCertification` 레코드의 존재가 곧 본인인증의 완결을 뜻하는 것은 아니다.\n{@link IIamportCertification.certified } 값이 `true` 여야만이 비로소, 본인인증\n대상자가 자신의 핸드폰 번호로 전송된 OTP 를 아임포트의 본인인증 팝업창에 정확히 적어,\n본인인증을 완료했음을 의미한다.", + "description": "간편 결제 카드 정보.", "x-typia-jsDocTags": [ { "name": "author", @@ -2016,34 +1968,62 @@ } ] }, - "IIamportCertification.IStore": { + "IIamportSubscription.IStore": { "type": "object", "properties": { - "name": { - "description": "본인인증대상자 성명.", + "card_number": { + "description": "카드 번호.\n\n형식: XXXX-XXXX-XXXX-XXXX", "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "string", + "pattern": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"\\\\d{4}-\\\\d{4}-\\\\d{4}-\\\\d{4}\">", + "kind": "pattern", + "value": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", + "validate": "/\\d{4}-\\d{4}-\\d{4}-\\d{4}/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] }, - "phone": { - "description": "본인인증 대상자 핸드폰 번호.\n\n핸드폰 번호에 \"-\" 값이 들어가던 아니던 상관 없음.\n\n다만, 내부적으로는 \"-\" 값을 제거하여 처리한다.", + "expiry": { + "description": "카드 유효기간.\n\n형식: YYYY-MM", "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "string", + "pattern": "^([0-9]{4})-(0[1-9]|1[012])$", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"^([0-9]{4})-(0[1-9]|1[012])$\">", + "kind": "pattern", + "value": "^([0-9]{4})-(0[1-9]|1[012])$", + "validate": "/^([0-9]{4})-(0[1-9]|1[012])$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] }, "birth": { - "description": "생년월일.\n\nYYYYMMDD 형식.", + "description": "생년월일 YYMMDD 또는 사업자등록번호 10자리.", "x-typia-required": true, "x-typia-optional": false, "type": "string", - "pattern": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", + "pattern": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", "x-typia-typeTags": [ { "target": "string", - "name": "Pattern<\"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$\">", + "name": "Pattern<\"^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\\\d{10})$\">", "kind": "pattern", - "value": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", - "validate": "/^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/.test($input)", + "value": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", + "validate": "/^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$/.test($input)", "exclusive": [ "format", "pattern" @@ -2051,202 +2031,383 @@ } ] }, - "gender_digit": { - "description": "주민등록 뒷부분 첫 자리.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "carrier": { - "description": "본인인증 대상자 통신사 코드.", - "x-typia-required": true, - "x-typia-optional": false, + "pwd_2digit": { + "description": "카드 비밀번호 앞 두 자리.", + "x-typia-required": false, + "x-typia-optional": true, "type": "string", - "enum": [ - "SKT", - "KT", - "LGT" + "pattern": "\\d{2}", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"\\\\d{2}\">", + "kind": "pattern", + "value": "\\d{2}", + "validate": "/\\d{2}/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } ] }, - "is_mvno": { - "description": "알뜰폰 여부.", + "cvc": { + "description": "카드 인증번호 (카드 뒷면 3 자리).", "x-typia-required": false, "x-typia-optional": true, - "type": "boolean" + "type": "string", + "pattern": "\\d{2}", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"\\\\d{2}\">", + "kind": "pattern", + "value": "\\d{2}", + "validate": "/\\d{2}/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] }, - "commpany": { - "description": "가맹점 서비스 명칭 또는 domain URL.\n\nKISA 에서 대상자에게 발송하는 SMS에 안내될 서비스 명칭.", + "customer_name": { "x-typia-required": false, "x-typia-optional": true, "type": "string" }, - "merchant_uid": { - "description": "귀사 서비스에서의 본인인증 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", + "customer_tel": { "x-typia-required": false, "x-typia-optional": true, "type": "string" }, - "pg": { - "description": "PG 사 구분자.\n\n다날 상점아이디를 2개 이상 동시에 사용하시려는 경우에 설정하면 된다.\n\n**danal.{상점아이디}** 형태로 지정.", + "customer_email": { "x-typia-required": false, "x-typia-optional": true, - "type": "string" - } + "type": "string", + "format": "email", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Format<\"email\">", + "kind": "format", + "value": "email", + "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "customr_addr": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "customer_postcode": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "customer_uid": { + "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + } }, "nullable": false, "required": [ - "name", - "phone", + "card_number", + "expiry", "birth", - "gender_digit", - "carrier" + "customer_uid" ], - "description": "본인 인증 입력 정보.", + "description": "간편 결제 카드 입력 정보.", "x-typia-jsDocTags": [] }, - "IIamportResponseIIamportCertification.IAccessor": { + "IIamportSubscription.IOnetime": { "type": "object", "properties": { - "code": { - "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", + "customer_uid": { + "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.\n\n이를 생략시 단순 결제로만 그치며, 카드 정보가 간편 결제용으로 등록되지 아니함.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "card_number": { + "description": "카드 번호.\n\n형식: XXXX-XXXX-XXXX-XXXX", "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string", + "pattern": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"\\\\d{4}-\\\\d{4}-\\\\d{4}-\\\\d{4}\">", + "kind": "pattern", + "value": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", + "validate": "/\\d{4}-\\d{4}-\\d{4}-\\d{4}/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] }, - "message": { - "description": "성공 또는 오류 메시지.", + "expiry": { + "description": "카드 유효기간.\n\n형식: YYYY-MM", "x-typia-required": true, "x-typia-optional": false, - "type": "string" - }, - "response": { - "$ref": "#/components/schemas/IIamportCertification.IAccessor" - } - }, - "nullable": false, - "required": [ - "code", - "message", - "response" - ], - "description": "아임포트 고유의 응답 데이터.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ + "type": "string", + "pattern": "^([0-9]{4})-(0[1-9]|1[012])$", + "x-typia-typeTags": [ { - "text": "Samchon", - "kind": "text" + "target": "string", + "name": "Pattern<\"^([0-9]{4})-(0[1-9]|1[012])$\">", + "kind": "pattern", + "value": "^([0-9]{4})-(0[1-9]|1[012])$", + "validate": "/^([0-9]{4})-(0[1-9]|1[012])$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] } ] - } - ] - }, - "IIamportCertification.IAccessor": { - "type": "object", - "properties": { - "imp_uid": { - "description": "본인인증정보의 식별자 키.", + }, + "birth": { + "description": "생년월일 YYMMDD 또는 사업자등록번호 10자리.", "x-typia-required": true, "x-typia-optional": false, + "type": "string", + "pattern": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\\\d{10})$\">", + "kind": "pattern", + "value": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", + "validate": "/^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "pwd_2digit": { + "description": "카드 비밀번호 앞 두 자리.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "pattern": "\\d{2}", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"\\\\d{2}\">", + "kind": "pattern", + "value": "\\d{2}", + "validate": "/\\d{2}/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "cvc": { + "description": "카드 인증번호 (카드 뒷면 3 자리).", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "pattern": "\\d{2}", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"\\\\d{2}\">", + "kind": "pattern", + "value": "\\d{2}", + "validate": "/\\d{2}/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "customer_name": { + "x-typia-required": false, + "x-typia-optional": true, "type": "string" - } - }, - "nullable": false, - "required": [ - "imp_uid" - ], - "description": "본인인증 정보의 접근자 구조체.", - "x-typia-jsDocTags": [] - }, - "IIamportCertification.IConfirm": { - "type": "object", - "properties": { - "otp": { - "description": "SMS 로 전송된 본인인증 번호.", - "x-typia-required": true, - "x-typia-optional": false, + }, + "customer_tel": { + "x-typia-required": false, + "x-typia-optional": true, "type": "string" - } - }, - "nullable": false, - "required": [ - "otp" - ], - "description": "본인인증 승인을 위한 입력 정보.", - "x-typia-jsDocTags": [] - }, - "IIamportPayment.IWebhook": { - "type": "object", - "properties": { - "imp_uid": { - "description": "결제 정보 {@link IIamportPayment } 의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, + }, + "customer_email": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "format": "email", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Format<\"email\">", + "kind": "format", + "value": "email", + "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "customr_addr": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "customer_postcode": { + "x-typia-required": false, + "x-typia-optional": true, "type": "string" }, "merchant_uid": { - "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", + "description": "주문 식별자 키.\n\n아임포트가 아닌 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "status": { - "description": "현재 상태.", + "amount": { + "description": "결제 총액.", "x-typia-required": true, "x-typia-optional": false, + "type": "number" + }, + "name": { + "description": "주문 이름.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "currency": { + "description": "통화 정보.", + "x-typia-required": false, + "x-typia-optional": true, "type": "string", "enum": [ - "paid", - "ready", - "failed", - "cancelled" + "KRW", + "USD", + "EUR", + "JPY" ] - } - }, - "nullable": false, - "required": [ - "imp_uid", - "merchant_uid", - "status" - ], - "description": "웹훅 데이터.", - "x-typia-jsDocTags": [] - }, - "IIamportPayment.IQuery": { - "type": "object", - "properties": { - "extension": { - "description": "페이팔의 경우, 이 값을 `true` 로 할 것.", + }, + "tax_free": { + "description": "면세 공급가액.\n\n기본값은 0 로써, 알아서 amount 의 1/11 로써 부가세 처리됨.", "x-typia-required": false, "x-typia-optional": true, - "type": "boolean" - } - }, - "nullable": false, - "description": "결제 수단이 페이팔인 경우, 페이팔의 구매자 보호정책에 의해 결제 승인 시점에\nPending 상태를 만든 후, 내부 심사등을 통해 최종 결제 완료라고 변경함.\n\n`iamport` 의 기술적 이슈로 해당 상태를 status: failed 로 기록함. 추후\n페이팔에서 최종결제완료로 변경된 경우, `iamport` 에서 `paid` 로 변경 후,\n해당건에 대한 웹훅 발송. `iamport` 를 사용하는 고객사에서는, failed 로 이미\n처리된 결제건에 대한 paid 상태의 웹훅을 받는 문제점이 생김.\n\n이에, `iamport` 에서 제공하는 `/payment/{imp_uid}` 에 query-string 으로\n`extension=true` 옵션을 추가해야 함", - "x-typia-jsDocTags": [ - { - "name": "issue", - "text": [ + "type": "number" + }, + "card_quota": { + "description": "할부 개월 수.\n\n일시불은 0.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "number" + }, + "buyer_name": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "buyer_email": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "format": "email", + "x-typia-typeTags": [ { - "text": "https://github.com/samchon/fake-iamport-server/issues/13", - "kind": "text" + "target": "string", + "name": "Format<\"email\">", + "kind": "format", + "value": "email", + "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", + "exclusive": [ + "format", + "pattern" + ] } ] }, - { - "name": "author", - "text": [ + "buyer_tel": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "buyer_addr": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "buyer_postcode": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "interest_free_by_merchant": { + "description": "카드할부처리할 때, 할부이자가 발생하는 경우 (카드사 무이자 프로모션 제외).\n\n부과되는 할부이자를 고객대신 가맹점이 지불하고자 PG사와 계약된 경우(현재, 나이스페이먼츠만 지원됨)", + "x-typia-required": false, + "x-typia-optional": true, + "type": "boolean" + }, + "use_card_point": { + "description": "승인요청시 카드사 포인트 차감하며 결제승인처리할지 flag.\n\nPG사 영업담당자와 계약 당시 사전 협의 필요(현재, 나이스페이먼츠만 지원됨)", + "x-typia-required": false, + "x-typia-optional": true, + "type": "boolean" + }, + "custom_data": { + "description": "임의 정보를 기재할 수 있다.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "notice_url": { + "description": "결제 성공시 통지될 Notification, 웹훅 URL.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "format": "url", + "x-typia-typeTags": [ { - "text": "Sangjin Han - https://github.com/ltnscp9028", - "kind": "text" + "target": "string", + "name": "Format<\"url\">", + "kind": "format", + "value": "url", + "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] } ] } - ] + }, + "nullable": false, + "required": [ + "card_number", + "expiry", + "birth", + "merchant_uid", + "amount", + "name" + ], + "description": "결제 신청 입력 정보.", + "x-typia-jsDocTags": [] }, - "IIamportResponseIIamportPayment": { + "IIamportResponseIIamportCardPayment": { "type": "object", "properties": { "code": { @@ -2262,7 +2423,7 @@ "type": "string" }, "response": { - "$ref": "#/components/schemas/IIamportPayment" + "$ref": "#/components/schemas/IIamportCardPayment" } }, "nullable": false, @@ -2284,34 +2445,6 @@ } ] }, - "IIamportPayment": { - "oneOf": [ - { - "$ref": "#/components/schemas/IIamportCardPayment" - }, - { - "$ref": "#/components/schemas/IIamportTransferPayment" - }, - { - "$ref": "#/components/schemas/IIamportVBankPayment" - }, - { - "$ref": "#/components/schemas/IIamportPayment.IBasephonekpaykakaopaypaycolpayssgpaytosspayculturelandsmartculturehappymoneybooknlifepoint" - } - ], - "description": "결제 정보.\n\n`IIamportPayment` 는 아임포트의 결제 정보를 형상화한 자료구조이자 유니언 타입의\n인터페이스로써, if condition 을 통하여 method 값을 특정하면, 파생 타입이 자동으로\n지정된다.\n\n```typescript\nif (payment.pay_method === \"card\")\n payment.card_number; // payment be IIamportCardPayment\n```", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Samchon", - "kind": "text" - } - ] - } - ] - }, "IIamportCardPayment": { "type": "object", "properties": { @@ -2726,331 +2859,658 @@ } ] }, - "IIamportTransferPayment": { + "IIamportSubscription.IAgain": { "type": "object", "properties": { - "bank_code": { - "description": "은행 식별자 코드.", + "merchant_uid": { + "description": "주문 식별자 키.\n\n아임포트가 아닌 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "bank_name": { - "description": "은행 이름.", + "amount": { + "description": "결제 총액.", "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "number" }, - "pay_method": { + "name": { + "description": "주문 이름.", "x-typia-required": true, "x-typia-optional": false, + "type": "string" + }, + "currency": { + "description": "통화 정보.", + "x-typia-required": false, + "x-typia-optional": true, "type": "string", "enum": [ - "trans" + "KRW", + "USD", + "EUR", + "JPY" ] }, - "imp_uid": { - "description": "결제 정보 {@link IIamportPayment } 의 식별자 키.", + "tax_free": { + "description": "면세 공급가액.\n\n기본값은 0 로써, 알아서 amount 의 1/11 로써 부가세 처리됨.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "number" + }, + "card_quota": { + "description": "할부 개월 수.\n\n일시불은 0.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "number" + }, + "buyer_name": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "buyer_email": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "format": "email", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Format<\"email\">", + "kind": "format", + "value": "email", + "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "buyer_tel": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "buyer_addr": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "buyer_postcode": { + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "interest_free_by_merchant": { + "description": "카드할부처리할 때, 할부이자가 발생하는 경우 (카드사 무이자 프로모션 제외).\n\n부과되는 할부이자를 고객대신 가맹점이 지불하고자 PG사와 계약된 경우(현재, 나이스페이먼츠만 지원됨)", + "x-typia-required": false, + "x-typia-optional": true, + "type": "boolean" + }, + "use_card_point": { + "description": "승인요청시 카드사 포인트 차감하며 결제승인처리할지 flag.\n\nPG사 영업담당자와 계약 당시 사전 협의 필요(현재, 나이스페이먼츠만 지원됨)", + "x-typia-required": false, + "x-typia-optional": true, + "type": "boolean" + }, + "custom_data": { + "description": "임의 정보를 기재할 수 있다.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "notice_url": { + "description": "결제 성공시 통지될 Notification, 웹훅 URL.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string", + "format": "url", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Format<\"url\">", + "kind": "format", + "value": "url", + "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "customer_uid": { + "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.", "x-typia-required": true, "x-typia-optional": false, "type": "string" + } + }, + "nullable": false, + "required": [ + "merchant_uid", + "amount", + "name", + "customer_uid" + ], + "description": "간편 결제 카드로 결제 신청 입력 정보.", + "x-typia-jsDocTags": [] + }, + "IIamportResponseIIamportCertification": { + "type": "object", + "properties": { + "code": { + "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" }, - "merchant_uid": { - "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", + "message": { + "description": "성공 또는 오류 메시지.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "name": { - "description": "주문명, 누락 가능.", + "response": { + "$ref": "#/components/schemas/IIamportCertification" + } + }, + "nullable": false, + "required": [ + "code", + "message", + "response" + ], + "description": "아임포트 고유의 응답 데이터.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ] + }, + "IIamportCertification": { + "type": "object", + "properties": { + "imp_uid": { + "description": "아임포트가 발급해 준 식별자 번호.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "merchant_uid": { + "description": "서비스로부터의 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", "x-typia-required": true, "x-typia-optional": false, "type": "string", "nullable": true }, - "amount": { - "description": "결제 총액.", + "name": { + "description": "본인인증대상자 성명.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "gender": { + "description": "성별.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "birth": { + "description": "생년월일.\n\n리눅스 타임이 쓰인다.", "x-typia-required": true, "x-typia-optional": false, "type": "number" }, - "cancel_amount": { - "description": "결제 취소, 환불 총액.", + "birthday": { + "description": "생년월일, YYYYMMDD 형식.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "pattern": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Pattern<\"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$\">", + "kind": "pattern", + "value": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", + "validate": "/^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] + }, + "foreigner": { + "description": "외국인 여부.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "boolean" + }, + "phone": { + "description": "본인인증 대상자 핸드폰 번호.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" - }, - "currency": { - "$ref": "#/components/schemas/IIamportPayment.Currency" + "type": "string" }, - "receipt_url": { - "description": "영수증 URL.", + "carrier": { + "description": "본인인증 대상자 통신사 코드.", "x-typia-required": true, "x-typia-optional": false, "type": "string", - "format": "url", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"url\">", - "kind": "format", - "value": "url", - "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } + "enum": [ + "SKT", + "KT", + "LGT" ] }, - "cash_receipt_issue": { - "description": "현금 영수증 발행 여부.", + "certified": { + "description": "OTP 인증 여부.", "x-typia-required": true, "x-typia-optional": false, "type": "boolean" }, - "channel": { + "certified_at": { + "description": "OTP 인증 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "unique_key": { + "description": "뭔지 잘 모름, 용도 아시는 분?", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "pg_provider": { + "unique_in_site": { + "description": "뭔지 잘 모름, 용도 아시는 분?", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "emb_pg_provider": { + "pg_tid": { + "description": "뭔지 잘 모름, 용도 아시는 분?", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true + "type": "string" }, - "pg_id": { + "pg_provider": { + "description": "PG 제공자.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "pg_tid": { + "origin": { + "description": "뭔지 잘 모름, 용도 아시는 분?", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "escrow": { + "__otp": { + "description": "(테스트 전용) OTP 코드.\n\n오직 `fake-iamport-server` 에서만 쓰이는 속성으로써, 본인인증을 시뮬레이션할 때,\n어떠한 OTP 코드가 발급되었는 지를 확인하기 위하여 사용된다. 이를 이용하여\n{@link functional.certifications.otp.confirm } 함수를 호출하면, 본인인증을 완료할\n수 있다.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + } + }, + "nullable": false, + "required": [ + "imp_uid", + "merchant_uid", + "name", + "gender", + "birth", + "birthday", + "foreigner", + "phone", + "carrier", + "certified", + "certified_at", + "unique_key", + "unique_in_site", + "pg_tid", + "pg_provider", + "origin" + ], + "description": "본인 인증 내역.\n\n`IIamportCertification` 은 아임포트의 본인인증 정보를 형상화한 자료구조 인터페이스이다.\n\n단, `IIamportCertification` 레코드의 존재가 곧 본인인증의 완결을 뜻하는 것은 아니다.\n{@link IIamportCertification.certified } 값이 `true` 여야만이 비로소, 본인인증\n대상자가 자신의 핸드폰 번호로 전송된 OTP 를 아임포트의 본인인증 팝업창에 정확히 적어,\n본인인증을 완료했음을 의미한다.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ] + }, + "IIamportCertification.IStore": { + "type": "object", + "properties": { + "name": { + "description": "본인인증대상자 성명.", "x-typia-required": true, "x-typia-optional": false, - "type": "boolean" + "type": "string" }, - "buyer_name": { + "phone": { + "description": "본인인증 대상자 핸드폰 번호.\n\n핸드폰 번호에 \"-\" 값이 들어가던 아니던 상관 없음.\n\n다만, 내부적으로는 \"-\" 값을 제거하여 처리한다.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true + "type": "string" }, - "buyer_email": { + "birth": { + "description": "생년월일.\n\nYYYYMMDD 형식.", "x-typia-required": true, "x-typia-optional": false, "type": "string", - "format": "email", + "pattern": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", "x-typia-typeTags": [ { "target": "string", - "name": "Format<\"email\">", - "kind": "format", - "value": "email", - "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", + "name": "Pattern<\"^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$\">", + "kind": "pattern", + "value": "^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", + "validate": "/^([0-9]{4})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/.test($input)", "exclusive": [ "format", "pattern" ] } - ], - "nullable": true - }, - "buyer_tel": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true + ] }, - "buyer_addr": { + "gender_digit": { + "description": "주민등록 뒷부분 첫 자리.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true + "type": "string" }, - "buyer_postcode": { + "carrier": { + "description": "본인인증 대상자 통신사 코드.", "x-typia-required": true, "x-typia-optional": false, "type": "string", - "nullable": true + "enum": [ + "SKT", + "KT", + "LGT" + ] }, - "customer_uid": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true + "is_mvno": { + "description": "알뜰폰 여부.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "boolean" }, - "customer_uid_usage": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true + "commpany": { + "description": "가맹점 서비스 명칭 또는 domain URL.\n\nKISA 에서 대상자에게 발송하는 SMS에 안내될 서비스 명칭.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" }, - "custom_data": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true + "merchant_uid": { + "description": "귀사 서비스에서의 본인인증 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" }, - "user_agent": { + "pg": { + "description": "PG 사 구분자.\n\n다날 상점아이디를 2개 이상 동시에 사용하시려는 경우에 설정하면 된다.\n\n**danal.{상점아이디}** 형태로 지정.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + } + }, + "nullable": false, + "required": [ + "name", + "phone", + "birth", + "gender_digit", + "carrier" + ], + "description": "본인 인증 입력 정보.", + "x-typia-jsDocTags": [] + }, + "IIamportResponseIIamportCertification.IAccessor": { + "type": "object", + "properties": { + "code": { + "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true - }, - "status": { - "$ref": "#/components/schemas/IIamportPayment.Status" + "type": "number" }, - "started_at": { - "description": "결제 신청 일시.\n\n리눅스 타임이 쓰임.", + "message": { + "description": "성공 또는 오류 메시지.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, - "paid_at": { - "description": "결제 (지불) 완료 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "response": { + "$ref": "#/components/schemas/IIamportCertification.IAccessor" + } + }, + "nullable": false, + "required": [ + "code", + "message", + "response" + ], + "description": "아임포트 고유의 응답 데이터.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ] + }, + "IIamportCertification.IAccessor": { + "type": "object", + "properties": { + "imp_uid": { + "description": "본인인증정보의 식별자 키.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" - }, - "failed_at": { - "description": "결제 실패 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "type": "string" + } + }, + "nullable": false, + "required": [ + "imp_uid" + ], + "description": "본인인증 정보의 접근자 구조체.", + "x-typia-jsDocTags": [] + }, + "IIamportCertification.IConfirm": { + "type": "object", + "properties": { + "otp": { + "description": "SMS 로 전송된 본인인증 번호.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" - }, - "cancelled_at": { - "description": "결제 취소 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "type": "string" + } + }, + "nullable": false, + "required": [ + "otp" + ], + "description": "본인인증 승인을 위한 입력 정보.", + "x-typia-jsDocTags": [] + }, + "IIamportPayment.IWebhook": { + "type": "object", + "properties": { + "imp_uid": { + "description": "결제 정보 {@link IIamportPayment } 의 식별자 키.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, - "fail_reason": { + "merchant_uid": { + "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true + "type": "string" }, - "cancel_reason": { + "status": { + "description": "현재 상태.", "x-typia-required": true, "x-typia-optional": false, "type": "string", - "nullable": true - }, - "cancel_history": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIamportPaymentCancel" - } + "enum": [ + "paid", + "ready", + "failed", + "cancelled" + ] } }, "nullable": false, "required": [ - "bank_code", - "bank_name", - "pay_method", "imp_uid", "merchant_uid", - "name", - "amount", - "cancel_amount", - "currency", - "receipt_url", - "cash_receipt_issue", - "channel", - "pg_provider", - "emb_pg_provider", - "pg_id", - "pg_tid", - "escrow", - "buyer_name", - "buyer_email", - "buyer_tel", - "buyer_addr", - "buyer_postcode", - "customer_uid", - "customer_uid_usage", - "custom_data", - "user_agent", - "status", - "started_at", - "paid_at", - "failed_at", - "cancelled_at", - "fail_reason", - "cancel_reason", - "cancel_history" + "status" ], - "description": "계좌 이체 결제 정보.", + "description": "웹훅 데이터.", + "x-typia-jsDocTags": [] + }, + "IIamportPayment.IQuery": { + "type": "object", + "properties": { + "extension": { + "description": "페이팔의 경우, 이 값을 `true` 로 할 것.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "boolean" + } + }, + "nullable": false, + "description": "결제 수단이 페이팔인 경우, 페이팔의 구매자 보호정책에 의해 결제 승인 시점에\nPending 상태를 만든 후, 내부 심사등을 통해 최종 결제 완료라고 변경함.\n\n`iamport` 의 기술적 이슈로 해당 상태를 status: failed 로 기록함. 추후\n페이팔에서 최종결제완료로 변경된 경우, `iamport` 에서 `paid` 로 변경 후,\n해당건에 대한 웹훅 발송. `iamport` 를 사용하는 고객사에서는, failed 로 이미\n처리된 결제건에 대한 paid 상태의 웹훅을 받는 문제점이 생김.\n\n이에, `iamport` 에서 제공하는 `/payment/{imp_uid}` 에 query-string 으로\n`extension=true` 옵션을 추가해야 함", "x-typia-jsDocTags": [ + { + "name": "issue", + "text": [ + { + "text": "https://github.com/samchon/fake-iamport-server/issues/13", + "kind": "text" + } + ] + }, { "name": "author", "text": [ { - "text": "Samchon", + "text": "Sangjin Han - https://github.com/ltnscp9028", "kind": "text" } ] } ] }, - "IIamportVBankPayment": { + "IIamportResponseIIamportPayment": { "type": "object", "properties": { - "vbank_code": { - "description": "가상 계좌 식별자 코드.", + "code": { + "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "number" }, - "vbank_name": { - "description": "가상 게좌 이름", + "message": { + "description": "성공 또는 오류 메시지.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "vbank_num": { - "description": "가상 계좌 번호", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" + "response": { + "$ref": "#/components/schemas/IIamportPayment" + } + }, + "nullable": false, + "required": [ + "code", + "message", + "response" + ], + "description": "아임포트 고유의 응답 데이터.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ] + }, + "IIamportPayment": { + "oneOf": [ + { + "$ref": "#/components/schemas/IIamportCardPayment" }, - "vbank_holder": { - "description": "가상 계좌 예금주.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" + { + "$ref": "#/components/schemas/IIamportTransferPayment" }, - "vbank_date": { - "description": "가상 계좌 입금 만료 기한.", + { + "$ref": "#/components/schemas/IIamportVBankPayment" + }, + { + "$ref": "#/components/schemas/IIamportPayment.IBasephonekpaykakaopaypaycolpayssgpaytosspayculturelandsmartculturehappymoneybooknlifepoint" + } + ], + "description": "결제 정보.\n\n`IIamportPayment` 는 아임포트의 결제 정보를 형상화한 자료구조이자 유니언 타입의\n인터페이스로써, if condition 을 통하여 method 값을 특정하면, 파생 타입이 자동으로\n지정된다.\n\n```typescript\nif (payment.pay_method === \"card\")\n payment.card_number; // payment be IIamportCardPayment\n```", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ + { + "text": "Samchon", + "kind": "text" + } + ] + } + ] + }, + "IIamportTransferPayment": { + "type": "object", + "properties": { + "bank_code": { + "description": "은행 식별자 코드.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, - "vbank_issued_at": { - "description": "가상 계좌 개설 일시.", + "bank_name": { + "description": "은행 이름.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, "pay_method": { "x-typia-required": true, "x-typia-optional": false, "type": "string", "enum": [ - "vbank" + "trans" ] }, "imp_uid": { @@ -3262,12 +3722,8 @@ }, "nullable": false, "required": [ - "vbank_code", - "vbank_name", - "vbank_num", - "vbank_holder", - "vbank_date", - "vbank_issued_at", + "bank_code", + "bank_name", "pay_method", "imp_uid", "merchant_uid", @@ -3301,7 +3757,7 @@ "cancel_reason", "cancel_history" ], - "description": "가상 계좌 결제 정보.", + "description": "계좌 이체 결제 정보.", "x-typia-jsDocTags": [ { "name": "author", @@ -3314,26 +3770,51 @@ } ] }, - "IIamportPayment.IBasephonekpaykakaopaypaycolpayssgpaytosspayculturelandsmartculturehappymoneybooknlifepoint": { + "IIamportVBankPayment": { "type": "object", "properties": { - "pay_method": { + "vbank_code": { + "description": "가상 계좌 식별자 코드.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "enum": [ - "phone", - "kpay", - "kakaopay", - "payco", - "lpay", - "ssgpay", - "tosspay", - "cultureland", - "smartculture", - "happymoney", - "booknlife", - "point" + "type": "string" + }, + "vbank_name": { + "description": "가상 게좌 이름", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "vbank_num": { + "description": "가상 계좌 번호", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "vbank_holder": { + "description": "가상 계좌 예금주.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "vbank_date": { + "description": "가상 계좌 입금 만료 기한.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "vbank_issued_at": { + "description": "가상 계좌 개설 일시.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "pay_method": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "enum": [ + "vbank" ] }, "imp_uid": { @@ -3531,277 +4012,60 @@ "cancel_reason": { "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true - }, - "cancel_history": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIamportPaymentCancel" - } - } - }, - "nullable": false, - "required": [ - "pay_method", - "imp_uid", - "merchant_uid", - "name", - "amount", - "cancel_amount", - "currency", - "receipt_url", - "cash_receipt_issue", - "channel", - "pg_provider", - "emb_pg_provider", - "pg_id", - "pg_tid", - "escrow", - "buyer_name", - "buyer_email", - "buyer_tel", - "buyer_addr", - "buyer_postcode", - "customer_uid", - "customer_uid_usage", - "custom_data", - "user_agent", - "status", - "started_at", - "paid_at", - "failed_at", - "cancelled_at", - "fail_reason", - "cancel_reason", - "cancel_history" - ], - "description": "결제 기본 (공통) 정보.", - "x-typia-jsDocTags": [] - }, - "IIamportPaymentCancel.IStore": { - "type": "object", - "properties": { - "imp_uid": { - "description": "결제 정보 {@link IIamportPayment } 의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "merchant_uid": { - "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "amount": { - "description": "취소 금액, 부분 취소도 가능하다.\n\n누락시 전액 취소.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "checksum": { - "description": "취소 트랜잭션 수행 전, 현재 시점의 취소 가능한 잔액.\n\nAPI요청자가 기록하고 있는 취소가능 잔액과 아임포트가 기록하고 있는 취소가능 잔액이\n일치하는지 사전에 검증하고, 검증에 실패하면 트랜잭션을 수행하지 않는다.\n\n`null` 인 경우에는 검증 프로세스를 생략.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number", - "minimum": 0, - "x-typia-typeTags": [ - { - "target": "number", - "name": "Minimum<0>", - "kind": "minimum", - "value": 0, - "validate": "0 <= $input", - "exclusive": [ - "minimum", - "exclusiveMinimum" - ] - } - ], - "nullable": true - }, - "reason": { - "description": "취소 사유.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "tax_free": { - "description": "취소요청금액 중 면세금액.", - "x-typia-jsDocTags": [ - { - "name": "default", - "text": [ - { - "text": "0", - "kind": "text" - } - ] - } - ], - "x-typia-required": false, - "x-typia-optional": true, - "type": "number", - "default": 0 - }, - "refund_holder": { - "description": "환불계좌 예금주.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "refund_bank": { - "description": "환불계좌 은행 코드.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "refund_account": { - "description": "환불계좌 계좌번호.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "refund_tel": { - "description": "환불계좌 예금주 연락처", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - } - }, - "nullable": false, - "required": [ - "imp_uid", - "merchant_uid", - "checksum", - "reason" - ], - "description": "결제 취소 입력 정보.", - "x-typia-jsDocTags": [] - }, - "IIamportResponseIIamportReceipt": { - "type": "object", - "properties": { - "code": { - "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "message": { - "description": "성공 또는 오류 메시지.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "response": { - "$ref": "#/components/schemas/IIamportReceipt" - } - }, - "nullable": false, - "required": [ - "code", - "message", - "response" - ], - "description": "아임포트 고유의 응답 데이터.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Samchon", - "kind": "text" - } - ] - } - ] - }, - "IIamportReceipt": { - "type": "object", - "properties": { - "imp_uid": { - "description": "귀속 결제의 {@link IIamportPayment.imp_uid }.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "receipt_uid": { - "description": "현금 영수증의 고유 식별자 ID.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "apply_num": { - "description": "승인 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/IIamportReceipt.Type" - }, - "amount": { - "description": "결제 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "vat": { - "description": "부가세.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "receipt_url": { - "description": "현금영수증 조회 URL.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "url", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"url\">", - "kind": "format", - "value": "url", - "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "applied_at": { - "description": "현금영수증 발행 시간.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" + "type": "string", + "nullable": true }, - "cancelled_at": { - "description": "현금영수증 취소 시간.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "cancel_history": { "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "array", + "items": { + "$ref": "#/components/schemas/IIamportPaymentCancel" + } } }, "nullable": false, "required": [ + "vbank_code", + "vbank_name", + "vbank_num", + "vbank_holder", + "vbank_date", + "vbank_issued_at", + "pay_method", "imp_uid", - "receipt_uid", - "apply_num", - "type", + "merchant_uid", + "name", "amount", - "vat", + "cancel_amount", + "currency", "receipt_url", - "applied_at", - "cancelled_at" + "cash_receipt_issue", + "channel", + "pg_provider", + "emb_pg_provider", + "pg_id", + "pg_tid", + "escrow", + "buyer_name", + "buyer_email", + "buyer_tel", + "buyer_addr", + "buyer_postcode", + "customer_uid", + "customer_uid_usage", + "custom_data", + "user_agent", + "status", + "started_at", + "paid_at", + "failed_at", + "cancelled_at", + "fail_reason", + "cancel_reason", + "cancel_history" ], - "description": "현금 영수증 정보.", + "description": "가상 계좌 결제 정보.", "x-typia-jsDocTags": [ { "name": "author", @@ -3814,353 +4078,375 @@ } ] }, - "IIamportReceipt.Type": { - "description": "현금영수증 발행 타입 (대상).", - "type": "string", - "enum": [ - "person", - "company" - ] - }, - "IIamportReceipt.IStore": { + "IIamportPayment.IBasephonekpaykakaopaypaycolpayssgpaytosspayculturelandsmartculturehappymoneybooknlifepoint": { "type": "object", "properties": { - "imp_uid": { - "description": "귀속 결제의 {@link IIamportPayment.imp_uid }.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "identifier": { - "description": "현금영수증 발생대상 식별정보.\n\n - 국세청현금영수증카드\n - 휴대폰번호\n - 주민등록번호\n - 사업자등록번호", + "pay_method": { "x-typia-required": true, "x-typia-optional": false, - "type": "string" - }, - "identifier_type": { - "description": "현금영수증 발행대상 유형.\n\n - person: 주민등록번호\n - business: 사업자등록번호\n - phone: 휴대폰번호\n - taxcard: 국세청현금영수증카드\n\n일부 PG 사의 경우 이 항목이 없어 된다는데, 어지간하면 그냥 쓰기 바람.", - "x-typia-required": false, - "x-typia-optional": true, "type": "string", "enum": [ "phone", - "person", - "business", - "taxcard" - ] - }, - "type": { - "description": "현금영수증 발행 타입 (대상).\n\n누락시 person 이 사용됨.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "enum": [ - "person", - "company" + "kpay", + "kakaopay", + "payco", + "lpay", + "ssgpay", + "tosspay", + "cultureland", + "smartculture", + "happymoney", + "booknlife", + "point" ] }, - "buyer_name": { - "description": "구매자 이름.\n\n형금영수증 발행건 사후 추적을 위해 가급 입력하기 바람.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "buyer_email": { - "description": "구매자 이메일.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "buyer_tel": { - "description": "구매자 전화번호.\n\n현금영수증 발행건 사후 추적을 위해 가급 입력하기 바람.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "tax_free": { - "description": "면세 금액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - } - }, - "nullable": false, - "required": [ - "imp_uid", - "identifier" - ], - "description": "현금영수증 입력 정보.", - "x-typia-jsDocTags": [] - }, - "IIamportUser.IAccessor": { - "type": "object", - "properties": { - "imp_key": { - "description": "API 키.", + "imp_uid": { + "description": "결제 정보 {@link IIamportPayment } 의 식별자 키.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "imp_secret": { - "description": "Secret 키.", + "merchant_uid": { + "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", "x-typia-required": true, "x-typia-optional": false, "type": "string" - } - }, - "nullable": false, - "required": [ - "imp_key", - "imp_secret" - ], - "description": "아임포트에서 부여해 준 API 및 secret 키.", - "x-typia-jsDocTags": [] - }, - "IIamportResponseIIamportUser": { - "type": "object", - "properties": { - "code": { - "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" }, - "message": { - "description": "성공 또는 오류 메시지.", + "name": { + "description": "주문명, 누락 가능.", "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "string", + "nullable": true }, - "response": { - "$ref": "#/components/schemas/IIamportUser" - } - }, - "nullable": false, - "required": [ - "code", - "message", - "response" - ], - "description": "아임포트 고유의 응답 데이터.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Samchon", - "kind": "text" - } - ] - } - ] - }, - "IIamportUser": { - "type": "object", - "properties": { - "now": { - "description": "토큰 발행 시간.", + "amount": { + "description": "결제 총액.", "x-typia-required": true, "x-typia-optional": false, "type": "number" }, - "expired_at": { - "description": "토큰 만료 시간.\n\n리눅스 타임이 기준이며, 이를 JS 에서 사용하려거든, 아래와 같이 변환해야 한다.\n\n```typescript\nnew Date(user.expired_at * 1_000);\n```", + "cancel_amount": { + "description": "결제 취소, 환불 총액.", "x-typia-required": true, "x-typia-optional": false, "type": "number" }, - "access_token": { - "description": "유저 인증 토큰.", + "currency": { + "$ref": "#/components/schemas/IIamportPayment.Currency" + }, + "receipt_url": { + "description": "영수증 URL.", "x-typia-required": true, "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "now", - "expired_at", - "access_token" - ], - "description": "아임포트 유저 인증 정보.\n\n아임포트는 고객사에게 API 및 secret 키 정보, {@link IIamportUser.IAccessor } 를 발급해준다.\n\n하지만 이를 곧장 아임포트의 유저 인증에 사용할 수는 없고, 해당 API 및 secret 키를 토대로 유저\n인증 토큰을 발급받아야 하는데, 이 유저 인증 토큰에는 하필이면 만로 시간이라는 게 존재한다.\n`IIamportUser` 는 바로 이러한 유저 인증 토큰 및 그것의 만료 시간을 형상화한 자료구조\n인터페이스이다.\n\n더하여 이처럼 만료 시간이 존재하는 아임포트의 유저 인증 토큰의 특성상, 이것의 만료 시간이\n초과되지 않도록 관리하는 것은 매우 힘든 일이다. 이에 `iamport-server-api` 에서는 아임포트\n유저 인증 토큰이 만료될 때마다 자동 갱신해주는, {@link IamportConnector } 클래스를 제공한다.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ + "type": "string", + "format": "url", + "x-typia-typeTags": [ { - "text": "Samchon", - "kind": "text" + "target": "string", + "name": "Format<\"url\">", + "kind": "format", + "value": "url", + "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] } ] - } - ] - }, - "IIamportVBankPayment.IStore": { - "type": "object", - "properties": { - "merchant_uid": { - "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", + }, + "cash_receipt_issue": { + "description": "현금 영수증 발행 여부.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "boolean" + }, + "channel": { "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "amount": { - "description": "총액.", + "pg_provider": { "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, - "vbank_code": { - "description": "가상계좌 은행 코드.", + "emb_pg_provider": { "x-typia-required": true, "x-typia-optional": false, - "type": "string" + "type": "string", + "nullable": true }, - "vbank_due": { - "description": "가상계좌 입금기한, 유닉스 타임.", + "pg_id": { "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, - "vbank_holder": { - "description": "예금주.", + "pg_tid": { "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "name": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "escrow": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "boolean" }, "buyer_name": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, "buyer_email": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "format": "email", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Format<\"email\">", + "kind": "format", + "value": "email", + "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ], + "nullable": true }, "buyer_tel": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, "buyer_addr": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, "buyer_postcode": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, - "pg": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "customer_uid": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, - "notice_url": { - "description": "가상 계좌 입금 정보를 수신할 URL.\n\n누락시 기본 웹훅 URL 사용.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "customer_uid_usage": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, "custom_data": { - "description": "커스텀 데이터, 자유롭게 사용 가능.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true }, - "pg_api_key": { - "description": "[이니시스 전용] 가맹점 콘솔에서 확인한 API 값.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "user_agent": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true + }, + "status": { + "$ref": "#/components/schemas/IIamportPayment.Status" + }, + "started_at": { + "description": "결제 신청 일시.\n\n리눅스 타임이 쓰임.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "paid_at": { + "description": "결제 (지불) 완료 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "failed_at": { + "description": "결제 실패 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "cancelled_at": { + "description": "결제 취소 일시.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" + }, + "fail_reason": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true + }, + "cancel_reason": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "string", + "nullable": true + }, + "cancel_history": { + "x-typia-required": true, + "x-typia-optional": false, + "type": "array", + "items": { + "$ref": "#/components/schemas/IIamportPaymentCancel" + } } }, "nullable": false, "required": [ + "pay_method", + "imp_uid", "merchant_uid", + "name", "amount", - "vbank_code", - "vbank_due", - "vbank_holder" + "cancel_amount", + "currency", + "receipt_url", + "cash_receipt_issue", + "channel", + "pg_provider", + "emb_pg_provider", + "pg_id", + "pg_tid", + "escrow", + "buyer_name", + "buyer_email", + "buyer_tel", + "buyer_addr", + "buyer_postcode", + "customer_uid", + "customer_uid_usage", + "custom_data", + "user_agent", + "status", + "started_at", + "paid_at", + "failed_at", + "cancelled_at", + "fail_reason", + "cancel_reason", + "cancel_history" ], - "description": "가상 계좌 결제 입력 정보.\n\n가상 계좌를 임의 생성할 수 있다.\n\n단, 일부 PG 사 혹은 `fake-iamport-server` 만 가능.\n\n - 세틀뱅크\n - 나이스페이먼츠\n - KG이니시스", + "description": "결제 기본 (공통) 정보.", "x-typia-jsDocTags": [] }, - "IIamportResponseIIamportVBankPayment": { + "IIamportPaymentCancel.IStore": { "type": "object", "properties": { - "code": { - "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", + "imp_uid": { + "description": "결제 정보 {@link IIamportPayment } 의 식별자 키.", "x-typia-required": true, "x-typia-optional": false, - "type": "number" + "type": "string" }, - "message": { - "description": "성공 또는 오류 메시지.", + "merchant_uid": { + "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "response": { - "$ref": "#/components/schemas/IIamportVBankPayment" - } - }, - "nullable": false, - "required": [ - "code", - "message", - "response" - ], - "description": "아임포트 고유의 응답 데이터.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ + "amount": { + "description": "취소 금액, 부분 취소도 가능하다.\n\n누락시 전액 취소.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "number" + }, + "checksum": { + "description": "취소 트랜잭션 수행 전, 현재 시점의 취소 가능한 잔액.\n\nAPI요청자가 기록하고 있는 취소가능 잔액과 아임포트가 기록하고 있는 취소가능 잔액이\n일치하는지 사전에 검증하고, 검증에 실패하면 트랜잭션을 수행하지 않는다.\n\n`null` 인 경우에는 검증 프로세스를 생략.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number", + "minimum": 0, + "x-typia-typeTags": [ { - "text": "Samchon", - "kind": "text" - } - ] - } - ] - }, - "IIamportVBankPayment.IUpdate": { - "type": "object", - "properties": { - "imp_uid": { - "description": "대상 결제 기록의 {@link IIamportPayment.imp_uid }.", + "target": "number", + "name": "Minimum<0>", + "kind": "minimum", + "value": 0, + "validate": "0 <= $input", + "exclusive": [ + "minimum", + "exclusiveMinimum" + ] + } + ], + "nullable": true + }, + "reason": { + "description": "취소 사유.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "amount": { - "description": "수정할 결제 금액.", + "tax_free": { + "description": "취소요청금액 중 면세금액.", + "x-typia-jsDocTags": [ + { + "name": "default", + "text": [ + { + "text": "0", + "kind": "text" + } + ] + } + ], "x-typia-required": false, "x-typia-optional": true, - "type": "number" + "type": "number", + "default": 0 }, - "vbank_due": { - "description": "수정할 가상계좌 입금 기한.", + "refund_holder": { + "description": "환불계좌 예금주.", "x-typia-required": false, "x-typia-optional": true, - "type": "number" + "type": "string" + }, + "refund_bank": { + "description": "환불계좌 은행 코드.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "refund_account": { + "description": "환불계좌 계좌번호.", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" + }, + "refund_tel": { + "description": "환불계좌 예금주 연락처", + "x-typia-required": false, + "x-typia-optional": true, + "type": "string" } }, "nullable": false, "required": [ - "imp_uid" + "imp_uid", + "merchant_uid", + "checksum", + "reason" ], - "description": "가상 계좌 결제의 수정 입력 정보.\n\n아직 입금되지 않은 가상계좌의 입금기한 또는 입금금액을 수정할 수 있다.\n\n다만, 세틀뱅크 혹은 `fake-iamport-server` 만 가능.", + "description": "결제 취소 입력 정보.", "x-typia-jsDocTags": [] }, - "IIamportResponseIIamportSubscription": { + "IIamportResponseIIamportReceipt": { "type": "object", "properties": { "code": { @@ -4176,7 +4462,7 @@ "type": "string" }, "response": { - "$ref": "#/components/schemas/IIamportSubscription" + "$ref": "#/components/schemas/IIamportReceipt" } }, "nullable": false, @@ -4198,104 +4484,88 @@ } ] }, - "IIamportSubscription": { + "IIamportReceipt": { "type": "object", "properties": { - "pg_provider": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "pg_id": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "card_name": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "card_code": { + "imp_uid": { + "description": "귀속 결제의 {@link IIamportPayment.imp_uid }.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "card_number": { + "receipt_uid": { + "description": "현금 영수증의 고유 식별자 ID.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "card_type": { + "apply_num": { + "description": "승인 번호.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "customer_name": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true - }, - "customer_tel": { - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "nullable": true + "type": { + "$ref": "#/components/schemas/IIamportReceipt.Type" }, - "customer_email": { + "amount": { + "description": "결제 총액.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true + "type": "number" }, - "customer_addr": { + "vat": { + "description": "부가세.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "nullable": true + "type": "number" }, - "customer_postcode": { + "receipt_url": { + "description": "현금영수증 조회 URL.", "x-typia-required": true, "x-typia-optional": false, "type": "string", - "nullable": true + "format": "url", + "x-typia-typeTags": [ + { + "target": "string", + "name": "Format<\"url\">", + "kind": "format", + "value": "url", + "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", + "exclusive": [ + "format", + "pattern" + ] + } + ] }, - "inserted": { + "applied_at": { + "description": "현금영수증 발행 시간.", "x-typia-required": true, "x-typia-optional": false, "type": "number" }, - "updated": { + "cancelled_at": { + "description": "현금영수증 취소 시간.\n\n리눅스 타임이 쓰이며, `null` 대신 0 을 씀.", "x-typia-required": true, "x-typia-optional": false, "type": "number" - }, - "customer_uid": { - "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" } }, "nullable": false, "required": [ - "pg_provider", - "pg_id", - "card_name", - "card_code", - "card_number", - "card_type", - "customer_name", - "customer_tel", - "customer_email", - "customer_addr", - "customer_postcode", - "inserted", - "updated", - "customer_uid" + "imp_uid", + "receipt_uid", + "apply_num", + "type", + "amount", + "vat", + "receipt_url", + "applied_at", + "cancelled_at" ], - "description": "간편 결제 카드 정보.", + "description": "현금 영수증 정보.", "x-typia-jsDocTags": [ { "name": "author", @@ -4308,150 +4578,95 @@ } ] }, - "IIamportSubscription.IStore": { + "IIamportReceipt.Type": { + "description": "현금영수증 발행 타입 (대상).", + "type": "string", + "enum": [ + "person", + "company" + ] + }, + "IIamportReceipt.IStore": { "type": "object", "properties": { - "card_number": { - "description": "카드 번호.\n\n형식: XXXX-XXXX-XXXX-XXXX", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"\\\\d{4}-\\\\d{4}-\\\\d{4}-\\\\d{4}\">", - "kind": "pattern", - "value": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", - "validate": "/\\d{4}-\\d{4}-\\d{4}-\\d{4}/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "expiry": { - "description": "카드 유효기간.\n\n형식: YYYY-MM", + "imp_uid": { + "description": "귀속 결제의 {@link IIamportPayment.imp_uid }.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "pattern": "^([0-9]{4})-(0[1-9]|1[012])$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^([0-9]{4})-(0[1-9]|1[012])$\">", - "kind": "pattern", - "value": "^([0-9]{4})-(0[1-9]|1[012])$", - "validate": "/^([0-9]{4})-(0[1-9]|1[012])$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "string" }, - "birth": { - "description": "생년월일 YYMMDD 또는 사업자등록번호 10자리.", + "identifier": { + "description": "현금영수증 발생대상 식별정보.\n\n - 국세청현금영수증카드\n - 휴대폰번호\n - 주민등록번호\n - 사업자등록번호", "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\\\d{10})$\">", - "kind": "pattern", - "value": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", - "validate": "/^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "pwd_2digit": { - "description": "카드 비밀번호 앞 두 자리.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "pattern": "\\d{2}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"\\\\d{2}\">", - "kind": "pattern", - "value": "\\d{2}", - "validate": "/\\d{2}/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "x-typia-optional": false, + "type": "string" }, - "cvc": { - "description": "카드 인증번호 (카드 뒷면 3 자리).", + "identifier_type": { + "description": "현금영수증 발행대상 유형.\n\n - person: 주민등록번호\n - business: 사업자등록번호\n - phone: 휴대폰번호\n - taxcard: 국세청현금영수증카드\n\n일부 PG 사의 경우 이 항목이 없어 된다는데, 어지간하면 그냥 쓰기 바람.", "x-typia-required": false, "x-typia-optional": true, "type": "string", - "pattern": "\\d{2}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"\\\\d{2}\">", - "kind": "pattern", - "value": "\\d{2}", - "validate": "/\\d{2}/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } + "enum": [ + "phone", + "person", + "business", + "taxcard" ] }, - "customer_name": { + "type": { + "description": "현금영수증 발행 타입 (대상).\n\n누락시 person 이 사용됨.", "x-typia-required": false, "x-typia-optional": true, - "type": "string" + "type": "string", + "enum": [ + "person", + "company" + ] }, - "customer_tel": { + "buyer_name": { + "description": "구매자 이름.\n\n형금영수증 발행건 사후 추적을 위해 가급 입력하기 바람.", "x-typia-required": false, "x-typia-optional": true, "type": "string" }, - "customer_email": { + "buyer_email": { + "description": "구매자 이메일.", "x-typia-required": false, "x-typia-optional": true, - "type": "string", - "format": "email", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"email\">", - "kind": "format", - "value": "email", - "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "string" }, - "customr_addr": { + "buyer_tel": { + "description": "구매자 전화번호.\n\n현금영수증 발행건 사후 추적을 위해 가급 입력하기 바람.", "x-typia-required": false, "x-typia-optional": true, "type": "string" }, - "customer_postcode": { + "tax_free": { + "description": "면세 금액.", "x-typia-required": false, "x-typia-optional": true, + "type": "number" + } + }, + "nullable": false, + "required": [ + "imp_uid", + "identifier" + ], + "description": "현금영수증 입력 정보.", + "x-typia-jsDocTags": [] + }, + "IIamportUser.IAccessor": { + "type": "object", + "properties": { + "imp_key": { + "description": "API 키.", + "x-typia-required": true, + "x-typia-optional": false, "type": "string" }, - "customer_uid": { - "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.", + "imp_secret": { + "description": "Secret 키.", "x-typia-required": true, "x-typia-optional": false, "type": "string" @@ -4459,203 +4674,128 @@ }, "nullable": false, "required": [ - "card_number", - "expiry", - "birth", - "customer_uid" + "imp_key", + "imp_secret" ], - "description": "간편 결제 카드 입력 정보.", + "description": "아임포트에서 부여해 준 API 및 secret 키.", "x-typia-jsDocTags": [] }, - "IIamportSubscription.IOnetime": { + "IIamportResponseIIamportUser": { "type": "object", "properties": { - "customer_uid": { - "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.\n\n이를 생략시 단순 결제로만 그치며, 카드 정보가 간편 결제용으로 등록되지 아니함.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "card_number": { - "description": "카드 번호.\n\n형식: XXXX-XXXX-XXXX-XXXX", + "code": { + "description": "에러 코드.\n\n값이 0 이면 오류가 없다는 뜻.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "pattern": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"\\\\d{4}-\\\\d{4}-\\\\d{4}-\\\\d{4}\">", - "kind": "pattern", - "value": "\\d{4}-\\d{4}-\\d{4}-\\d{4}", - "validate": "/\\d{4}-\\d{4}-\\d{4}-\\d{4}/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "number" }, - "expiry": { - "description": "카드 유효기간.\n\n형식: YYYY-MM", + "message": { + "description": "성공 또는 오류 메시지.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "pattern": "^([0-9]{4})-(0[1-9]|1[012])$", - "x-typia-typeTags": [ + "type": "string" + }, + "response": { + "$ref": "#/components/schemas/IIamportUser" + } + }, + "nullable": false, + "required": [ + "code", + "message", + "response" + ], + "description": "아임포트 고유의 응답 데이터.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ { - "target": "string", - "name": "Pattern<\"^([0-9]{4})-(0[1-9]|1[012])$\">", - "kind": "pattern", - "value": "^([0-9]{4})-(0[1-9]|1[012])$", - "validate": "/^([0-9]{4})-(0[1-9]|1[012])$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] + "text": "Samchon", + "kind": "text" } ] - }, - "birth": { - "description": "생년월일 YYMMDD 또는 사업자등록번호 10자리.", + } + ] + }, + "IIamportUser": { + "type": "object", + "properties": { + "now": { + "description": "토큰 발행 시간.", "x-typia-required": true, "x-typia-optional": false, - "type": "string", - "pattern": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\\\d{10})$\">", - "kind": "pattern", - "value": "^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$", - "validate": "/^(([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01]))|(\\d{10})$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "pwd_2digit": { - "description": "카드 비밀번호 앞 두 자리.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "pattern": "\\d{2}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"\\\\d{2}\">", - "kind": "pattern", - "value": "\\d{2}", - "validate": "/\\d{2}/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "cvc": { - "description": "카드 인증번호 (카드 뒷면 3 자리).", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "pattern": "\\d{2}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"\\\\d{2}\">", - "kind": "pattern", - "value": "\\d{2}", - "validate": "/\\d{2}/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "number" }, - "customer_name": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" + "expired_at": { + "description": "토큰 만료 시간.\n\n리눅스 타임이 기준이며, 이를 JS 에서 사용하려거든, 아래와 같이 변환해야 한다.\n\n```typescript\nnew Date(user.expired_at * 1_000);\n```", + "x-typia-required": true, + "x-typia-optional": false, + "type": "number" }, - "customer_tel": { - "x-typia-required": false, - "x-typia-optional": true, + "access_token": { + "description": "유저 인증 토큰.", + "x-typia-required": true, + "x-typia-optional": false, "type": "string" - }, - "customer_email": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "format": "email", - "x-typia-typeTags": [ + } + }, + "nullable": false, + "required": [ + "now", + "expired_at", + "access_token" + ], + "description": "아임포트 유저 인증 정보.\n\n아임포트는 고객사에게 API 및 secret 키 정보, {@link IIamportUser.IAccessor } 를 발급해준다.\n\n하지만 이를 곧장 아임포트의 유저 인증에 사용할 수는 없고, 해당 API 및 secret 키를 토대로 유저\n인증 토큰을 발급받아야 하는데, 이 유저 인증 토큰에는 하필이면 만로 시간이라는 게 존재한다.\n`IIamportUser` 는 바로 이러한 유저 인증 토큰 및 그것의 만료 시간을 형상화한 자료구조\n인터페이스이다.\n\n더하여 이처럼 만료 시간이 존재하는 아임포트의 유저 인증 토큰의 특성상, 이것의 만료 시간이\n초과되지 않도록 관리하는 것은 매우 힘든 일이다. 이에 `iamport-server-api` 에서는 아임포트\n유저 인증 토큰이 만료될 때마다 자동 갱신해주는, {@link IamportConnector } 클래스를 제공한다.", + "x-typia-jsDocTags": [ + { + "name": "author", + "text": [ { - "target": "string", - "name": "Format<\"email\">", - "kind": "format", - "value": "email", - "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", - "exclusive": [ - "format", - "pattern" - ] + "text": "Samchon", + "kind": "text" } ] - }, - "customr_addr": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "customer_postcode": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, + } + ] + }, + "IIamportVBankPayment.IStore": { + "type": "object", + "properties": { "merchant_uid": { - "description": "주문 식별자 키.\n\n아임포트가 아닌 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", + "description": "주문 식별자 키.\n\n아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, "amount": { - "description": "결제 총액.", + "description": "총액.", "x-typia-required": true, "x-typia-optional": false, "type": "number" }, - "name": { - "description": "주문 이름.", + "vbank_code": { + "description": "가상계좌 은행 코드.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, - "currency": { - "description": "통화 정보.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "enum": [ - "KRW", - "USD", - "EUR", - "JPY" - ] - }, - "tax_free": { - "description": "면세 공급가액.\n\n기본값은 0 로써, 알아서 amount 의 1/11 로써 부가세 처리됨.", - "x-typia-required": false, - "x-typia-optional": true, + "vbank_due": { + "description": "가상계좌 입금기한, 유닉스 타임.", + "x-typia-required": true, + "x-typia-optional": false, "type": "number" }, - "card_quota": { - "description": "할부 개월 수.\n\n일시불은 0.", + "vbank_holder": { + "description": "예금주.", + "x-typia-required": true, + "x-typia-optional": false, + "type": "string" + }, + "name": { "x-typia-required": false, "x-typia-optional": true, - "type": "number" + "type": "string" }, "buyer_name": { "x-typia-required": false, @@ -4665,21 +4805,7 @@ "buyer_email": { "x-typia-required": false, "x-typia-optional": true, - "type": "string", - "format": "email", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"email\">", - "kind": "format", - "value": "email", - "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "string" }, "buyer_tel": { "x-typia-required": false, @@ -4696,58 +4822,42 @@ "x-typia-optional": true, "type": "string" }, - "interest_free_by_merchant": { - "description": "카드할부처리할 때, 할부이자가 발생하는 경우 (카드사 무이자 프로모션 제외).\n\n부과되는 할부이자를 고객대신 가맹점이 지불하고자 PG사와 계약된 경우(현재, 나이스페이먼츠만 지원됨)", + "pg": { "x-typia-required": false, "x-typia-optional": true, - "type": "boolean" + "type": "string" }, - "use_card_point": { - "description": "승인요청시 카드사 포인트 차감하며 결제승인처리할지 flag.\n\nPG사 영업담당자와 계약 당시 사전 협의 필요(현재, 나이스페이먼츠만 지원됨)", + "notice_url": { + "description": "가상 계좌 입금 정보를 수신할 URL.\n\n누락시 기본 웹훅 URL 사용.", "x-typia-required": false, "x-typia-optional": true, - "type": "boolean" + "type": "string" }, "custom_data": { - "description": "임의 정보를 기재할 수 있다.", + "description": "커스텀 데이터, 자유롭게 사용 가능.", "x-typia-required": false, "x-typia-optional": true, "type": "string" }, - "notice_url": { - "description": "결제 성공시 통지될 Notification, 웹훅 URL.", + "pg_api_key": { + "description": "[이니시스 전용] 가맹점 콘솔에서 확인한 API 값.", "x-typia-required": false, "x-typia-optional": true, - "type": "string", - "format": "url", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"url\">", - "kind": "format", - "value": "url", - "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] + "type": "string" } }, "nullable": false, "required": [ - "card_number", - "expiry", - "birth", "merchant_uid", "amount", - "name" + "vbank_code", + "vbank_due", + "vbank_holder" ], - "description": "결제 신청 입력 정보.", + "description": "가상 계좌 결제 입력 정보.\n\n가상 계좌를 임의 생성할 수 있다.\n\n단, 일부 PG 사 혹은 `fake-iamport-server` 만 가능.\n\n - 세틀뱅크\n - 나이스페이먼츠\n - KG이니시스", "x-typia-jsDocTags": [] }, - "IIamportResponseIIamportCardPayment": { + "IIamportResponseIIamportVBankPayment": { "type": "object", "properties": { "code": { @@ -4763,7 +4873,7 @@ "type": "string" }, "response": { - "$ref": "#/components/schemas/IIamportCardPayment" + "$ref": "#/components/schemas/IIamportVBankPayment" } }, "nullable": false, @@ -4785,143 +4895,33 @@ } ] }, - "IIamportSubscription.IAgain": { + "IIamportVBankPayment.IUpdate": { "type": "object", "properties": { - "merchant_uid": { - "description": "주문 식별자 키.\n\n아임포트가 아닌 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", + "imp_uid": { + "description": "대상 결제 기록의 {@link IIamportPayment.imp_uid }.", "x-typia-required": true, "x-typia-optional": false, "type": "string" }, "amount": { - "description": "결제 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "name": { - "description": "주문 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "currency": { - "description": "통화 정보.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "enum": [ - "KRW", - "USD", - "EUR", - "JPY" - ] - }, - "tax_free": { - "description": "면세 공급가액.\n\n기본값은 0 로써, 알아서 amount 의 1/11 로써 부가세 처리됨.", + "description": "수정할 결제 금액.", "x-typia-required": false, "x-typia-optional": true, "type": "number" }, - "card_quota": { - "description": "할부 개월 수.\n\n일시불은 0.", + "vbank_due": { + "description": "수정할 가상계좌 입금 기한.", "x-typia-required": false, "x-typia-optional": true, "type": "number" - }, - "buyer_name": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "buyer_email": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "format": "email", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"email\">", - "kind": "format", - "value": "email", - "validate": "/^(([^<>()[\\]\\.,;:\\s@\\\"]+(\\.[^<>()[\\]\\.,;:\\s@\\\"]+)*)|(\\\".+\\\"))@(([^<>()[\\]\\.,;:\\s@\\\"]+\\.)+[^<>()[\\]\\.,;:\\s@\\\"]{2,})$/i.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "buyer_tel": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "buyer_addr": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "buyer_postcode": { - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "interest_free_by_merchant": { - "description": "카드할부처리할 때, 할부이자가 발생하는 경우 (카드사 무이자 프로모션 제외).\n\n부과되는 할부이자를 고객대신 가맹점이 지불하고자 PG사와 계약된 경우(현재, 나이스페이먼츠만 지원됨)", - "x-typia-required": false, - "x-typia-optional": true, - "type": "boolean" - }, - "use_card_point": { - "description": "승인요청시 카드사 포인트 차감하며 결제승인처리할지 flag.\n\nPG사 영업담당자와 계약 당시 사전 협의 필요(현재, 나이스페이먼츠만 지원됨)", - "x-typia-required": false, - "x-typia-optional": true, - "type": "boolean" - }, - "custom_data": { - "description": "임의 정보를 기재할 수 있다.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "notice_url": { - "description": "결제 성공시 통지될 Notification, 웹훅 URL.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "format": "url", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"url\">", - "kind": "format", - "value": "url", - "validate": "/^[a-zA-Z0-9]+:\\/\\/(?:www.)?[-a-zA-Z0-9@:%._+~#=]{1,256}.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test($input)", - "exclusive": [ - "format", - "pattern" - ] - } - ] - }, - "customer_uid": { - "description": "고객 식별자 키.\n\n아임포트가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.\n\n다만 고객이라기보다 실제로는 카드의 식별자 키로 써야함.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" } }, "nullable": false, "required": [ - "merchant_uid", - "amount", - "name", - "customer_uid" + "imp_uid" ], - "description": "간편 결제 카드로 결제 신청 입력 정보.", + "description": "가상 계좌 결제의 수정 입력 정보.\n\n아직 입금되지 않은 가상계좌의 입금기한 또는 입금금액을 수정할 수 있다.\n\n다만, 세틀뱅크 혹은 `fake-iamport-server` 만 가능.", "x-typia-jsDocTags": [] } }, diff --git a/packages/payment-api/package.json b/packages/payment-api/package.json index 34c7c36..40818fa 100644 --- a/packages/payment-api/package.json +++ b/packages/payment-api/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/payment-api", - "version": "5.0.6", + "version": "5.1.0", "description": "API for Payment Backend", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -34,9 +34,9 @@ "typescript": "^5.2.2" }, "dependencies": { - "@nestia/fetcher": "^2.3.4", - "iamport-server-api": "^5.0.6", - "toss-payments-server-api": "^5.0.6", - "typia": "^5.2.4" + "@nestia/fetcher": "^2.3.9", + "iamport-server-api": "^5.1.0", + "toss-payments-server-api": "^5.1.0", + "typia": "^5.2.6" } } \ No newline at end of file diff --git a/packages/payment-api/swagger.json b/packages/payment-api/swagger.json index d0c9592..82a5857 100644 --- a/packages/payment-api/swagger.json +++ b/packages/payment-api/swagger.json @@ -7,7 +7,7 @@ } ], "info": { - "version": "5.0.6", + "version": "5.1.0", "title": "@samchon/payment-backend", "description": "Payment Backend Server", "license": { diff --git a/packages/payment-backend/docs/ERD.md b/packages/payment-backend/docs/ERD.md index b059dd5..c01ea8c 100644 --- a/packages/payment-backend/docs/ERD.md +++ b/packages/payment-backend/docs/ERD.md @@ -60,9 +60,9 @@ erDiagram String body "nullable" DateTime created_at } -"payment_history_cancels" }|--|| "payment_histories" : history -"payment_history_webhooks" }|--|| "payment_histories" : history -"payment_history_webhook_responses" }|--|| "payment_history_webhooks" : webhook +"payment_history_cancels" }o--|| "payment_histories" : history +"payment_history_webhooks" }o--|| "payment_histories" : history +"payment_history_webhook_responses" }o--|| "payment_history_webhooks" : webhook ``` ### `payment_reservations` diff --git a/packages/payment-backend/nestia.config.ts b/packages/payment-backend/nestia.config.ts index 7b32be0..863ceb1 100644 --- a/packages/payment-backend/nestia.config.ts +++ b/packages/payment-backend/nestia.config.ts @@ -4,12 +4,12 @@ import { NestFactory } from "@nestjs/core"; import { PaymentModule } from "./src/PaymentModule"; const NESTIA_CONFIG: INestiaConfig = { - simulate: true, - input: async () => NestFactory.create(await PaymentModule()), - output: "src/api", - distribute: "../payment-api", - swagger: { - output: "../payment-api/swagger.json", - }, + simulate: true, + input: () => NestFactory.create(PaymentModule), + output: "src/api", + distribute: "../payment-api", + swagger: { + output: "../payment-api/swagger.json", + }, }; export default NESTIA_CONFIG; diff --git a/packages/payment-backend/package.json b/packages/payment-backend/package.json index 23e2b71..84b8e61 100644 --- a/packages/payment-backend/package.json +++ b/packages/payment-backend/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/payment-backend", - "version": "5.0.6", + "version": "5.1.0", "description": "Payment Backend Server", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -17,7 +17,6 @@ "eslint": "eslint src && eslint test", "eslint:fix": "eslint src --fix && eslint test --fix", "prepare": "ts-patch install && npm run build:prisma", - "prettier": "prettier src --write && prettier test --write", "-----------------------------------------------": "", "reset-for-debugging": "npm run test -- --reset true --include __nothing__", "test": "node bin/test", @@ -50,8 +49,7 @@ }, "homepage": "https://github.com/samchon/payments", "devDependencies": { - "@nestia/sdk": "^2.3.4", - "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@nestia/sdk": "^2.3.9", "@types/atob": "^2.1.2", "@types/bcryptjs": "^2.4.4", "@types/btoa": "^1.2.3", @@ -64,28 +62,36 @@ "@typescript-eslint/eslint-plugin": "^5.26.0", "@typescript-eslint/parser": "^5.26.0", "cli": "^1.0.1", + "copy-webpack-plugin": "^11.0.0", "copyfiles": "^2.4.1", - "iamport-server-api": "^5.0.6", - "nestia": "^5.0.1", + "iamport-server-api": "^5.1.0", + "nestia": "^5.0.3", "pm2": "^4.5.6", - "prettier": "^2.6.2", - "prisma-markdown": "^1.0.6", + "prisma-markdown": "^1.0.7", "rimraf": "^3.0.2", "sloc": "^0.2.1", - "toss-payments-server-api": "^5.0.6", + "toss-payments-server-api": "^5.1.0", + "ts-loader": "^9.5.0", "ts-node": "^10.9.1", "ts-patch": "^3.0.2", "typescript": "^5.2.2", - "typescript-transform-paths": "^3.4.6" + "typescript-transform-paths": "^3.4.6", + "webpack": "^5.89.0", + "webpack-cli": "^5.1.4", + "write-file-webpack-plugin": "^4.5.1" }, "dependencies": { - "@nestia/core": "^2.3.4", + "@nestia/core": "^2.3.9", + "@nestjs/common": "^10.2.8", + "@nestjs/core": "^10.2.8", + "@nestjs/platform-fastify": "^10.2.8", "@prisma/client": "^5.3.1", "bcryptjs": "^2.4.3", "dotenv": "^16.3.1", "dotenv-expand": "^10.0.0", - "fake-iamport-server": "^5.0.6", - "fake-toss-payments-server": "^5.0.6", + "fake-iamport-server": "^5.1.0", + "fake-toss-payments-server": "^5.1.0", + "fastify": "^4.24.3", "git-last-commit": "^1.0.0", "inquirer": "^8.2.5", "mutex-server": "^0.3.1", @@ -93,7 +99,7 @@ "serialize-error": "^4.1.0", "source-map-support": "^0.5.19", "tstl": "^2.5.13", - "typia": "^5.2.4" + "typia": "^5.2.6" }, "files": [ "lib", diff --git a/packages/payment-backend/src/PaymentAsset.ts b/packages/payment-backend/src/PaymentAsset.ts index 823f584..eb6adbf 100644 --- a/packages/payment-backend/src/PaymentAsset.ts +++ b/packages/payment-backend/src/PaymentAsset.ts @@ -5,15 +5,13 @@ import { IamportAsset } from "./services/iamport/IamportAsset"; import { TossAsset } from "./services/toss/TossAsset"; export namespace PaymentAsset { - export function toss_connection( - storeId: string, - ): Promise { - return TossAsset.connection(storeId); - } + export function toss_connection(storeId: string): Promise { + return TossAsset.connection(storeId); + } - export function iamport_connection( - storeId: string, - ): Promise { - return IamportAsset.connection(storeId); - } + export function iamport_connection( + storeId: string, + ): Promise { + return IamportAsset.connection(storeId); + } } diff --git a/packages/payment-backend/src/PaymentBackend.ts b/packages/payment-backend/src/PaymentBackend.ts index 5a3aa7c..4c4d78c 100644 --- a/packages/payment-backend/src/PaymentBackend.ts +++ b/packages/payment-backend/src/PaymentBackend.ts @@ -1,5 +1,6 @@ -import nest from "@modules/nestjs"; +import { INestApplication } from "@nestjs/common"; import { NestFactory } from "@nestjs/core"; +import { FastifyAdapter } from "@nestjs/platform-fastify"; import express from "express"; import FakeIamport from "fake-iamport-server"; import FakeToss from "fake-toss-payments-server"; @@ -15,102 +16,106 @@ import PaymentAPI from "./api"; * @author Samchon */ export class PaymentBackend { - private application_?: nest.INestApplication; - private is_closing_: boolean = false; - - private fake_servers_: IFakeServer[] = []; - - /** - * 서버 개설하기. - * - * 통합 결제 백엔드 서버를 개설한다. 이 때 개설되는 서버의 종류는 - * {@link PaymentGlobal.mode} 를 따르며, 만일 개설되는 서버가 테스트 자동화 프로그램에 - * 의하여 시작된 것이라면 ({@link PaymentGlobal.testing} 값이 true), 이와 연동하게 될 - * 가짜 PG 결제사 서버들도 함께 개설한다. - */ - public async open(): Promise { - //---- - // OPEN THE BACKEND SERVER - //---- - // MOUNT CONTROLLERS - this.application_ = await NestFactory.create(await PaymentModule(), { - logger: false, - }); - - // CONFIGURATIONS - this.is_closing_ = false; - this.application_.enableCors(); - this.application_.use(this.middleware.bind(this)); - - // DO OPEN - await this.application_.listen(PaymentConfiguration.API_PORT()); - - // CONFIGURE FAKE SERVERS IF TESTING - if (PaymentGlobal.testing === true) { - // OPEN FAKE SERVERS - this.fake_servers_ = [ - new FakeIamport.FakeIamportBackend(), - new FakeToss.FakeTossBackend(), - ]; - for (const server of this.fake_servers_) { - await server.open(); - } - - // CONFIGURE WEBHOOK URLS - const host: string = `http://127.0.0.1:${PaymentConfiguration.API_PORT()}`; - FakeIamport.FakeIamportConfiguration.WEBHOOK_URL = `${host}${PaymentAPI.functional.payments.webhooks.iamport.METADATA.path}`; - FakeToss.FakeTossConfiguration.WEBHOOK_URL = `${host}${PaymentAPI.functional.payments.webhooks.toss.METADATA.path}`; - } - - //---- - // POST-PROCESSES - //---- - // INFORM TO THE PM2 - if (process.send) process.send("ready"); - - // WHEN KILL COMMAND COMES - process.on("SIGINT", async () => { - this.is_closing_ = true; - await this.close(); - process.exit(0); - }); + private application_?: INestApplication; + private is_closing_: boolean = false; + + private fake_servers_: IFakeServer[] = []; + + /** + * 서버 개설하기. + * + * 통합 결제 백엔드 서버를 개설한다. 이 때 개설되는 서버의 종류는 + * {@link PaymentGlobal.mode} 를 따르며, 만일 개설되는 서버가 테스트 자동화 프로그램에 + * 의하여 시작된 것이라면 ({@link PaymentGlobal.testing} 값이 true), 이와 연동하게 될 + * 가짜 PG 결제사 서버들도 함께 개설한다. + */ + public async open(): Promise { + //---- + // OPEN THE BACKEND SERVER + //---- + // MOUNT CONTROLLERS + this.application_ = await NestFactory.create( + PaymentModule, + new FastifyAdapter(), + { + logger: false, + }, + ); + + // CONFIGURATIONS + this.is_closing_ = false; + this.application_.enableCors(); + this.application_.use(this.middleware.bind(this)); + + // DO OPEN + await this.application_.listen(PaymentConfiguration.API_PORT()); + + // CONFIGURE FAKE SERVERS IF TESTING + if (PaymentGlobal.testing === true) { + // OPEN FAKE SERVERS + this.fake_servers_ = [ + new FakeIamport.FakeIamportBackend(), + new FakeToss.FakeTossBackend(), + ]; + for (const server of this.fake_servers_) { + await server.open(); + } + + // CONFIGURE WEBHOOK URLS + const host: string = `http://127.0.0.1:${PaymentConfiguration.API_PORT()}`; + FakeIamport.FakeIamportConfiguration.WEBHOOK_URL = `${host}${PaymentAPI.functional.payments.webhooks.iamport.METADATA.path}`; + FakeToss.FakeTossConfiguration.WEBHOOK_URL = `${host}${PaymentAPI.functional.payments.webhooks.toss.METADATA.path}`; } - /** - * 개설한 서버 닫기. - */ - public async close(): Promise { - if (this.application_ === undefined) return; - - // DO CLOSE - await this.application_.close(); - delete this.application_; - - // EXIT FROM THE CRITICAL-SERVER - if ((await PaymentGlobal.critical.is_loaded()) === true) { - const critical = await PaymentGlobal.critical.get(); - await critical.close(); - } - - // CLOSE FAKE SERVERS - for (const server of this.fake_servers_) { - await server.close(); - } - this.fake_servers_ = []; + //---- + // POST-PROCESSES + //---- + // INFORM TO THE PM2 + if (process.send) process.send("ready"); + + // WHEN KILL COMMAND COMES + process.on("SIGINT", async () => { + this.is_closing_ = true; + await this.close(); + process.exit(0); + }); + } + + /** + * 개설한 서버 닫기. + */ + public async close(): Promise { + if (this.application_ === undefined) return; + + // DO CLOSE + await this.application_.close(); + delete this.application_; + + // EXIT FROM THE CRITICAL-SERVER + if ((await PaymentGlobal.critical.is_loaded()) === true) { + const critical = await PaymentGlobal.critical.get(); + await critical.close(); } - private middleware( - _request: express.Request, - response: express.Response, - next: FunctionLike, - ): void { - if (this.is_closing_ === true) response.set("Connection", "close"); - next(); + // CLOSE FAKE SERVERS + for (const server of this.fake_servers_) { + await server.close(); } + this.fake_servers_ = []; + } + + private middleware( + _request: express.Request, + response: express.Response, + next: FunctionLike, + ): void { + if (this.is_closing_ === true) response.set("Connection", "close"); + next(); + } } interface IFakeServer { - open(): Promise; - close(): Promise; + open(): Promise; + close(): Promise; } type FunctionLike = (...args: any) => any; diff --git a/packages/payment-backend/src/PaymentConfiguration.ts b/packages/payment-backend/src/PaymentConfiguration.ts index 7baf565..3bed00b 100644 --- a/packages/payment-backend/src/PaymentConfiguration.ts +++ b/packages/payment-backend/src/PaymentConfiguration.ts @@ -1,6 +1,10 @@ -import nest from "@modules/nestjs"; import { ExceptionManager } from "@nestia/core"; import { IEncryptionPassword } from "@nestia/fetcher"; +import { + ConflictException, + InternalServerErrorException, + NotFoundException, +} from "@nestjs/common"; import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; @@ -15,92 +19,92 @@ if (EXTENSION === "js") require("source-map-support").install(); * @author Samchon */ export namespace PaymentConfiguration { - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- CONNECTIONS ----------------------------------------------------------- */ - export const API_PORT = () => Number(PaymentGlobal.env.PAYMENT_API_PORT); - export const UPDATOR_PORT = () => - Number(PaymentGlobal.env.PAYMENT_UPDATOR_PORT); - export const MASTER_IP = () => - PaymentGlobal.mode === "local" - ? "127.0.0.1" - : PaymentGlobal.mode === "dev" - ? "your-dev-server-ip" - : "your-real-server-master-ip"; + export const API_PORT = () => Number(PaymentGlobal.env.PAYMENT_API_PORT); + export const UPDATOR_PORT = () => + Number(PaymentGlobal.env.PAYMENT_UPDATOR_PORT); + export const MASTER_IP = () => + PaymentGlobal.mode === "local" + ? "127.0.0.1" + : PaymentGlobal.mode === "dev" + ? "your-dev-server-ip" + : "your-real-server-master-ip"; - export const ENCRYPTION_PASSWORD = (): Readonly => ({ - key: PaymentGlobal.env.PAYMENT_CONNECTION_ENCRYPTION_KEY ?? "", - iv: PaymentGlobal.env.PAYMENT_CONNECTION_ENCRYPTION_IV ?? "", - }); - export const SYSTEM_PASSWORD = () => - PaymentGlobal.env.PAYMENT_SYSTEM_PASSWORD; + export const ENCRYPTION_PASSWORD = (): Readonly => ({ + key: PaymentGlobal.env.PAYMENT_CONNECTION_ENCRYPTION_KEY ?? "", + iv: PaymentGlobal.env.PAYMENT_CONNECTION_ENCRYPTION_IV ?? "", + }); + export const SYSTEM_PASSWORD = () => + PaymentGlobal.env.PAYMENT_SYSTEM_PASSWORD; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- VENDORS ----------------------------------------------------------- */ - /** - * 아임포트의 API 및 시크릿 키 getter 함수. - * - * `IAMPORT_USER_ACCESSOR` 는 아임포트에 사용할 API 및 시크릿 키를 리턴해주는 getter - * 함수로써, 귀하는 이 함수를 수정하여, 아임포트로부터 발급받은 API 및 시크릿 키를 - * 리턴하도록 한다. - * - * 만일 귀사의 서비스가 아임포트로부터 복수의 스토어 ID 를 발급받았다면, 이 또한 - * 고려하여 리턴되는 API 및 시크릿 키 값을 정해주도록 한다. 또한, - * {@link PaymentGlobal.testing 테스트 용도} 내지 {@link PaymentGlobal.mode 개발 서버} - * 전용으로 발급받은 스토어 ID 또한 존재한다면, 이 또한 고려토록 한다. - * - * @param storeId 스토어 ID - * @returns 아임포트의 API 및 시크릿 키 - */ - export const IAMPORT_USER_ACCESSOR = ( - storeId: string, - ): IIamportUser.IAccessor => { - storeId; - return { - imp_key: PaymentGlobal.env.PAYMENT_IAMPORT_KEY, - imp_secret: PaymentGlobal.env.PAYMENT_IAMPORT_SECRET, - }; + /** + * 아임포트의 API 및 시크릿 키 getter 함수. + * + * `IAMPORT_USER_ACCESSOR` 는 아임포트에 사용할 API 및 시크릿 키를 리턴해주는 getter + * 함수로써, 귀하는 이 함수를 수정하여, 아임포트로부터 발급받은 API 및 시크릿 키를 + * 리턴하도록 한다. + * + * 만일 귀사의 서비스가 아임포트로부터 복수의 스토어 ID 를 발급받았다면, 이 또한 + * 고려하여 리턴되는 API 및 시크릿 키 값을 정해주도록 한다. 또한, + * {@link PaymentGlobal.testing 테스트 용도} 내지 {@link PaymentGlobal.mode 개발 서버} + * 전용으로 발급받은 스토어 ID 또한 존재한다면, 이 또한 고려토록 한다. + * + * @param storeId 스토어 ID + * @returns 아임포트의 API 및 시크릿 키 + */ + export const IAMPORT_USER_ACCESSOR = ( + storeId: string, + ): IIamportUser.IAccessor => { + storeId; + return { + imp_key: PaymentGlobal.env.PAYMENT_IAMPORT_KEY, + imp_secret: PaymentGlobal.env.PAYMENT_IAMPORT_SECRET, }; + }; - /** - * 토스 페이먼츠의 시크릿 키 getter 함수. - * - * `TOSS_SECRET_KEY` 는 토스 페이먼츠에 사용할 시크릿 키를 리턴해주는 getter 함수로써, - * 귀하는 이 함수를 수정하여, 토스 페이먼츠로부터 발급받은 시크릿 키를 리턴하도록 한다. - * - * 만일 귀사의 서비스가 토스 페이먼츠로부터 복수의 스토어 ID 를 발급받았다면, 이 또한 - * 고려하여 리턴되는 시크릿 키 값을 정해주도록 한다. 또한, - * {@link PaymentGlobal.testing 테스트 용도} 내지 {@link PaymentGlobal.mode 개발 서버} - * 전용으로 발급받은 스토어 ID 또한 존재한다면, 이 또한 고려토록 한다. - * - * @param storeId 스토어 ID - * @returns 토스 페이먼츠의 시크릿 키 - */ - export const TOSS_SECRET_KEY = (storeId: string): string => { - storeId; - return PaymentGlobal.env.PAYMENT_TOSS_PAYMENTS_SECRET; - }; + /** + * 토스 페이먼츠의 시크릿 키 getter 함수. + * + * `TOSS_SECRET_KEY` 는 토스 페이먼츠에 사용할 시크릿 키를 리턴해주는 getter 함수로써, + * 귀하는 이 함수를 수정하여, 토스 페이먼츠로부터 발급받은 시크릿 키를 리턴하도록 한다. + * + * 만일 귀사의 서비스가 토스 페이먼츠로부터 복수의 스토어 ID 를 발급받았다면, 이 또한 + * 고려하여 리턴되는 시크릿 키 값을 정해주도록 한다. 또한, + * {@link PaymentGlobal.testing 테스트 용도} 내지 {@link PaymentGlobal.mode 개발 서버} + * 전용으로 발급받은 스토어 ID 또한 존재한다면, 이 또한 고려토록 한다. + * + * @param storeId 스토어 ID + * @returns 토스 페이먼츠의 시크릿 키 + */ + export const TOSS_SECRET_KEY = (storeId: string): string => { + storeId; + return PaymentGlobal.env.PAYMENT_TOSS_PAYMENTS_SECRET; + }; - /** - * @internal - */ - export const ROOT = __dirname.split("\\").join("/") + "/.."; + /** + * @internal + */ + export const ROOT = __dirname.split("\\").join("/") + "/.."; - /** - * @internal - */ - export const CREATED_AT: Date = new Date(); + /** + * @internal + */ + export const CREATED_AT: Date = new Date(); } // CUSTOM EXCEPTIION CONVERSION ExceptionManager.insert(PrismaClientKnownRequestError, (exp) => { - switch (exp.code) { - case "P2025": - return new nest.NotFoundException(exp.message); - case "P2002": // UNIQUE CONSTRAINT - return new nest.ConflictException(exp.message); - default: - return new nest.InternalServerErrorException(exp.message); - } + switch (exp.code) { + case "P2025": + return new NotFoundException(exp.message); + case "P2002": // UNIQUE CONSTRAINT + return new ConflictException(exp.message); + default: + return new InternalServerErrorException(exp.message); + } }); diff --git a/packages/payment-backend/src/PaymentGlobal.ts b/packages/payment-backend/src/PaymentGlobal.ts index 3edc80c..a52feaf 100644 --- a/packages/payment-backend/src/PaymentGlobal.ts +++ b/packages/payment-backend/src/PaymentGlobal.ts @@ -13,116 +13,116 @@ import { PaymentConfiguration } from "./PaymentConfiguration"; * @author Samchon */ export class PaymentGlobal { - /** - * 백엔드 서버 실행 모드. - * - * 현재의 `payments-server` 가 어느 환경에서 실행되고 있는가. - * - * - local: 로컬 시스템 - * - dev: 개발 서버 - * - real: 실제 서버 - */ - public static get mode(): "local" | "dev" | "real" { - return (modeWrapper.value ??= environments.get().PAYMENT_MODE); - } + /** + * 백엔드 서버 실행 모드. + * + * 현재의 `payments-server` 가 어느 환경에서 실행되고 있는가. + * + * - local: 로컬 시스템 + * - dev: 개발 서버 + * - real: 실제 서버 + */ + public static get mode(): "local" | "dev" | "real" { + return (modeWrapper.value ??= environments.get().PAYMENT_MODE); + } - public static setMode(mode: typeof PaymentGlobal.mode): void { - typia.assert(mode); - modeWrapper.value = mode; - } + public static setMode(mode: typeof PaymentGlobal.mode): void { + typia.assert(mode); + modeWrapper.value = mode; + } - public static get env() { - return environments.get(); - } + public static get env() { + return environments.get(); + } - public static get prisma(): PrismaClient { - return prismaClient.get(); - } + public static get prisma(): PrismaClient { + return prismaClient.get(); + } - public static readonly critical: MutableSingleton< - MutexConnector - > = new MutableSingleton(async () => { - const connector: MutexConnector = new MutexConnector( - PaymentConfiguration.SYSTEM_PASSWORD(), - null, - ); - await connector.connect( - `ws://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.UPDATOR_PORT()}/api`, - ); - return connector; - }); + public static readonly critical: MutableSingleton< + MutexConnector + > = new MutableSingleton(async () => { + const connector: MutexConnector = new MutexConnector( + PaymentConfiguration.SYSTEM_PASSWORD(), + null, + ); + await connector.connect( + `ws://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.UPDATOR_PORT()}/api`, + ); + return connector; + }); } export namespace PaymentGlobal { - /** - * 테스트 여부. - * - * 현 `payments-server` 가 테스트 자동화 프로그램에서 구동 중인지 여부. - * - * 만일 이 값이 true 이거든, 통합 결제 백엔드 서버를 구동할 때, 이와 연동하게 될 - * 각종 가짜 결제 PG 서버들도 함께 구동된다. 현재 본 `payments-server` 의 테스트 - * 자동화 프로그램과 연동되는 가짜 PG 서버들의 목록은 아래와 같다. - * - * - [fake-iamport-server](https://github.com/samchon/fake-iamport-server) - * - [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) - * - * 더하여 현 서버가 로컬 시스템에서 구동된다 하여 반드시 테스트 중이라는 보장은 - * 없으며, 반대로 현 서버가 테스트 중이라 하여 반드시 로컬 시스템이라는 보장 또한 - * 없으니, 이 점을 착각하지 말기 바란다. - */ - export let testing: boolean = false; + /** + * 테스트 여부. + * + * 현 `payments-server` 가 테스트 자동화 프로그램에서 구동 중인지 여부. + * + * 만일 이 값이 true 이거든, 통합 결제 백엔드 서버를 구동할 때, 이와 연동하게 될 + * 각종 가짜 결제 PG 서버들도 함께 구동된다. 현재 본 `payments-server` 의 테스트 + * 자동화 프로그램과 연동되는 가짜 PG 서버들의 목록은 아래와 같다. + * + * - [fake-iamport-server](https://github.com/samchon/fake-iamport-server) + * - [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) + * + * 더하여 현 서버가 로컬 시스템에서 구동된다 하여 반드시 테스트 중이라는 보장은 + * 없으며, 반대로 현 서버가 테스트 중이라 하여 반드시 로컬 시스템이라는 보장 또한 + * 없으니, 이 점을 착각하지 말기 바란다. + */ + export let testing: boolean = false; } interface IMode { - value?: "local" | "dev" | "real"; + value?: "local" | "dev" | "real"; } const modeWrapper: IMode = {}; interface IEnvironments { - // DEFAULT CONFIGURATIONS - PAYMENT_MODE: "local" | "dev" | "real"; - PAYMENT_API_PORT: `${number}`; - PAYMENT_UPDATOR_PORT: `${number}`; - PAYMENT_SYSTEM_PASSWORD: string; + // DEFAULT CONFIGURATIONS + PAYMENT_MODE: "local" | "dev" | "real"; + PAYMENT_API_PORT: `${number}`; + PAYMENT_UPDATOR_PORT: `${number}`; + PAYMENT_SYSTEM_PASSWORD: string; - // POSTGRES CONNECTION INFO - PAYMENT_POSTGRES_HOST: string; - PAYMENT_POSTGRES_PORT: `${number}`; - PAYMENT_POSTGRES_DATABASE: string; - PAYMENT_POSTGRES_SCHEMA: string; - PAYMENT_POSTGRES_USERNAME: string; - PAYMENT_POSTGRES_USERNAME_READONLY: string; - PAYMENT_POSTGRES_PASSWORD: string; - PAYMENT_POSTGRES_URL: string; + // POSTGRES CONNECTION INFO + PAYMENT_POSTGRES_HOST: string; + PAYMENT_POSTGRES_PORT: `${number}`; + PAYMENT_POSTGRES_DATABASE: string; + PAYMENT_POSTGRES_SCHEMA: string; + PAYMENT_POSTGRES_USERNAME: string; + PAYMENT_POSTGRES_USERNAME_READONLY: string; + PAYMENT_POSTGRES_PASSWORD: string; + PAYMENT_POSTGRES_URL: string; - // ENCRYPTION KEYS - PAYMENT_CONNECTION_ENCRYPTION_KEY: string; - PAYMENT_CONNECTION_ENCRYPTION_IV: string; - PAYMENT_HISTORY_ENCRYPTION_KEY: string; - PAYMENT_HISTORY_ENCRYPTION_IV: string; - PAYMENT_RESERVATION_ENCRYPTION_KEY: string; - PAYMENT_RESERVATION_ENCRYPTION_IV: string; - PAYMENT_CANCEL_HISTORY_ENCRYPTION_KEY: string; - PAYMENT_CANCEL_HISTORY_ENCRYPTION_IV: string; + // ENCRYPTION KEYS + PAYMENT_CONNECTION_ENCRYPTION_KEY: string; + PAYMENT_CONNECTION_ENCRYPTION_IV: string; + PAYMENT_HISTORY_ENCRYPTION_KEY: string; + PAYMENT_HISTORY_ENCRYPTION_IV: string; + PAYMENT_RESERVATION_ENCRYPTION_KEY: string; + PAYMENT_RESERVATION_ENCRYPTION_IV: string; + PAYMENT_CANCEL_HISTORY_ENCRYPTION_KEY: string; + PAYMENT_CANCEL_HISTORY_ENCRYPTION_IV: string; - // VENDOR'S SECRETS - PAYMENT_IAMPORT_KEY: string; - PAYMENT_IAMPORT_SECRET: string; - PAYMENT_TOSS_PAYMENTS_SECRET: string; + // VENDOR'S SECRETS + PAYMENT_IAMPORT_KEY: string; + PAYMENT_IAMPORT_SECRET: string; + PAYMENT_TOSS_PAYMENTS_SECRET: string; } const environments: Singleton = new Singleton(() => { - const env = dotenv.config(); - dotenvExpand.expand(env); - return typia.assert(process.env); + const env = dotenv.config(); + dotenvExpand.expand(env); + return typia.assert(process.env); }); const prismaClient = new Singleton( - () => - new PrismaClient({ - datasources: { - db: { - url: PaymentGlobal.env.PAYMENT_POSTGRES_URL, - }, - }, - }), + () => + new PrismaClient({ + datasources: { + db: { + url: PaymentGlobal.env.PAYMENT_POSTGRES_URL, + }, + }, + }), ); diff --git a/packages/payment-backend/src/PaymentModule.ts b/packages/payment-backend/src/PaymentModule.ts index ecacbcf..9aa4204 100644 --- a/packages/payment-backend/src/PaymentModule.ts +++ b/packages/payment-backend/src/PaymentModule.ts @@ -1,9 +1,27 @@ import core from "@nestia/core"; import { PaymentConfiguration } from "./PaymentConfiguration"; +import { MonitorHealthController } from "./controllers/monitors/MonitorHealthController"; +import { MonitorPerformanceController } from "./controllers/monitors/MonitorPerformanceController"; +import { MonitorSystemController } from "./controllers/monitors/MonitorSystemController"; +import { PaymentHistoriesController } from "./controllers/payments/PaymentHistoriesController"; +import { PaymentInternalController } from "./controllers/payments/PaymentInternalController"; +import { PaymentReservationsController } from "./controllers/payments/PaymentReservationsController"; +import { PaymentWebhooksController } from "./controllers/payments/PaymentWebhooksController"; -export const PaymentModule = () => - core.EncryptedModule.dynamic( - __dirname + "/controllers", - PaymentConfiguration.ENCRYPTION_PASSWORD(), - ); +@core.EncryptedModule( + { + controllers: [ + MonitorHealthController, + MonitorPerformanceController, + MonitorSystemController, + + PaymentHistoriesController, + PaymentInternalController, + PaymentReservationsController, + PaymentWebhooksController, + ], + }, + PaymentConfiguration.ENCRYPTION_PASSWORD(), +) +export class PaymentModule {} diff --git a/packages/payment-backend/src/PaymentSetupWizard.ts b/packages/payment-backend/src/PaymentSetupWizard.ts index ace9ad6..794cdf2 100644 --- a/packages/payment-backend/src/PaymentSetupWizard.ts +++ b/packages/payment-backend/src/PaymentSetupWizard.ts @@ -3,24 +3,24 @@ import cp from "child_process"; import { PaymentGlobal } from "./PaymentGlobal"; export namespace PaymentSetupWizard { - export async function schema(): Promise { - if (PaymentGlobal.testing === false) - throw new Error( - "Erron on PaymentSetupWizard.schema(): unable to reset database in non-test mode.", - ); + export async function schema(): Promise { + if (PaymentGlobal.testing === false) + throw new Error( + "Erron on PaymentSetupWizard.schema(): unable to reset database in non-test mode.", + ); - const execute = (type: string) => (argv: string) => - cp.execSync( - `npx prisma migrate ${type} --schema=src/schema.prisma ${argv}`, - { stdio: "ignore" }, - ); - execute("reset")("--force"); - execute("dev")("--name init"); + const execute = (type: string) => (argv: string) => + cp.execSync( + `npx prisma migrate ${type} --schema=src/schema.prisma ${argv}`, + { stdio: "ignore" }, + ); + execute("reset")("--force"); + execute("dev")("--name init"); - await PaymentGlobal.prisma.$executeRawUnsafe( - `GRANT SELECT ON ALL TABLES IN SCHEMA ${PaymentGlobal.env.PAYMENT_POSTGRES_SCHEMA} TO ${PaymentGlobal.env.PAYMENT_POSTGRES_USERNAME_READONLY}`, - ); - } + await PaymentGlobal.prisma.$executeRawUnsafe( + `GRANT SELECT ON ALL TABLES IN SCHEMA ${PaymentGlobal.env.PAYMENT_POSTGRES_SCHEMA} TO ${PaymentGlobal.env.PAYMENT_POSTGRES_USERNAME_READONLY}`, + ); + } - export async function seed(): Promise {} + export async function seed(): Promise {} } diff --git a/packages/payment-backend/src/PaymentUpdator.ts b/packages/payment-backend/src/PaymentUpdator.ts index b58c05d..af2aab6 100644 --- a/packages/payment-backend/src/PaymentUpdator.ts +++ b/packages/payment-backend/src/PaymentUpdator.ts @@ -10,96 +10,85 @@ import { Terminal } from "./utils/Terminal"; * @author Samchon */ export namespace PaymentUpdator { - export interface IController { - update(): Promise; - } + export interface IController { + update(): Promise; + } - /** - * 마스터 인스턴스에서의 업데이트 리스터 실행. - * - * @returns 뮤텍스 서버 인스턴스 - */ - export async function master(): Promise< - MutexServer - > { - // PREPARE ASSETS - const server: MutexServer = - new MutexServer(); - const clientSet: HashSet> = new HashSet(); - const provider: IController = { - update: async () => { - const clientList: MutexAcceptor[] = [...clientSet]; - const tasks: Promise[] = clientList.map( - async (client) => { - try { - await client.getDriver().update(); - } catch {} - }, - ); - await Promise.all(tasks); - }, - }; + /** + * 마스터 인스턴스에서의 업데이트 리스터 실행. + * + * @returns 뮤텍스 서버 인스턴스 + */ + export async function master(): Promise< + MutexServer + > { + // PREPARE ASSETS + const server: MutexServer = new MutexServer(); + const clientSet: HashSet> = new HashSet(); + const provider: IController = { + update: async () => { + const clientList: MutexAcceptor[] = [...clientSet]; + const tasks: Promise[] = clientList.map(async (client) => { + try { + await client.getDriver().update(); + } catch {} + }); + await Promise.all(tasks); + }, + }; - // OPEN SERVER - await server.open( - PaymentConfiguration.UPDATOR_PORT(), - async (acceptor) => { - if ( - acceptor.header !== PaymentConfiguration.SYSTEM_PASSWORD() - ) { - await acceptor.reject(); - return; - } else if (acceptor.path === "/slave") { - await acceptor.accept(null); + // OPEN SERVER + await server.open(PaymentConfiguration.UPDATOR_PORT(), async (acceptor) => { + if (acceptor.header !== PaymentConfiguration.SYSTEM_PASSWORD()) { + await acceptor.reject(); + return; + } else if (acceptor.path === "/slave") { + await acceptor.accept(null); - clientSet.insert(acceptor); - acceptor - .join() - .then(() => clientSet.erase(acceptor)) - .catch(() => {}); - } else if (acceptor.path === "/api") - await acceptor.accept(null); - else if (acceptor.path === "/update") - await acceptor.accept(provider); - }, - ); - return server; - } + clientSet.insert(acceptor); + acceptor + .join() + .then(() => clientSet.erase(acceptor)) + .catch(() => {}); + } else if (acceptor.path === "/api") await acceptor.accept(null); + else if (acceptor.path === "/update") await acceptor.accept(provider); + }); + return server; + } - /** - * 슬레이브 인스턴스에서의 업데이트 리스터 실행. - * - * @param 업데이트 리스너 마스터 서버의 host 주소, 생략시 기본값 사용 - * @returns 뮤텍스 커넥터 인스턴스 - */ - export async function slave( - host?: string, - ): Promise> { - const connector: MutexConnector = - new MutexConnector( - PaymentConfiguration.SYSTEM_PASSWORD(), - Controller, - ); - await connector.connect( - `ws://${ - host ?? PaymentConfiguration.MASTER_IP() - }:${PaymentConfiguration.UPDATOR_PORT()}/slave`, - ); - return connector; - } + /** + * 슬레이브 인스턴스에서의 업데이트 리스터 실행. + * + * @param 업데이트 리스너 마스터 서버의 host 주소, 생략시 기본값 사용 + * @returns 뮤텍스 커넥터 인스턴스 + */ + export async function slave( + host?: string, + ): Promise> { + const connector: MutexConnector = new MutexConnector( + PaymentConfiguration.SYSTEM_PASSWORD(), + Controller, + ); + await connector.connect( + `ws://${ + host ?? PaymentConfiguration.MASTER_IP() + }:${PaymentConfiguration.UPDATOR_PORT()}/slave`, + ); + return connector; + } - /** - * @internal - */ - namespace Controller { - export async function update(): Promise { - // REFRESH REPOSITORY - await Terminal.execute("git pull"); - await Terminal.execute("npm install"); - await Terminal.execute("npm run build"); + /** + * @internal + */ + namespace Controller { + export async function update(): Promise { + // REFRESH REPOSITORY + await Terminal.execute("git pull"); + await Terminal.execute("npm install"); + await Terminal.execute("npm run build"); - // RELOAD PM2 - await Terminal.execute("npm run start:reload"); - } + // RELOAD PM2 + await Terminal.execute("npm run start:reload"); } + } } diff --git a/packages/payment-backend/src/api/functional/monitors/health/index.ts b/packages/payment-backend/src/api/functional/monitors/health/index.ts index d5510b4..c6a7258 100644 --- a/packages/payment-backend/src/api/functional/monitors/health/index.ts +++ b/packages/payment-backend/src/api/functional/monitors/health/index.ts @@ -8,7 +8,7 @@ import type { IConnection } from "@nestia/fetcher"; import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; /** - * @controller HealthController.get + * @controller MonitorHealthController.get * @path GET /monitors/health * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/packages/payment-backend/src/api/functional/monitors/performance/index.ts b/packages/payment-backend/src/api/functional/monitors/performance/index.ts index 6efe697..2a720dd 100644 --- a/packages/payment-backend/src/api/functional/monitors/performance/index.ts +++ b/packages/payment-backend/src/api/functional/monitors/performance/index.ts @@ -11,7 +11,7 @@ import typia from "typia"; import type { IPerformance } from "../../../structures/monitors/IPerformance"; /** - * @controller PerformanceController.get + * @controller MonitorPerformanceController.get * @path GET /monitors/performance * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/packages/payment-backend/src/api/functional/monitors/system/index.ts b/packages/payment-backend/src/api/functional/monitors/system/index.ts index dbf5b9c..d1ac16e 100644 --- a/packages/payment-backend/src/api/functional/monitors/system/index.ts +++ b/packages/payment-backend/src/api/functional/monitors/system/index.ts @@ -11,7 +11,7 @@ import typia from "typia"; import type { ISystem } from "../../../structures/monitors/ISystem"; /** - * @controller SystemController.get + * @controller MonitorSystemController.get * @path GET /monitors/system * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/packages/payment-backend/src/api/structures/common/IEntity.ts b/packages/payment-backend/src/api/structures/common/IEntity.ts index 07fb86d..a0e35bb 100644 --- a/packages/payment-backend/src/api/structures/common/IEntity.ts +++ b/packages/payment-backend/src/api/structures/common/IEntity.ts @@ -6,10 +6,10 @@ * @author Samchon */ export interface IEntity { - /** - * Primary Key - * - * @format uuid - */ - id: string; + /** + * Primary Key + * + * @format uuid + */ + id: string; } diff --git a/packages/payment-backend/src/api/structures/monitors/IPerformance.ts b/packages/payment-backend/src/api/structures/monitors/IPerformance.ts index 6e4df06..ed35309 100644 --- a/packages/payment-backend/src/api/structures/monitors/IPerformance.ts +++ b/packages/payment-backend/src/api/structures/monitors/IPerformance.ts @@ -1,5 +1,5 @@ export interface IPerformance { - cpu: NodeJS.CpuUsage; - memory: NodeJS.MemoryUsage; - resource: NodeJS.ResourceUsage; + cpu: NodeJS.CpuUsage; + memory: NodeJS.MemoryUsage; + resource: NodeJS.ResourceUsage; } diff --git a/packages/payment-backend/src/api/structures/monitors/ISystem.ts b/packages/payment-backend/src/api/structures/monitors/ISystem.ts index 500fcd3..fccfccd 100644 --- a/packages/payment-backend/src/api/structures/monitors/ISystem.ts +++ b/packages/payment-backend/src/api/structures/monitors/ISystem.ts @@ -1,75 +1,75 @@ export interface ISystem { - /** - * Random Unique ID. - */ - uid: string; + /** + * Random Unique ID. + */ + uid: string; - /** - * `process.argv` - */ - arguments: string[]; + /** + * `process.argv` + */ + arguments: string[]; - /** - * Git commit information. - */ - commit: ISystem.ICommit; + /** + * Git commit information. + */ + commit: ISystem.ICommit; - /** - * `package.json` */ - package: ISystem.IPackage; + /** + * `package.json` */ + package: ISystem.IPackage; - /** - * Creation time of this system. - */ - created_at: string; + /** + * Creation time of this system. + */ + created_at: string; } export namespace ISystem { + /** + * Git commit information. + */ + export interface ICommit { + shortHash: string; + branch: string; + hash: string; + subject: string; + sanitizedSubject: string; + body: string; + author: ICommit.IUser; + committer: ICommit.IUser; + authored_at: string; + commited_at: string; + notes?: string; + tags: string[]; + } + export namespace ICommit { /** - * Git commit information. + * Git user information. */ - export interface ICommit { - shortHash: string; - branch: string; - hash: string; - subject: string; - sanitizedSubject: string; - body: string; - author: ICommit.IUser; - committer: ICommit.IUser; - authored_at: string; - commited_at: string; - notes?: string; - tags: string[]; - } - export namespace ICommit { - /** - * Git user information. - */ - export interface IUser { - name: string; - email: string; - } + export interface IUser { + name: string; + email: string; } + } - /** - * NPM package information. - */ - export interface IPackage { - name: string; - version: string; - description: string; - scripts: Record; - repository: { type: "git"; url: string }; - author: string; - license: string; - bugs: { url: string }; - homepage: string; - devDependencies: Record; - dependencies: Record; - publishConfig?: { registry: string }; - main?: string; - typings?: string; - files?: string[]; - } + /** + * NPM package information. + */ + export interface IPackage { + name: string; + version: string; + description: string; + scripts: Record; + repository: { type: "git"; url: string }; + author: string; + license: string; + bugs: { url: string }; + homepage: string; + devDependencies: Record; + dependencies: Record; + publishConfig?: { registry: string }; + main?: string; + typings?: string; + files?: string[]; + } } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts b/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts index 0fbc08c..09e390b 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts @@ -8,94 +8,94 @@ import { IPaymentSource } from "./IPaymentSource"; * @author Samchon */ export interface IPaymentCancelHistory { + /** + * 결제 취소 사유. + */ + reason: string; + + /** + * 환불 금액. + */ + price: number & tags.Minimum<0>; + + /** + * 레코드 생성 일시. + * + * 즉, 결제 취소가 발생한 시각. + */ + created_at: string & tags.Format<"date-time">; +} +export namespace IPaymentCancelHistory { + /** + * 결제 취소 입력 정보. + */ + export interface IStore { /** - * 결제 취소 사유. + * 결제의 근간이 된 원천 레코드 정보. */ - reason: string; + source: IPaymentSource; /** - * 환불 금액. + * 결제 이력에 대한 비밀번호 입력. */ - price: number & tags.Minimum<0>; + password: string; /** - * 레코드 생성 일시. - * - * 즉, 결제 취소가 발생한 시각. + * 환불 금액. */ - created_at: string & tags.Format<"date-time">; -} -export namespace IPaymentCancelHistory { + price: number & tags.Minimum<0>; + /** - * 결제 취소 입력 정보. + * 결제 취소 사유. */ - export interface IStore { - /** - * 결제의 근간이 된 원천 레코드 정보. - */ - source: IPaymentSource; - - /** - * 결제 이력에 대한 비밀번호 입력. - */ - password: string; - - /** - * 환불 금액. - */ - price: number & tags.Minimum<0>; - - /** - * 결제 취소 사유. - */ - reason: string; - - /** - * 환불 계좌 정보. - * - * 가상 계좌로 입금한 경우, 결제 취소시, 이를 환불받을 계좌가 필요함. - * - * 단, 이 정보는 본 결제 시스템에 저장하지 아니함. - */ - account: null | IBankAccount; - } + reason: string; /** - * 은행 계좌 정보. + * 환불 계좌 정보. * * 가상 계좌로 입금한 경우, 결제 취소시, 이를 환불받을 계좌가 필요함. * * 단, 이 정보는 본 결제 시스템에 저장하지 아니함. */ - export interface IBankAccount { - /** - * 은행 이름. - */ - bank: string; + account: null | IBankAccount; + } - /** - * 계좌번호. - */ - account: string; + /** + * 은행 계좌 정보. + * + * 가상 계좌로 입금한 경우, 결제 취소시, 이를 환불받을 계좌가 필요함. + * + * 단, 이 정보는 본 결제 시스템에 저장하지 아니함. + */ + export interface IBankAccount { + /** + * 은행 이름. + */ + bank: string; - /** - * 예금주. - */ - holder: string; + /** + * 계좌번호. + */ + account: string; - /** - * 연락처, 핸드폰 번호. - */ - mobile: string; - } + /** + * 예금주. + */ + holder: string; /** - * @internal + * 연락처, 핸드폰 번호. */ - export interface IProps { - data: object; - created_at: Date; - price: number; - reason: string; - } + mobile: string; + } + + /** + * @internal + */ + export interface IProps { + data: object; + created_at: Date; + price: number; + reason: string; + } } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentHistory.ts b/packages/payment-backend/src/api/structures/payments/IPaymentHistory.ts index 079e977..b92f881 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentHistory.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentHistory.ts @@ -30,142 +30,142 @@ import { IPaymentVendor } from "./IPaymentVendor"; * @author Samchon */ export type IPaymentHistory = - | IPaymentHistory.IamportType - | IPaymentHistory.TossType; + | IPaymentHistory.IamportType + | IPaymentHistory.TossType; export namespace IPaymentHistory { + /** + * 아임포트로부터의 결제 내역. + */ + export type IamportType = BaseType<"iamport", IIamportPayment>; + + /** + * 토스 페이먼츠로부터의 결제 내역. + */ + export type TossType = BaseType<"toss.payments", ITossPayment>; + + /** + * 결제 내역의 기본 정보. + */ + export interface BaseType< + VendorCode extends IPaymentVendor.Code, + Data extends object, + > { /** - * 아임포트로부터의 결제 내역. + * Primary Key. */ - export type IamportType = BaseType<"iamport", IIamportPayment>; + id: string & tags.Format<"uuid">; /** - * 토스 페이먼츠로부터의 결제 내역. + * 벤더사 식별자 코드. + * + * {@link IPaymentVendor.code}와 완전히 동일한 값이되, 단지 union type + * specialization 을 위해 중복 표기하였을 뿐이다. `if condition` 을 통하여 + * {@link IPaymentHistory.data}의 타입을 특정할 수 있다. */ - export type TossType = BaseType<"toss.payments", ITossPayment>; + vendor_code: VendorCode; /** - * 결제 내역의 기본 정보. + * 벤더 정보. */ - export interface BaseType< - VendorCode extends IPaymentVendor.Code, - Data extends object, - > { - /** - * Primary Key. - */ - id: string & tags.Format<"uuid">; - - /** - * 벤더사 식별자 코드. - * - * {@link IPaymentVendor.code}와 완전히 동일한 값이되, 단지 union type - * specialization 을 위해 중복 표기하였을 뿐이다. `if condition` 을 통하여 - * {@link IPaymentHistory.data}의 타입을 특정할 수 있다. - */ - vendor_code: VendorCode; + vendor: IPaymentVendor; - /** - * 벤더 정보. - */ - vendor: IPaymentVendor; + /** + * 원천 래코드 정보. + */ + source: IPaymentSource; - /** - * 원천 래코드 정보. - */ - source: IPaymentSource; + /** + * 결제 상세 데이터, 벤더별로 데이터 양식이 다르니 주의할 것. + */ + data: Data; - /** - * 결제 상세 데이터, 벤더별로 데이터 양식이 다르니 주의할 것. - */ - data: Data; + /** + * 통화 단위 + * + * KRW, USB, JPY 등. + */ + currency: string; - /** - * 통화 단위 - * - * KRW, USB, JPY 등. - */ - currency: string; + /** + * 결제 가격. + */ + price: number & tags.Minimum<0>; - /** - * 결제 가격. - */ - price: number & tags.Minimum<0>; + /** + * 결제 취소시의 환불 금액. + */ + refund: null | (number & tags.Minimum<0>); - /** - * 결제 취소시의 환불 금액. - */ - refund: null | (number & tags.Minimum<0>); + /** + * 결제 정보가 갱신되었을 때, 이를 수신할 URL + */ + webhook_url: null | (string & tags.Format<"url">); - /** - * 결제 정보가 갱신되었을 때, 이를 수신할 URL - */ - webhook_url: null | (string & tags.Format<"url">); + /** + * 결제 레코드 생성 일시. + */ + created_at: string & tags.Format<"date-time">; - /** - * 결제 레코드 생성 일시. - */ - created_at: string & tags.Format<"date-time">; + /** + * 결제 완료 일시. + */ + paid_at: null | (string & tags.Format<"date-time">); - /** - * 결제 완료 일시. - */ - paid_at: null | (string & tags.Format<"date-time">); + /** + * 결제 취소 일시. + */ + cancelled_at: null | (string & tags.Format<"date-time">); - /** - * 결제 취소 일시. - */ - cancelled_at: null | (string & tags.Format<"date-time">); + /** + * 결제 취소 내역 리스트. + */ + cancels: IPaymentCancelHistory[]; + } + + /** + * 결제 입력 정보. + * + * SDK 에서 받은 데이터를 취합하여 결제 진행 상황을 서버에 알려준다. + */ + export interface IStore { + /** + * 벤더사 정보 + */ + vendor: IPaymentVendor<"iamport" | "toss.payments">; - /** - * 결제 취소 내역 리스트. - */ - cancels: IPaymentCancelHistory[]; - } + /** + * 결제의 근간이 된 원천 레코드 정보. + */ + source: IPaymentSource; /** - * 결제 입력 정보. + * 결제되어야 할 총액. * - * SDK 에서 받은 데이터를 취합하여 결제 진행 상황을 서버에 알려준다. - */ - export interface IStore { - /** - * 벤더사 정보 - */ - vendor: IPaymentVendor<"iamport" | "toss.payments">; - - /** - * 결제의 근간이 된 원천 레코드 정보. - */ - source: IPaymentSource; - - /** - * 결제되어야 할 총액. - * - * 실 결제금액과 비교하여 이와 다를 시, 422 에러가 리턴됨. - */ - price: number & tags.Minimum<0>; - - /** - * 레코드 열람에 사용할 비밀번호 설정. - */ - password: string; - - /** - * 결제 정보가 갱신되었을 때, 이를 수신할 URL - */ - webhook_url: string & tags.Format<"url">; - } - - /** - * @internal - */ - export interface IProps { - currency: string; - price: number; - refund: null | number; - paid_at: null | Date; - cancelled_at: null | Date; - cancels: IPaymentCancelHistory.IProps[]; - data: object; - } + * 실 결제금액과 비교하여 이와 다를 시, 422 에러가 리턴됨. + */ + price: number & tags.Minimum<0>; + + /** + * 레코드 열람에 사용할 비밀번호 설정. + */ + password: string; + + /** + * 결제 정보가 갱신되었을 때, 이를 수신할 URL + */ + webhook_url: string & tags.Format<"url">; + } + + /** + * @internal + */ + export interface IProps { + currency: string; + price: number; + refund: null | number; + paid_at: null | Date; + cancelled_at: null | Date; + cancels: IPaymentCancelHistory.IProps[]; + data: object; + } } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts b/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts index d3fa144..046ede2 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts @@ -20,97 +20,97 @@ import { IPaymentVendor } from "./IPaymentVendor"; * @author Samchon */ export type IPaymentReservation = - | IPaymentReservation.IamportType - | IPaymentReservation.TossType; + | IPaymentReservation.IamportType + | IPaymentReservation.TossType; export namespace IPaymentReservation { + /** + * 아임 포트의 간편 결제 카드 정보. + */ + export type IamportType = BaseType<"iamport", IIamportSubscription>; + + /** + * 토스의 간편 결제 수단 정보. + */ + export type TossType = BaseType<"toss.payments", ITossBilling>; + + /** + * 간편 결제 수단의 기본 정보. + */ + export interface BaseType< + VendorCode extends IPaymentVendor.Code, + Data extends object, + > { /** - * 아임 포트의 간편 결제 카드 정보. + * Primary Key. */ - export type IamportType = BaseType<"iamport", IIamportSubscription>; + id: string & tags.Format<"uuid">; /** - * 토스의 간편 결제 수단 정보. + * 벤더사 코드. + * + * {@link IPaymentVendor.code}와 완전히 동일한 값이되, 단지 union type + * specialization 을 위해 중복 표기하였을 뿐이다. If else condition 을 통하여 + * {@link IPaymentReservation.data}의 타입을 특정할 수 있다. */ - export type TossType = BaseType<"toss.payments", ITossBilling>; + vendor_code: VendorCode; /** - * 간편 결제 수단의 기본 정보. + * 벤더사. */ - export interface BaseType< - VendorCode extends IPaymentVendor.Code, - Data extends object, - > { - /** - * Primary Key. - */ - id: string & tags.Format<"uuid">; + vendor: IPaymentVendor; - /** - * 벤더사 코드. - * - * {@link IPaymentVendor.code}와 완전히 동일한 값이되, 단지 union type - * specialization 을 위해 중복 표기하였을 뿐이다. If else condition 을 통하여 - * {@link IPaymentReservation.data}의 타입을 특정할 수 있다. - */ - vendor_code: VendorCode; - - /** - * 벤더사. - */ - vendor: IPaymentVendor; - - /** - * 대상 액터의 참조 정보. - */ - source: IPaymentSource; + /** + * 대상 액터의 참조 정보. + */ + source: IPaymentSource; - /** - * 제목. - */ - title: string; + /** + * 제목. + */ + title: string; - /** - * 벤더사 데이터. - */ - data: Data; + /** + * 벤더사 데이터. + */ + data: Data; - /** - * 레코드 생성 일시. - */ - created_at: string & tags.Format<"date-time">; - } + /** + * 레코드 생성 일시. + */ + created_at: string & tags.Format<"date-time">; + } + /** + * 간편 결제 수단 등록 정보. + * + * 결제사의 팝업 창로부터 전달받은 식별자 정보를 취합하여 전달한다. + * + * 참고로 아임포트의 경우 간편 결제로 등록한 카드에 자체 식별자 번호를 부여하지 않는다. + * 따라서 귀하의 서비스가 발행한 식별자 ID 가 곧, 해당 간편 결제 수단의 유일무이한 + * 식별자ㅏ 되니, 이를 {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 + * 모두 동일하게 할당해주면 된다. + */ + export interface IStore { /** - * 간편 결제 수단 등록 정보. - * - * 결제사의 팝업 창로부터 전달받은 식별자 정보를 취합하여 전달한다. - * - * 참고로 아임포트의 경우 간편 결제로 등록한 카드에 자체 식별자 번호를 부여하지 않는다. - * 따라서 귀하의 서비스가 발행한 식별자 ID 가 곧, 해당 간편 결제 수단의 유일무이한 - * 식별자ㅏ 되니, 이를 {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 - * 모두 동일하게 할당해주면 된다. + * 벤더사 정보. */ - export interface IStore { - /** - * 벤더사 정보. - */ - vendor: IPaymentVendor; + vendor: IPaymentVendor; - /** - * 원천 레코드 정보. - */ - source: IPaymentSource; + /** + * 원천 레코드 정보. + */ + source: IPaymentSource; - /** - * 제목 - */ - title: string; + /** + * 제목 + */ + title: string; - /** - * 간편결제 비밀번호. - * - * 주의할 점은 카드 비밀번호가 아니라는 것. - */ - password: string; - } + /** + * 간편결제 비밀번호. + * + * 주의할 점은 카드 비밀번호가 아니라는 것. + */ + password: string; + } } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentSource.ts b/packages/payment-backend/src/api/structures/payments/IPaymentSource.ts index b998534..1a142c1 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentSource.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentSource.ts @@ -12,42 +12,42 @@ import { tags } from "typia"; * @author Samchon */ export interface IPaymentSource { - /** - * DB 스키마 이름 - */ - schema: string; + /** + * DB 스키마 이름 + */ + schema: string; - /** - * DB 테이블 명 - */ - table: string; + /** + * DB 테이블 명 + */ + table: string; - /** - * 참조 레코드의 PK - */ - id: string & tags.Format<"uuid">; + /** + * 참조 레코드의 PK + */ + id: string & tags.Format<"uuid">; } export namespace IPaymentSource { - /** - * 접근자 정보. - * - * `IPaymentSource.IAccessor` 는 {@link IPaymentHistory 결제 내역} 내지 - * {@link IPaymentReservation 간편 결제 수단 정보}를 조회할 때, 그것의 고유 식별자 - * ID 가 아닌 원천 레코드 식별자 정보 {@link IPaymentSource} 를 통하여 조회할 때 - * 사용하는 접근자 정보이다. - * - * 다만 `payments-server` 의 모든 개별 레코드는 이를 조회할 시 비밀번호가 필요하기에, - * {@link IPaymentSource} 의 속성들에 비밀번호가 하나 더 추가되었을 뿐이다. - */ - export interface IAccessor extends IPaymentSource, IPassword {} + /** + * 접근자 정보. + * + * `IPaymentSource.IAccessor` 는 {@link IPaymentHistory 결제 내역} 내지 + * {@link IPaymentReservation 간편 결제 수단 정보}를 조회할 때, 그것의 고유 식별자 + * ID 가 아닌 원천 레코드 식별자 정보 {@link IPaymentSource} 를 통하여 조회할 때 + * 사용하는 접근자 정보이다. + * + * 다만 `payments-server` 의 모든 개별 레코드는 이를 조회할 시 비밀번호가 필요하기에, + * {@link IPaymentSource} 의 속성들에 비밀번호가 하나 더 추가되었을 뿐이다. + */ + export interface IAccessor extends IPaymentSource, IPassword {} + /** + * 비밀번호 입력 정보. + */ + export interface IPassword { /** - * 비밀번호 입력 정보. + * 레코드 조회를 위한 비밀번호 입력. */ - export interface IPassword { - /** - * 레코드 조회를 위한 비밀번호 입력. - */ - password: string; - } + password: string; + } } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts b/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts index 0ddd377..1e207cf 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts @@ -6,46 +6,46 @@ * @author Samchon */ export interface IPaymentVendor { - /** - * 벤더사 식별자 코드. - * - * 아임포트의 경우에는 `iamport` 를, 토스의 경우에는 `toss.payments` 를 적어주면 된다. - */ - code: Code; + /** + * 벤더사 식별자 코드. + * + * 아임포트의 경우에는 `iamport` 를, 토스의 경우에는 `toss.payments` 를 적어주면 된다. + */ + code: Code; - /** - * 벤더사에 등록한 스토어 ID. - * - * 결제 PG 사들은 서비스 주소가 다르거나, 또는 동일한 서비스이되 연결되는 백엔드 서버 - * 주소가 다르거든, 각기 다른 스토어 계정을 신청해 사용하라고 한다. 이는 요즘같이 MSA - * (Micro Service Architecture) 가 대세인 시대에 매우 불합리한 방식이기는 하지만, 어쨋든 - * 이러한 이유로 인하여, 한 회사 내지 단체가 복수의 스토어 ID 를 가지는 경우가 왕왕 - * 있다. - * - * 때문에 `payments-server` 는, {@link IPaymentHistory 결제 내역}을 발행하거나 - * {@link IPaymentReservation 간편 결제 수단}을 등록할 때 모두, 사용된 스토어의 ID - * 를 반드시 기재하도록 하고 있다. - */ - store_id: string; + /** + * 벤더사에 등록한 스토어 ID. + * + * 결제 PG 사들은 서비스 주소가 다르거나, 또는 동일한 서비스이되 연결되는 백엔드 서버 + * 주소가 다르거든, 각기 다른 스토어 계정을 신청해 사용하라고 한다. 이는 요즘같이 MSA + * (Micro Service Architecture) 가 대세인 시대에 매우 불합리한 방식이기는 하지만, 어쨋든 + * 이러한 이유로 인하여, 한 회사 내지 단체가 복수의 스토어 ID 를 가지는 경우가 왕왕 + * 있다. + * + * 때문에 `payments-server` 는, {@link IPaymentHistory 결제 내역}을 발행하거나 + * {@link IPaymentReservation 간편 결제 수단}을 등록할 때 모두, 사용된 스토어의 ID + * 를 반드시 기재하도록 하고 있다. + */ + store_id: string; - /** - * 벤더사로부터 발급받은 식별자 번호. - * - * 결제 PG 사들이 제공하는 팝업창을 이용하여 결제를 진행하거나 혹은 간편 결제 수단을 - * 등록하거든, 결제 PG 사들은 해당 건에 대하여 별도의 식별자 번호를 발급한다. - * `IPaymentVendor.uid` 는 이처럼 결제 PG 사들이 발급해 준 식별자 번호를 기재하는 - * 속성이다. - * - * 단 예외가 하나 있어, 아임포트는 간편 결제 카드 등록 건에 대하여 별도의 식별자 번호를 - * 부여하지 않고, 귀하의 서비스에서 발급해 준 ID 를 그대로 사용한다. 때문에 아임포트를 - * 통한 간편 결제 카드 등록의 건만 예외적으로, `IPaymentVendor.uid` 에 - * {@link IPaymentSource.id} 를 동일하게 할당해주어야 한다. - */ - uid: string; + /** + * 벤더사로부터 발급받은 식별자 번호. + * + * 결제 PG 사들이 제공하는 팝업창을 이용하여 결제를 진행하거나 혹은 간편 결제 수단을 + * 등록하거든, 결제 PG 사들은 해당 건에 대하여 별도의 식별자 번호를 발급한다. + * `IPaymentVendor.uid` 는 이처럼 결제 PG 사들이 발급해 준 식별자 번호를 기재하는 + * 속성이다. + * + * 단 예외가 하나 있어, 아임포트는 간편 결제 카드 등록 건에 대하여 별도의 식별자 번호를 + * 부여하지 않고, 귀하의 서비스에서 발급해 준 ID 를 그대로 사용한다. 때문에 아임포트를 + * 통한 간편 결제 카드 등록의 건만 예외적으로, `IPaymentVendor.uid` 에 + * {@link IPaymentSource.id} 를 동일하게 할당해주어야 한다. + */ + uid: string; } export namespace IPaymentVendor { - /** - * 벤더사 식별자 코드 타입. - */ - export type Code = "iamport" | "toss.payments"; + /** + * 벤더사 식별자 코드 타입. + */ + export type Code = "iamport" | "toss.payments"; } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentWebhook.ts b/packages/payment-backend/src/api/structures/payments/IPaymentWebhook.ts index 6347697..79eab61 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentWebhook.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentWebhook.ts @@ -9,36 +9,36 @@ import { IPaymentSource } from "./IPaymentSource"; * @author Samchon */ export interface IPaymentWebhook { - /** - * Primary Key. - */ - id: string & tags.Format<"uuid">; + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; - /** - * 원천 레코드 정보. - */ - source: IPaymentSource; + /** + * 원천 레코드 정보. + */ + source: IPaymentSource; - /** - * 웹훅 이벤트 수신 전 결제 내역 정보. - * - * PG 사로부터 웹훅 이벤트 데이터를 수신하기 전, 즉 이전 상태의 결제 내역 정보. - */ - previous: IPaymentWebhook.IHistory; + /** + * 웹훅 이벤트 수신 전 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하기 전, 즉 이전 상태의 결제 내역 정보. + */ + previous: IPaymentWebhook.IHistory; - /** - * 웹훅 이벤트 수신 후 결제 내역 정보. - * - * PG 사로부터 웹훅 이벤트 데이터를 수신하여, 새로이 바뀌게 된 결제 내역 정보. - */ - current: IPaymentWebhook.IHistory; + /** + * 웹훅 이벤트 수신 후 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하여, 새로이 바뀌게 된 결제 내역 정보. + */ + current: IPaymentWebhook.IHistory; } export namespace IPaymentWebhook { - /** - * 결제 내역 정보. - * - * 본래의 결제 내역 {@link IPaymentHistory} 에서 중복되는 원천 레코드 정보 - * {@link IPaymentSource} 를 뺌. - */ - export type IHistory = Omit; + /** + * 결제 내역 정보. + * + * 본래의 결제 내역 {@link IPaymentHistory} 에서 중복되는 원천 레코드 정보 + * {@link IPaymentSource} 를 뺌. + */ + export type IHistory = Omit; } diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentWebhookHistory.ts b/packages/payment-backend/src/api/structures/payments/IPaymentWebhookHistory.ts index 83e43d4..d9a9e37 100644 --- a/packages/payment-backend/src/api/structures/payments/IPaymentWebhookHistory.ts +++ b/packages/payment-backend/src/api/structures/payments/IPaymentWebhookHistory.ts @@ -22,36 +22,36 @@ import { IPaymentSource } from "./IPaymentSource"; * @author Samchon */ export interface IPaymentWebhookHistory { - /** - * Primary Key. - */ - id: string & tags.Format<"uuid">; + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; - /** - * 원천 레코드 정보. - */ - source: IPaymentSource; + /** + * 원천 레코드 정보. + */ + source: IPaymentSource; - /** - * 웹훅 이벤트 수신 전 결제 내역 정보. - * - * PG 사로부터 웹훅 이벤트 데이터를 수신하기 전, 즉 이전 상태의 결제 내역 정보. - */ - previous: IPaymentWebhookHistory.IHistory; + /** + * 웹훅 이벤트 수신 전 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하기 전, 즉 이전 상태의 결제 내역 정보. + */ + previous: IPaymentWebhookHistory.IHistory; - /** - * 웹훅 이벤트 수신 후 결제 내역 정보. - * - * PG 사로부터 웹훅 이벤트 데이터를 수신하여, 새로이 바뀌게 된 결제 내역 정보. - */ - current: IPaymentWebhookHistory.IHistory; + /** + * 웹훅 이벤트 수신 후 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하여, 새로이 바뀌게 된 결제 내역 정보. + */ + current: IPaymentWebhookHistory.IHistory; } export namespace IPaymentWebhookHistory { - /** - * 결제 내역 정보. - * - * 본래의 결제 내역 {@link IPaymentHistory} 에서 중복되는 원천 레코드 정보 - * {@link IPaymentSource} 를 뺌. - */ - export type IHistory = Omit; + /** + * 결제 내역 정보. + * + * 본래의 결제 내역 {@link IPaymentHistory} 에서 중복되는 원천 레코드 정보 + * {@link IPaymentSource} 를 뺌. + */ + export type IHistory = Omit; } diff --git a/packages/payment-backend/src/controllers/monitors/HealthController.ts b/packages/payment-backend/src/controllers/monitors/HealthController.ts deleted file mode 100644 index b1d65a8..0000000 --- a/packages/payment-backend/src/controllers/monitors/HealthController.ts +++ /dev/null @@ -1,7 +0,0 @@ -import nest from "@modules/nestjs"; - -@nest.Controller("monitors/health") -export class HealthController { - @nest.Get() - public get(): void {} -} diff --git a/packages/payment-backend/src/controllers/monitors/MonitorHealthController.ts b/packages/payment-backend/src/controllers/monitors/MonitorHealthController.ts new file mode 100644 index 0000000..3c58b27 --- /dev/null +++ b/packages/payment-backend/src/controllers/monitors/MonitorHealthController.ts @@ -0,0 +1,7 @@ +import { Controller, Get } from "@nestjs/common"; + +@Controller("monitors/health") +export class MonitorHealthController { + @Get() + public get(): void {} +} diff --git a/packages/payment-backend/src/controllers/monitors/MonitorPerformanceController.ts b/packages/payment-backend/src/controllers/monitors/MonitorPerformanceController.ts new file mode 100644 index 0000000..6efed33 --- /dev/null +++ b/packages/payment-backend/src/controllers/monitors/MonitorPerformanceController.ts @@ -0,0 +1,16 @@ +import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; + +import { IPerformance } from "../../api/structures/monitors/IPerformance"; + +@Controller("monitors/performance") +export class MonitorPerformanceController { + @core.EncryptedRoute.Get() + public async get(): Promise { + return { + cpu: process.cpuUsage(), + memory: process.memoryUsage(), + resource: process.resourceUsage(), + }; + } +} diff --git a/packages/payment-backend/src/controllers/monitors/MonitorSystemController.ts b/packages/payment-backend/src/controllers/monitors/MonitorSystemController.ts new file mode 100644 index 0000000..f6d2f7b --- /dev/null +++ b/packages/payment-backend/src/controllers/monitors/MonitorSystemController.ts @@ -0,0 +1,19 @@ +import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; + +import { ISystem } from "../../api/structures/monitors/ISystem"; +import { SystemProvider } from "../../providers/monitors/SystemProvider"; + +@Controller("monitors/system") +export class MonitorSystemController { + @core.EncryptedRoute.Get() + public async get(): Promise { + return { + uid: SystemProvider.uid, + arguments: process.argv, + package: await SystemProvider.package(), + commit: await SystemProvider.commit(), + created_at: SystemProvider.created_at.toString(), + }; + } +} diff --git a/packages/payment-backend/src/controllers/monitors/PerformanceController.ts b/packages/payment-backend/src/controllers/monitors/PerformanceController.ts deleted file mode 100644 index 0147690..0000000 --- a/packages/payment-backend/src/controllers/monitors/PerformanceController.ts +++ /dev/null @@ -1,16 +0,0 @@ -import nest from "@modules/nestjs"; -import core from "@nestia/core"; - -import { IPerformance } from "../../api/structures/monitors/IPerformance"; - -@nest.Controller("monitors/performance") -export class PerformanceController { - @core.EncryptedRoute.Get() - public async get(): Promise { - return { - cpu: process.cpuUsage(), - memory: process.memoryUsage(), - resource: process.resourceUsage(), - }; - } -} diff --git a/packages/payment-backend/src/controllers/monitors/SystemController.ts b/packages/payment-backend/src/controllers/monitors/SystemController.ts deleted file mode 100644 index 47809d9..0000000 --- a/packages/payment-backend/src/controllers/monitors/SystemController.ts +++ /dev/null @@ -1,19 +0,0 @@ -import nest from "@modules/nestjs"; -import core from "@nestia/core"; - -import { ISystem } from "../../api/structures/monitors/ISystem"; -import { SystemProvider } from "../../providers/monitors/SystemProvider"; - -@nest.Controller("monitors/system") -export class SystemController { - @core.EncryptedRoute.Get() - public async get(): Promise { - return { - uid: SystemProvider.uid, - arguments: process.argv, - package: await SystemProvider.package(), - commit: await SystemProvider.commit(), - created_at: SystemProvider.created_at.toString(), - }; - } -} diff --git a/packages/payment-backend/src/controllers/payments/PaymentHistoriesController.ts b/packages/payment-backend/src/controllers/payments/PaymentHistoriesController.ts index 5a0ba64..a3807a0 100644 --- a/packages/payment-backend/src/controllers/payments/PaymentHistoriesController.ts +++ b/packages/payment-backend/src/controllers/payments/PaymentHistoriesController.ts @@ -1,5 +1,5 @@ -import nest from "@modules/nestjs"; import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; import { IPaymentCancelHistory } from "@samchon/payment-api/lib/structures/payments/IPaymentCancelHistory"; import { IPaymentHistory } from "@samchon/payment-api/lib/structures/payments/IPaymentHistory"; import { IPaymentSource } from "@samchon/payment-api/lib/structures/payments/IPaymentSource"; @@ -8,70 +8,70 @@ import { tags } from "typia"; import { PaymentCancelHistoryProvider } from "../../providers/payments/PaymentCancelHistoryProvider"; import { PaymentHistoryProvider } from "../../providers/payments/PaymentHistoryProvider"; -@nest.Controller("payments/histories") +@Controller("payments/histories") export class PaymentHistoriesController { - /** - * 결제 내역 상세 조회하기. - * - * @param input 결제 내역의 원천 정보 + 비밀번호 - * @returns 결제 내역 - * - * @author Samchon - */ - @core.EncryptedRoute.Patch("get") - public async get( - @core.EncryptedBody() input: IPaymentSource.IAccessor, - ): Promise { - return PaymentHistoryProvider.find({ - source_schema: input.schema, - source_table: input.table, - source_id: input.id, - })(input.password); - } + /** + * 결제 내역 상세 조회하기. + * + * @param input 결제 내역의 원천 정보 + 비밀번호 + * @returns 결제 내역 + * + * @author Samchon + */ + @core.EncryptedRoute.Patch("get") + public async get( + @core.EncryptedBody() input: IPaymentSource.IAccessor, + ): Promise { + return PaymentHistoryProvider.find({ + source_schema: input.schema, + source_table: input.table, + source_id: input.id, + })(input.password); + } - /** - * 결제 내역 상세 조회하기. - * - * @param id Primary Key - * @param input 결제 내역의 비밀번호 - * @returns 결제 내역 - * - * @author Samchon - */ - @core.EncryptedRoute.Patch(":id") - public async at( - @core.TypedParam("id") id: string & tags.Format<"uuid">, - @core.EncryptedBody() input: IPaymentSource.IPassword, - ): Promise { - return PaymentHistoryProvider.find({ id })(input.password); - } + /** + * 결제 내역 상세 조회하기. + * + * @param id Primary Key + * @param input 결제 내역의 비밀번호 + * @returns 결제 내역 + * + * @author Samchon + */ + @core.EncryptedRoute.Patch(":id") + public async at( + @core.TypedParam("id") id: string & tags.Format<"uuid">, + @core.EncryptedBody() input: IPaymentSource.IPassword, + ): Promise { + return PaymentHistoryProvider.find({ id })(input.password); + } - /** - * 결제 내역 발행하기. - * - * @param input 결제 내역 입력 정보 - * @returns 결제 내역 - * - * @author Samchon - */ - @core.EncryptedRoute.Post() - public async store( - @core.EncryptedBody() input: IPaymentHistory.IStore, - ): Promise { - return PaymentHistoryProvider.store(input); - } + /** + * 결제 내역 발행하기. + * + * @param input 결제 내역 입력 정보 + * @returns 결제 내역 + * + * @author Samchon + */ + @core.EncryptedRoute.Post() + public async store( + @core.EncryptedBody() input: IPaymentHistory.IStore, + ): Promise { + return PaymentHistoryProvider.store(input); + } - /** - * 결제 취소하기. - * - * @param input 결제 취소 내역 입력 정보 - * - * @author Samchon - */ - @core.EncryptedRoute.Put("cancel") - public async cancel( - @core.EncryptedBody() input: IPaymentCancelHistory.IStore, - ): Promise { - return PaymentCancelHistoryProvider.store(input); - } + /** + * 결제 취소하기. + * + * @param input 결제 취소 내역 입력 정보 + * + * @author Samchon + */ + @core.EncryptedRoute.Put("cancel") + public async cancel( + @core.EncryptedBody() input: IPaymentCancelHistory.IStore, + ): Promise { + return PaymentCancelHistoryProvider.store(input); + } } diff --git a/packages/payment-backend/src/controllers/payments/PaymentInternalController.ts b/packages/payment-backend/src/controllers/payments/PaymentInternalController.ts index 762830a..4b7db98 100644 --- a/packages/payment-backend/src/controllers/payments/PaymentInternalController.ts +++ b/packages/payment-backend/src/controllers/payments/PaymentInternalController.ts @@ -1,16 +1,16 @@ -import nest from "@modules/nestjs"; import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; import { IPaymentWebhookHistory } from "@samchon/payment-api/lib/structures/payments/IPaymentWebhookHistory"; import { FakePaymentStorage } from "../../providers/payments/FakePaymentStorage"; -@nest.Controller("payments/internal") +@Controller("payments/internal") export class PaymentInternalController { - /** - * @internal - */ - @core.EncryptedRoute.Post("webhook") - public webhook(@core.EncryptedBody() input: IPaymentWebhookHistory): void { - FakePaymentStorage.webhooks.push_back(input); - } + /** + * @internal + */ + @core.EncryptedRoute.Post("webhook") + public webhook(@core.EncryptedBody() input: IPaymentWebhookHistory): void { + FakePaymentStorage.webhooks.push_back(input); + } } diff --git a/packages/payment-backend/src/controllers/payments/PaymentReservationsController.ts b/packages/payment-backend/src/controllers/payments/PaymentReservationsController.ts index c3c4bac..e95c96f 100644 --- a/packages/payment-backend/src/controllers/payments/PaymentReservationsController.ts +++ b/packages/payment-backend/src/controllers/payments/PaymentReservationsController.ts @@ -1,55 +1,55 @@ -import nest from "@modules/nestjs"; import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; import { IPaymentReservation } from "@samchon/payment-api/lib/structures/payments/IPaymentReservation"; import { IPaymentSource } from "@samchon/payment-api/lib/structures/payments/IPaymentSource"; import { tags } from "typia"; import { PaymentReservationProvider } from "../../providers/payments/PaymentReservationProvider"; -@nest.Controller("payments/reservations") +@Controller("payments/reservations") export class PaymentReservationsController { - /** - * 간편 결제 수단 조회하기. - * - * @param input 간편 결제 수단의 원천 정보 + 비밀번호 - * @returns 결제 내역 - */ - @core.EncryptedRoute.Patch("get") - public async get( - @core.EncryptedBody() input: IPaymentSource.IAccessor, - ): Promise { - return PaymentReservationProvider.find({ - source_schema: input.schema, - source_table: input.table, - source_id: input.id, - })(input.password); - } + /** + * 간편 결제 수단 조회하기. + * + * @param input 간편 결제 수단의 원천 정보 + 비밀번호 + * @returns 결제 내역 + */ + @core.EncryptedRoute.Patch("get") + public async get( + @core.EncryptedBody() input: IPaymentSource.IAccessor, + ): Promise { + return PaymentReservationProvider.find({ + source_schema: input.schema, + source_table: input.table, + source_id: input.id, + })(input.password); + } - /** - * 간편 결제 수단 조회하기. - * - * @param id Primary Key - * @param input 비밀번호 - * @returns 간편 결제 수단 정보 - */ - @core.EncryptedRoute.Patch(":id") - public async at( - @core.TypedParam("id") id: string & tags.Format<"uuid">, - @core.EncryptedBody() input: IPaymentSource.IPassword, - ): Promise { - return PaymentReservationProvider.find({ id })(input.password); - } + /** + * 간편 결제 수단 조회하기. + * + * @param id Primary Key + * @param input 비밀번호 + * @returns 간편 결제 수단 정보 + */ + @core.EncryptedRoute.Patch(":id") + public async at( + @core.TypedParam("id") id: string & tags.Format<"uuid">, + @core.EncryptedBody() input: IPaymentSource.IPassword, + ): Promise { + return PaymentReservationProvider.find({ id })(input.password); + } - /** - * 간편 결제 수단 등록하기. - * - * @param input 간편 결제 수단 입력 정보 - * @returns 간편 결제 수단 정보 - */ - @core.EncryptedRoute.Post() - public async store( - @core.EncryptedBody() input: IPaymentReservation.IStore, - ): Promise { - return PaymentReservationProvider.store(input); - } + /** + * 간편 결제 수단 등록하기. + * + * @param input 간편 결제 수단 입력 정보 + * @returns 간편 결제 수단 정보 + */ + @core.EncryptedRoute.Post() + public async store( + @core.EncryptedBody() input: IPaymentReservation.IStore, + ): Promise { + return PaymentReservationProvider.store(input); + } } diff --git a/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts b/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts index 248bc1e..125a926 100644 --- a/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts +++ b/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts @@ -1,5 +1,5 @@ -import nest from "@modules/nestjs"; import core from "@nestia/core"; +import { Controller } from "@nestjs/common"; import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossPaymentWebhook } from "toss-payments-server-api/lib/structures/ITossPaymentWebhook"; @@ -8,53 +8,47 @@ import { PaymentWebhookProvider } from "../../providers/payments/PaymentWebhookP import { IamportPaymentService } from "../../services/iamport/IamportPaymentService"; import { TossPaymentService } from "../../services/toss/TossPaymentService"; -@nest.Controller("payments/webhooks") +@Controller("payments/webhooks") export class PaymentWebhooksController { - /** - * @danger - */ - @core.TypedRoute.Post("iamport") - public async iamport( - @core.TypedBody() input: IIamportPayment.IWebhook, - ): Promise { - await PaymentWebhookProvider.process("iamport")< - IIamportPayment.IWebhook, - IIamportPayment - >({ - uid: (input) => - input.status !== "ready" && input.status !== "failed" - ? input.imp_uid - : null, - fetch: (history) => - IamportPaymentService.at( - history.vendor_store_id, - history.vendor_uid, - ), - props: IamportPaymentService.parse, - })(input); - } + /** + * @danger + */ + @core.TypedRoute.Post("iamport") + public async iamport( + @core.TypedBody() input: IIamportPayment.IWebhook, + ): Promise { + await PaymentWebhookProvider.process("iamport")< + IIamportPayment.IWebhook, + IIamportPayment + >({ + uid: (input) => + input.status !== "ready" && input.status !== "failed" + ? input.imp_uid + : null, + fetch: (history) => + IamportPaymentService.at(history.vendor_store_id, history.vendor_uid), + props: IamportPaymentService.parse, + })(input); + } - /** - * @internal - */ - @core.TypedRoute.Post("toss") - public async toss( - @core.TypedBody() input: ITossPaymentWebhook, - ): Promise { - await PaymentWebhookProvider.process("toss.payments")< - ITossPaymentWebhook, - ITossPayment - >({ - uid: (input) => - input.data.status !== "WAITING_FOR_DEPOSIT" - ? input.data.paymentKey - : null, - fetch: (history) => - TossPaymentService.at( - history.vendor_store_id, - history.vendor_uid, - ), - props: TossPaymentService.parse, - })(input); - } + /** + * @internal + */ + @core.TypedRoute.Post("toss") + public async toss( + @core.TypedBody() input: ITossPaymentWebhook, + ): Promise { + await PaymentWebhookProvider.process("toss.payments")< + ITossPaymentWebhook, + ITossPayment + >({ + uid: (input) => + input.data.status !== "WAITING_FOR_DEPOSIT" + ? input.data.paymentKey + : null, + fetch: (history) => + TossPaymentService.at(history.vendor_store_id, history.vendor_uid), + props: TossPaymentService.parse, + })(input); + } } diff --git a/packages/payment-backend/src/executable/master.ts b/packages/payment-backend/src/executable/master.ts index 17a0bab..6e34076 100644 --- a/packages/payment-backend/src/executable/master.ts +++ b/packages/payment-backend/src/executable/master.ts @@ -1,9 +1,9 @@ import { PaymentUpdator } from "../PaymentUpdator"; async function main(): Promise { - await PaymentUpdator.master(); + await PaymentUpdator.master(); } main().catch((exp) => { - console.error(exp); - process.exit(-1); + console.error(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/executable/monitor.ts b/packages/payment-backend/src/executable/monitor.ts index da93cc4..cf0a153 100644 --- a/packages/payment-backend/src/executable/monitor.ts +++ b/packages/payment-backend/src/executable/monitor.ts @@ -5,26 +5,26 @@ import { IPerformance } from "../api/structures/monitors/IPerformance"; import { ISystem } from "../api/structures/monitors/ISystem"; async function main(): Promise { - // CONFIGURE MODE - if (process.argv[2]) PaymentGlobal.setMode(process.argv[2] as "local"); + // CONFIGURE MODE + if (process.argv[2]) PaymentGlobal.setMode(process.argv[2] as "local"); - // GET PERFORMANCE & SYSTEM INFO - const connection: PaymentAPI.IConnection = { - host: `http${ - PaymentGlobal.mode === "local" ? "" : "s" - }://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.API_PORT()}`, - encryption: PaymentConfiguration.ENCRYPTION_PASSWORD(), - }; - const performance: IPerformance = - await PaymentAPI.functional.monitors.performance.get(connection); - const system: ISystem = await PaymentAPI.functional.monitors.system.get( - connection, - ); + // GET PERFORMANCE & SYSTEM INFO + const connection: PaymentAPI.IConnection = { + host: `http${ + PaymentGlobal.mode === "local" ? "" : "s" + }://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.API_PORT()}`, + encryption: PaymentConfiguration.ENCRYPTION_PASSWORD(), + }; + const performance: IPerformance = + await PaymentAPI.functional.monitors.performance.get(connection); + const system: ISystem = await PaymentAPI.functional.monitors.system.get( + connection, + ); - // TRACE THEM - console.log({ performance, system }); + // TRACE THEM + console.log({ performance, system }); } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/executable/schema.ts b/packages/payment-backend/src/executable/schema.ts index 4d2cf76..e254ad9 100644 --- a/packages/payment-backend/src/executable/schema.ts +++ b/packages/payment-backend/src/executable/schema.ts @@ -4,85 +4,85 @@ import { PaymentGlobal } from "../PaymentGlobal"; import { PaymentSetupWizard } from "../PaymentSetupWizard"; async function execute( - database: string, - username: string, - password: string, - script: string, + database: string, + username: string, + password: string, + script: string, ): Promise { - try { - const prisma = new PrismaClient({ - datasources: { - db: { - url: `postgresql://${username}:${password}@${PaymentGlobal.env.PAYMENT_POSTGRES_HOST}:${PaymentGlobal.env.PAYMENT_POSTGRES_PORT}/${database}`, - }, - }, - }); - const queries: string[] = script - .split("\n") - .map((str) => str.trim()) - .filter((str) => !!str); - for (const query of queries) - try { - await prisma.$queryRawUnsafe(query); - } catch (e) { - await prisma.$disconnect(); - } + try { + const prisma = new PrismaClient({ + datasources: { + db: { + url: `postgresql://${username}:${password}@${PaymentGlobal.env.PAYMENT_POSTGRES_HOST}:${PaymentGlobal.env.PAYMENT_POSTGRES_PORT}/${database}`, + }, + }, + }); + const queries: string[] = script + .split("\n") + .map((str) => str.trim()) + .filter((str) => !!str); + for (const query of queries) + try { + await prisma.$queryRawUnsafe(query); + } catch (e) { await prisma.$disconnect(); - } catch (err) { - console.log(err); - } + } + await prisma.$disconnect(); + } catch (err) { + console.log(err); + } } async function main(): Promise { - const config = { - database: PaymentGlobal.env.PAYMENT_POSTGRES_DATABASE, - schema: PaymentGlobal.env.PAYMENT_POSTGRES_SCHEMA, - username: PaymentGlobal.env.PAYMENT_POSTGRES_USERNAME, - readonlyUsername: PaymentGlobal.env.PAYMENT_POSTGRES_USERNAME_READONLY, - password: PaymentGlobal.env.PAYMENT_POSTGRES_PASSWORD, - }; - const root = { - account: process.argv[2] ?? "postgres", - password: process.argv[3] ?? "root", - }; + const config = { + database: PaymentGlobal.env.PAYMENT_POSTGRES_DATABASE, + schema: PaymentGlobal.env.PAYMENT_POSTGRES_SCHEMA, + username: PaymentGlobal.env.PAYMENT_POSTGRES_USERNAME, + readonlyUsername: PaymentGlobal.env.PAYMENT_POSTGRES_USERNAME_READONLY, + password: PaymentGlobal.env.PAYMENT_POSTGRES_PASSWORD, + }; + const root = { + account: process.argv[2] ?? "postgres", + password: process.argv[3] ?? "root", + }; - await execute( - "postgres", - root.account, - root.password, - ` + await execute( + "postgres", + root.account, + root.password, + ` CREATE USER ${config.username} WITH ENCRYPTED PASSWORD '${config.password}'; ALTER ROLE ${config.username} WITH CREATEDB CREATE DATABASE ${config.database} OWNER ${config.username}; `, - ); + ); - await execute( - config.database, - root.account, - root.password, - ` + await execute( + config.database, + root.account, + root.password, + ` CREATE SCHEMA ${config.schema} AUTHORIZATION ${config.username}; `, - ); + ); - await execute( - config.database, - root.account, - root.password, - ` + await execute( + config.database, + root.account, + root.password, + ` GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA ${config.schema} TO ${config.username}; CREATE USER ${config.readonlyUsername} WITH ENCRYPTED PASSWORD '${config.password}'; GRANT USAGE ON SCHEMA ${config.schema} TO ${config.readonlyUsername}; GRANT SELECT ON ALL TABLES IN SCHEMA ${config.schema} TO ${config.readonlyUsername}; `, - ); + ); - PaymentGlobal.testing = true; - await PaymentSetupWizard.schema(); + PaymentGlobal.testing = true; + await PaymentSetupWizard.schema(); } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/executable/server.ts b/packages/payment-backend/src/executable/server.ts index c0b5f87..3b594cd 100644 --- a/packages/payment-backend/src/executable/server.ts +++ b/packages/payment-backend/src/executable/server.ts @@ -4,77 +4,76 @@ import { Singleton } from "tstl/thread/Singleton"; import { PaymentBackend } from "../PaymentBackend"; import { PaymentGlobal } from "../PaymentGlobal"; -import { PaymentUpdator } from "../PaymentUpdator"; -import { Scheduler } from "../schedulers/Scheduler"; +// import { PaymentUpdator } from "../PaymentUpdator"; import { ErrorUtil } from "../utils/ErrorUtil"; const EXTENSION = __filename.substr(-2); if (EXTENSION === "js") require("source-map-support/register"); const directory = new Singleton(async () => { - await mkdir(`${__dirname}/../../assets`); - await mkdir(`${__dirname}/../../assets/logs`); - await mkdir(`${__dirname}/../../assets/logs/errors`); + await mkdir(`${__dirname}/../../assets`); + await mkdir(`${__dirname}/../../assets/logs`); + await mkdir(`${__dirname}/../../assets/logs/errors`); }); function cipher(val: number): string { - if (val < 10) return "0" + val; - else return String(val); + if (val < 10) return "0" + val; + else return String(val); } async function mkdir(path: string): Promise { - try { - await fs.promises.mkdir(path); - } catch {} + try { + await fs.promises.mkdir(path); + } catch {} } async function handle_error(exp: any): Promise { - try { - const date: Date = new Date(); - const fileName: string = `${date.getFullYear()}${cipher( - date.getMonth() + 1, - )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( - date.getMinutes(), - )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; - const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); + try { + const date: Date = new Date(); + const fileName: string = `${date.getFullYear()}${cipher( + date.getMonth() + 1, + )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( + date.getMinutes(), + )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; + const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); - await directory.get(); - await fs.promises.writeFile( - `${__dirname}/../../assets/logs/errors/${fileName}.log`, - content, - "utf8", - ); - } catch {} + await directory.get(); + await fs.promises.writeFile( + `${__dirname}/../../assets/logs/errors/${fileName}.log`, + content, + "utf8", + ); + } catch {} } async function main(): Promise { - if ( - PaymentGlobal.mode === "local" && - process.argv.some((str) => str === "testing") - ) - PaymentGlobal.testing = true; + if ( + PaymentGlobal.mode === "local" && + process.argv.some((str) => str === "testing") + ) + PaymentGlobal.testing = true; - // BACKEND SEVER LATER - const backend: PaymentBackend = new PaymentBackend(); - await backend.open(); + // BACKEND SEVER LATER + const backend: PaymentBackend = new PaymentBackend(); + await backend.open(); - //---- - // POST-PROCESSES - //---- - // UNEXPECTED ERRORS - global.process.on("uncaughtException", handle_error); - global.process.on("unhandledRejection", handle_error); + //---- + // POST-PROCESSES + //---- + // UNEXPECTED ERRORS + global.process.on("uncaughtException", handle_error); + global.process.on("unhandledRejection", handle_error); - // SCHEDULER ONLY WHEN MASTER - if (PaymentGlobal.mode !== "real" || process.argv[3] === "master") { - if (PaymentGlobal.mode === "local") - try { - await PaymentUpdator.master(); - } catch {} - await Scheduler.repeat(); - } + // // SCHEDULER ONLY WHEN MASTER + // if (PaymentGlobal.mode !== "real" || process.argv[3] === "master") { + // if (PaymentGlobal.mode === "local") + // try { + // await PaymentUpdator.master(); + // } catch {} + // await Scheduler.repeat(); + // } } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/executable/update.ts b/packages/payment-backend/src/executable/update.ts index 81613ab..5849944 100644 --- a/packages/payment-backend/src/executable/update.ts +++ b/packages/payment-backend/src/executable/update.ts @@ -9,49 +9,47 @@ import api from "../api"; import { ISystem } from "../api/structures/monitors/ISystem"; async function main(): Promise { - // CONFIGURE MODE - if (process.argv[2]) - PaymentGlobal.setMode( - process.argv[2].toUpperCase() as typeof PaymentGlobal.mode, - ); - - // CONNECT TO THE UPDATOR SERVER - const connector: MutexConnector = new MutexConnector( - PaymentConfiguration.SYSTEM_PASSWORD(), - null, - ); - await connector.connect( - `ws://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.UPDATOR_PORT()}/update`, + // CONFIGURE MODE + if (process.argv[2]) + PaymentGlobal.setMode( + process.argv[2].toUpperCase() as typeof PaymentGlobal.mode, ); - // REQUEST UPDATE WITH MONOPOLYING A GLOBAL MUTEX - const mutex: RemoteMutex = await connector.getMutex("update"); - const success: boolean = await UniqueLock.try_lock(mutex, async () => { - const updator: Promisive = - connector.getDriver(); - await updator.update(); - }); - await connector.close(); + // CONNECT TO THE UPDATOR SERVER + const connector: MutexConnector = new MutexConnector( + PaymentConfiguration.SYSTEM_PASSWORD(), + null, + ); + await connector.connect( + `ws://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.UPDATOR_PORT()}/update`, + ); - // SUCCESS OR NOT - if (success === false) { - console.log("Already on updating."); - process.exit(-1); - } + // REQUEST UPDATE WITH MONOPOLYING A GLOBAL MUTEX + const mutex: RemoteMutex = await connector.getMutex("update"); + const success: boolean = await UniqueLock.try_lock(mutex, async () => { + const updator: Promisive = + connector.getDriver(); + await updator.update(); + }); + await connector.close(); - // PRINT THE COMMIT STATUS - const connection: api.IConnection = { - host: `http://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.API_PORT()}`, - encryption: PaymentConfiguration.ENCRYPTION_PASSWORD(), - }; - const system: ISystem = await api.functional.monitors.system.get( - connection, - ); - console.log("branch", system.arguments[2], system.commit.branch); - console.log("hash", system.commit.hash); - console.log("commit-time", system.commit.commited_at); + // SUCCESS OR NOT + if (success === false) { + console.log("Already on updating."); + process.exit(-1); + } + + // PRINT THE COMMIT STATUS + const connection: api.IConnection = { + host: `http://${PaymentConfiguration.MASTER_IP()}:${PaymentConfiguration.API_PORT()}`, + encryption: PaymentConfiguration.ENCRYPTION_PASSWORD(), + }; + const system: ISystem = await api.functional.monitors.system.get(connection); + console.log("branch", system.arguments[2], system.commit.branch); + console.log("hash", system.commit.hash); + console.log("commit-time", system.commit.commited_at); } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/executable/updator-master.ts b/packages/payment-backend/src/executable/updator-master.ts index 21465b9..37fe3f1 100644 --- a/packages/payment-backend/src/executable/updator-master.ts +++ b/packages/payment-backend/src/executable/updator-master.ts @@ -1,10 +1,10 @@ import { PaymentUpdator } from "../PaymentUpdator"; async function main(): Promise { - await PaymentUpdator.master(); - await PaymentUpdator.slave("127.0.0.1"); + await PaymentUpdator.master(); + await PaymentUpdator.slave("127.0.0.1"); } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/executable/updator-slave.ts b/packages/payment-backend/src/executable/updator-slave.ts index 1ec724e..5042204 100644 --- a/packages/payment-backend/src/executable/updator-slave.ts +++ b/packages/payment-backend/src/executable/updator-slave.ts @@ -1,9 +1,9 @@ import { PaymentUpdator } from "../PaymentUpdator"; async function main(): Promise { - await PaymentUpdator.slave(); + await PaymentUpdator.slave(); } main().catch((exp) => { - console.error(exp); - process.exit(-1); + console.error(exp); + process.exit(-1); }); diff --git a/packages/payment-backend/src/modules/express.ts b/packages/payment-backend/src/modules/express.ts deleted file mode 100644 index 3a67624..0000000 --- a/packages/payment-backend/src/modules/express.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as express from "express"; - -export default express; diff --git a/packages/payment-backend/src/modules/nestjs.ts b/packages/payment-backend/src/modules/nestjs.ts deleted file mode 100644 index b375de2..0000000 --- a/packages/payment-backend/src/modules/nestjs.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as nest from "@nestjs/common"; - -export default nest; diff --git a/packages/payment-backend/src/providers/monitors/SystemProvider.ts b/packages/payment-backend/src/providers/monitors/SystemProvider.ts index b606ee9..784bdee 100644 --- a/packages/payment-backend/src/providers/monitors/SystemProvider.ts +++ b/packages/payment-backend/src/providers/monitors/SystemProvider.ts @@ -7,47 +7,47 @@ import { ISystem } from "../../api/structures/monitors/ISystem"; import { DateUtil } from "../../utils/DateUtil"; export class SystemProvider { - public static readonly uid: string = v4(); - public static readonly created_at: Date = new Date(); + public static readonly uid: string = v4(); + public static readonly created_at: Date = new Date(); - public static package(): Promise { - return package_.get(); - } + public static package(): Promise { + return package_.get(); + } - public static commit(): Promise { - return commit_.get(); - } + public static commit(): Promise { + return commit_.get(); + } } // LOAD COMMITS & PACKAGES const commit_: Singleton> = new Singleton( - () => - new Promise((resolve, reject) => { - git.getLastCommit((err, commit) => { - if (err) reject(err); - else - resolve({ - ...commit, - authored_at: DateUtil.to_string( - new Date(Number(commit.authoredOn) * 1000), - true, - ), - commited_at: DateUtil.to_string( - new Date(Number(commit.committedOn) * 1000), - true, - ), - }); - }); - }), + () => + new Promise((resolve, reject) => { + git.getLastCommit((err, commit) => { + if (err) reject(err); + else + resolve({ + ...commit, + authored_at: DateUtil.to_string( + new Date(Number(commit.authoredOn) * 1000), + true, + ), + commited_at: DateUtil.to_string( + new Date(Number(commit.committedOn) * 1000), + true, + ), + }); + }); + }), ); const package_: Singleton> = new Singleton( - async () => { - const content: string = await fs.promises.readFile( - `${__dirname}/../../../../package.json`, - "utf8", - ); - return JSON.parse(content); - }, + async () => { + const content: string = await fs.promises.readFile( + `${__dirname}/../../../../package.json`, + "utf8", + ); + return JSON.parse(content); + }, ); commit_.get().catch(() => {}); diff --git a/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts b/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts index 3995ef3..9b21026 100644 --- a/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts +++ b/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts @@ -2,5 +2,5 @@ import { IPaymentWebhookHistory } from "@samchon/payment-api/lib/structures/paym import { Vector } from "tstl/container/Vector"; export namespace FakePaymentStorage { - export const webhooks: Vector = new Vector(); + export const webhooks: Vector = new Vector(); } diff --git a/packages/payment-backend/src/providers/payments/PaymentCancelHistoryProvider.ts b/packages/payment-backend/src/providers/payments/PaymentCancelHistoryProvider.ts index 78c4af1..1faf3bd 100644 --- a/packages/payment-backend/src/providers/payments/PaymentCancelHistoryProvider.ts +++ b/packages/payment-backend/src/providers/payments/PaymentCancelHistoryProvider.ts @@ -1,5 +1,5 @@ -import nest from "@modules/nestjs"; import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { BadRequestException } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { IEntity } from "@samchon/payment-api/lib/structures/common/IEntity"; import { IPaymentCancelHistory } from "@samchon/payment-api/lib/structures/payments/IPaymentCancelHistory"; @@ -14,91 +14,90 @@ import { TossPaymentService } from "../../services/toss/TossPaymentService"; import { PaymentHistoryProvider } from "./PaymentHistoryProvider"; export namespace PaymentCancelHistoryProvider { - export namespace json { - export const transform = ( - input: Prisma.payment_history_cancelsGetPayload< - ReturnType - >, - ): IPaymentCancelHistory & { time: Date } => ({ - price: input.amount, - reason: input.reason, - created_at: input.created_at.toISOString(), - time: input.created_at, - }); - export const select = () => - Prisma.validator< - | Prisma.payment_history_cancelsFindFirstArgs - | Prisma.payment_history_cancelsFindManyArgs - >()({}); - } + export namespace json { + export const transform = ( + input: Prisma.payment_history_cancelsGetPayload< + ReturnType + >, + ): IPaymentCancelHistory & { time: Date } => ({ + price: input.amount, + reason: input.reason, + created_at: input.created_at.toISOString(), + time: input.created_at, + }); + export const select = () => + Prisma.validator< + | Prisma.payment_history_cancelsFindFirstArgs + | Prisma.payment_history_cancelsFindManyArgs + >()({}); + } - export const store = async ( - input: IPaymentCancelHistory.IStore, - ): Promise => { - const history: IPaymentHistory = await PaymentHistoryProvider.find({ - source_schema: input.source.schema, - source_table: input.source.table, - source_id: input.source.id, - })(input.password); - const props: IPaymentHistory.IProps = await request(history)(input); - return PaymentHistoryProvider.update(history)(props); - }; + export const store = async ( + input: IPaymentCancelHistory.IStore, + ): Promise => { + const history: IPaymentHistory = await PaymentHistoryProvider.find({ + source_schema: input.source.schema, + source_table: input.source.table, + source_id: input.source.id, + })(input.password); + const props: IPaymentHistory.IProps = await request(history)(input); + return PaymentHistoryProvider.update(history)(props); + }; + + export const collect = + (history: IEntity) => + ( + input: IPaymentCancelHistory.IProps, + ): Prisma.payment_history_cancelsCreateManyInput => ({ + id: v4(), + payment_history_id: history.id, + amount: input.price, + reason: input.reason, + data: encrypt(JSON.stringify(input.data)), + created_at: input.created_at, + }); - export const collect = - (history: IEntity) => - ( - input: IPaymentCancelHistory.IProps, - ): Prisma.payment_history_cancelsCreateManyInput => ({ - id: v4(), - payment_history_id: history.id, + const request = + (history: IPaymentHistory) => + async ( + input: IPaymentCancelHistory.IStore, + ): Promise => { + if (history.vendor_code === "iamport") { + const payment: IIamportPayment = await IamportPaymentService.cancel( + history.vendor.store_id, + { + imp_uid: history.vendor.uid, + merchant_uid: history.source.id, amount: input.price, reason: input.reason, - data: encrypt(JSON.stringify(input.data)), - created_at: input.created_at, - }); - - const request = - (history: IPaymentHistory) => - async ( - input: IPaymentCancelHistory.IStore, - ): Promise => { - if (history.vendor_code === "iamport") { - const payment: IIamportPayment = - await IamportPaymentService.cancel( - history.vendor.store_id, - { - imp_uid: history.vendor.uid, - merchant_uid: history.source.id, - amount: input.price, - reason: input.reason, - checksum: null, - refund_bank: input.account?.bank, - refund_account: input.account?.account, - refund_holder: input.account?.holder, - refund_tel: input.account?.mobile, - }, - ); - return IamportPaymentService.parse(payment); - } else if (history.vendor_code === "toss.payments") { - const payment: ITossPayment = await TossPaymentService.cancel( - history.vendor.store_id, - { - paymentKey: history.vendor.uid, - cancelReason: input.reason, - cancelAmount: input.price, - refundReceiveAccount: input.account - ? { - bank: input.account.bank, - accountNumber: input.account.account, - holderName: input.account.holder, - } - : undefined, - }, - ); - return TossPaymentService.parse(payment); - } - throw new nest.BadRequestException(`Unknown vendor.`); - }; + checksum: null, + refund_bank: input.account?.bank, + refund_account: input.account?.account, + refund_holder: input.account?.holder, + refund_tel: input.account?.mobile, + }, + ); + return IamportPaymentService.parse(payment); + } else if (history.vendor_code === "toss.payments") { + const payment: ITossPayment = await TossPaymentService.cancel( + history.vendor.store_id, + { + paymentKey: history.vendor.uid, + cancelReason: input.reason, + cancelAmount: input.price, + refundReceiveAccount: input.account + ? { + bank: input.account.bank, + accountNumber: input.account.account, + holderName: input.account.holder, + } + : undefined, + }, + ); + return TossPaymentService.parse(payment); + } + throw new BadRequestException(`Unknown vendor.`); + }; } const encrypt = (value: string) => AesPkcs5.encrypt(value, KEY(), IV()); diff --git a/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts b/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts index e0c1d12..2200fdf 100644 --- a/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts +++ b/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts @@ -1,6 +1,6 @@ -import nest from "@modules/nestjs"; -import "@nestia/fetcher"; import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import "@nestia/fetcher/lib/PlainFetcher"; +import { BadRequestException, ForbiddenException } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { IEntity } from "@samchon/payment-api/lib/structures/common/IEntity"; import { IPaymentHistory } from "@samchon/payment-api/lib/structures/payments/IPaymentHistory"; @@ -17,189 +17,182 @@ import { BcryptUtil } from "../../utils/BcryptUtil"; import { PaymentCancelHistoryProvider } from "./PaymentCancelHistoryProvider"; export namespace PaymentHistoryProvider { - export namespace json { - export const transform = ( - history: Prisma.payment_historiesGetPayload< - ReturnType - >, - ): IPaymentHistory => ({ - id: history.id, - vendor_code: history.vendor_code as "iamport", - vendor: { - code: history.vendor_code as "iamport", - uid: history.vendor_uid, - store_id: history.vendor_store_id, - }, - source: { - schema: history.source_schema, - table: history.source_table, - id: history.source_id, - }, - cancels: history.cancels - .map(PaymentCancelHistoryProvider.json.transform) - .sort((a, b) => a.time.getTime() - b.time.getTime()), - currency: history.currency, - price: history.price, - refund: history.refund !== 0 ? history.refund : null, - data: JSON.parse(decrypt(history.data)), - webhook_url: history.webhook_url ?? null, - created_at: history.created_at.toString(), - paid_at: - history.paid_at !== null ? history.paid_at.toString() : null, - cancelled_at: - history.cancelled_at !== null - ? history.cancelled_at.toString() - : null, - }); + export namespace json { + export const transform = ( + history: Prisma.payment_historiesGetPayload>, + ): IPaymentHistory => ({ + id: history.id, + vendor_code: history.vendor_code as "iamport", + vendor: { + code: history.vendor_code as "iamport", + uid: history.vendor_uid, + store_id: history.vendor_store_id, + }, + source: { + schema: history.source_schema, + table: history.source_table, + id: history.source_id, + }, + cancels: history.cancels + .map(PaymentCancelHistoryProvider.json.transform) + .sort((a, b) => a.time.getTime() - b.time.getTime()), + currency: history.currency, + price: history.price, + refund: history.refund !== 0 ? history.refund : null, + data: JSON.parse(decrypt(history.data)), + webhook_url: history.webhook_url ?? null, + created_at: history.created_at.toString(), + paid_at: history.paid_at !== null ? history.paid_at.toString() : null, + cancelled_at: + history.cancelled_at !== null ? history.cancelled_at.toString() : null, + }); - export const select = () => - Prisma.validator< - | Prisma.payment_historiesFindFirstArgs - | Prisma.payment_historiesFindManyArgs - >()({ - include: { - cancels: PaymentCancelHistoryProvider.json.select(), - }, - }); - } + export const select = () => + Prisma.validator< + | Prisma.payment_historiesFindFirstArgs + | Prisma.payment_historiesFindManyArgs + >()({ + include: { + cancels: PaymentCancelHistoryProvider.json.select(), + }, + }); + } - export const find = - (where: Prisma.payment_historiesWhereInput) => - async (password: string): Promise => { - const history = - await PaymentGlobal.prisma.payment_histories.findFirstOrThrow({ - where, - ...json.select(), - }); - if ( - !(await BcryptUtil.equals({ - input: password, - hashed: history.password, - })) - ) - throw new nest.ForbiddenException("Wrong password."); - return json.transform(history); - }; + export const find = + (where: Prisma.payment_historiesWhereInput) => + async (password: string): Promise => { + const history = + await PaymentGlobal.prisma.payment_histories.findFirstOrThrow({ + where, + ...json.select(), + }); + if ( + !(await BcryptUtil.equals({ + input: password, + hashed: history.password, + })) + ) + throw new ForbiddenException("Wrong password."); + return json.transform(history); + }; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- WEBHOOK ----------------------------------------------------------- */ - export async function webhook( - history: Prisma.payment_historiesGetPayload<{}> | IPaymentHistory, - input: IPaymentWebhookHistory, - ): Promise { - if (history.webhook_url === null) - throw new Error( - "Error on PaymentHistoryProvider.webhook(): no webhook_url.", - ); - const response: Response = await fetch(history.webhook_url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(input), - }); - if (response.status !== 200 && response.status !== 201) - throw new HttpError( - "POST", - history.webhook_url, - response.status, - {}, - await response.text(), - ); - } + export async function webhook( + history: Prisma.payment_historiesGetPayload<{}> | IPaymentHistory, + input: IPaymentWebhookHistory, + ): Promise { + if (history.webhook_url === null) + throw new Error( + "Error on PaymentHistoryProvider.webhook(): no webhook_url.", + ); + const response: Response = await fetch(history.webhook_url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(input), + }); + if (response.status !== 200 && response.status !== 201) + throw new HttpError( + "POST", + history.webhook_url, + response.status, + {}, + await response.text(), + ); + } - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- STORE ----------------------------------------------------------- */ - export async function store( - input: IPaymentHistory.IStore, - ): Promise { - const props = await approve(input); - const history = await PaymentGlobal.prisma.payment_histories.create({ - data: { - id: v4(), - // VENDOR - vendor_code: input.vendor.code, - vendor_store_id: input.vendor.store_id, - vendor_uid: input.vendor.uid, - // SOURCE - source_schema: input.source.schema, - source_table: input.source.table, - source_id: input.source.id, - password: await BcryptUtil.hash(input.password), - // PAYMENT - data: encrypt(JSON.stringify(props.data)), - webhook_url: input.webhook_url, - currency: props.currency, - price: props.price, - refund: props.refund, - created_at: new Date(), - paid_at: props.paid_at, - cancelled_at: props.cancelled_at, - // CANCELS - }, - ...json.select(), - }); - return json.transform(history); - } + export async function store( + input: IPaymentHistory.IStore, + ): Promise { + const props = await approve(input); + const history = await PaymentGlobal.prisma.payment_histories.create({ + data: { + id: v4(), + // VENDOR + vendor_code: input.vendor.code, + vendor_store_id: input.vendor.store_id, + vendor_uid: input.vendor.uid, + // SOURCE + source_schema: input.source.schema, + source_table: input.source.table, + source_id: input.source.id, + password: await BcryptUtil.hash(input.password), + // PAYMENT + data: encrypt(JSON.stringify(props.data)), + webhook_url: input.webhook_url, + currency: props.currency, + price: props.price, + refund: props.refund, + created_at: new Date(), + paid_at: props.paid_at, + cancelled_at: props.cancelled_at, + // CANCELS + }, + ...json.select(), + }); + return json.transform(history); + } - export const update = - (history: IEntity) => - async (input: IPaymentHistory.IProps): Promise => { - // RE-CONSTRUCT CANCEL HISTORIES - await PaymentGlobal.prisma.payment_history_cancels.createMany({ - data: input.cancels.map( - PaymentCancelHistoryProvider.collect(history), - ), - skipDuplicates: true, - }); + export const update = + (history: IEntity) => + async (input: IPaymentHistory.IProps): Promise => { + // RE-CONSTRUCT CANCEL HISTORIES + await PaymentGlobal.prisma.payment_history_cancels.createMany({ + data: input.cancels.map(PaymentCancelHistoryProvider.collect(history)), + skipDuplicates: true, + }); - // UPDATE HISTORY - await PaymentGlobal.prisma.payment_histories.update({ - where: { id: history.id }, - data: { - currency: input.currency, - price: input.price, - refund: input.refund, - paid_at: input.paid_at, - cancelled_at: input.cancelled_at, - data: encrypt(JSON.stringify(input.data)), - }, - ...json.select(), - }); - const record = - await PaymentGlobal.prisma.payment_histories.findFirstOrThrow({ - where: { id: history.id }, - ...json.select(), - }); - return json.transform(record); - }; + // UPDATE HISTORY + await PaymentGlobal.prisma.payment_histories.update({ + where: { id: history.id }, + data: { + currency: input.currency, + price: input.price, + refund: input.refund, + paid_at: input.paid_at, + cancelled_at: input.cancelled_at, + data: encrypt(JSON.stringify(input.data)), + }, + ...json.select(), + }); + const record = + await PaymentGlobal.prisma.payment_histories.findFirstOrThrow({ + where: { id: history.id }, + ...json.select(), + }); + return json.transform(record); + }; - async function approve( - input: IPaymentHistory.IStore, - ): Promise { - if (input.vendor.code === "iamport") { - const data: IIamportPayment = await IamportPaymentService.approve( - input.vendor.store_id, - input.vendor.uid, - input.source.id, - input.price, - ); - return IamportPaymentService.parse(data); - } else if (input.vendor.code === "toss.payments") { - const data: ITossPayment = await TossPaymentService.approve( - input.vendor.store_id, - input.vendor.uid, - { - orderId: input.source.id, - amount: input.price, - }, - ); - return TossPaymentService.parse(data); - } - throw new nest.BadRequestException(`Unknown vendor.`); + async function approve( + input: IPaymentHistory.IStore, + ): Promise { + if (input.vendor.code === "iamport") { + const data: IIamportPayment = await IamportPaymentService.approve( + input.vendor.store_id, + input.vendor.uid, + input.source.id, + input.price, + ); + return IamportPaymentService.parse(data); + } else if (input.vendor.code === "toss.payments") { + const data: ITossPayment = await TossPaymentService.approve( + input.vendor.store_id, + input.vendor.uid, + { + orderId: input.source.id, + amount: input.price, + }, + ); + return TossPaymentService.parse(data); } + throw new BadRequestException(`Unknown vendor.`); + } } const encrypt = (value: string) => AesPkcs5.encrypt(value, KEY(), IV()); diff --git a/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts b/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts index 92ab09d..af064b5 100644 --- a/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts +++ b/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts @@ -1,5 +1,5 @@ -import nest from "@modules/nestjs"; import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { ForbiddenException } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { IPaymentReservation } from "@samchon/payment-api/lib/structures/payments/IPaymentReservation"; import imp from "iamport-server-api"; @@ -14,103 +14,101 @@ import { TossAsset } from "../../services/toss/TossAsset"; import { BcryptUtil } from "../../utils/BcryptUtil"; export namespace PaymentReservationProvider { - export namespace json { - export const transform = ( - reservation: Prisma.payment_reservationsGetPayload< - ReturnType - >, - ): IPaymentReservation => ({ - id: reservation.id, - vendor_code: reservation.vendor_code as "iamport", - vendor: { - code: reservation.vendor_code as "iamport", - store_id: reservation.vendor_store_id, - uid: reservation.vendor_uid, - }, - source: { - schema: reservation.source_schema, - table: reservation.source_table, - id: reservation.source_id, - }, - title: reservation.title, - data: JSON.parse(decrypt(reservation.data)), - created_at: reservation.created_at.toString(), - }); - - export const select = () => - Prisma.validator< - | Prisma.payment_reservationsFindFirstArgs - | Prisma.payment_reservationsFindManyArgs - >()({}); - } + export namespace json { + export const transform = ( + reservation: Prisma.payment_reservationsGetPayload< + ReturnType + >, + ): IPaymentReservation => ({ + id: reservation.id, + vendor_code: reservation.vendor_code as "iamport", + vendor: { + code: reservation.vendor_code as "iamport", + store_id: reservation.vendor_store_id, + uid: reservation.vendor_uid, + }, + source: { + schema: reservation.source_schema, + table: reservation.source_table, + id: reservation.source_id, + }, + title: reservation.title, + data: JSON.parse(decrypt(reservation.data)), + created_at: reservation.created_at.toString(), + }); - export const find = - (where: Prisma.payment_reservationsWhereInput) => - async (password: string): Promise => { - const reservation = - await PaymentGlobal.prisma.payment_reservations.findFirstOrThrow( - { - where, - ...json.select(), - }, - ); - if ( - !(await BcryptUtil.equals({ - input: password, - hashed: reservation.password, - })) - ) - throw new nest.ForbiddenException("Wrong password."); - return json.transform(reservation); - }; + export const select = () => + Prisma.validator< + | Prisma.payment_reservationsFindFirstArgs + | Prisma.payment_reservationsFindManyArgs + >()({}); + } - export async function store( - input: IPaymentReservation.IStore, - ): Promise { - const data = - input.vendor.code === "toss.payments" - ? await get_toss_billing(input) - : await get_iamport_subscription(input); - const record = await PaymentGlobal.prisma.payment_reservations.create({ - data: { - id: v4(), - vendor_code: input.vendor.code, - vendor_store_id: input.vendor.store_id, - vendor_uid: input.vendor.uid, - source_schema: input.source.schema, - source_table: input.source.table, - source_id: input.source.id, - password: await BcryptUtil.hash(input.password), - title: input.title, - data: encrypt(JSON.stringify(data)), - created_at: new Date(), - }, - ...json.select(), + export const find = + (where: Prisma.payment_reservationsWhereInput) => + async (password: string): Promise => { + const reservation = + await PaymentGlobal.prisma.payment_reservations.findFirstOrThrow({ + where, + ...json.select(), }); - return json.transform(record); - } + if ( + !(await BcryptUtil.equals({ + input: password, + hashed: reservation.password, + })) + ) + throw new ForbiddenException("Wrong password."); + return json.transform(reservation); + }; + + export async function store( + input: IPaymentReservation.IStore, + ): Promise { + const data = + input.vendor.code === "toss.payments" + ? await get_toss_billing(input) + : await get_iamport_subscription(input); + const record = await PaymentGlobal.prisma.payment_reservations.create({ + data: { + id: v4(), + vendor_code: input.vendor.code, + vendor_store_id: input.vendor.store_id, + vendor_uid: input.vendor.uid, + source_schema: input.source.schema, + source_table: input.source.table, + source_id: input.source.id, + password: await BcryptUtil.hash(input.password), + title: input.title, + data: encrypt(JSON.stringify(data)), + created_at: new Date(), + }, + ...json.select(), + }); + return json.transform(record); + } - async function get_iamport_subscription( - input: IPaymentReservation.IStore, - ): Promise { - const { response } = await imp.functional.subscribe.customers.at( - await IamportAsset.connection(input.vendor.store_id), - input.vendor.uid, - ); - return response; - } + async function get_iamport_subscription( + input: IPaymentReservation.IStore, + ): Promise { + const { response } = await imp.functional.subscribe.customers.at( + await IamportAsset.connection(input.vendor.store_id), + input.vendor.uid, + ); + return response; + } - async function get_toss_billing( - input: IPaymentReservation.IStore, - ): Promise { - return toss.functional.v1.billing.authorizations.at( - await TossAsset.connection(input.vendor.store_id), - input.vendor.uid, - { - customerKey: input.source.id, - }, - ); - } + async function get_toss_billing( + input: IPaymentReservation.IStore, + ): Promise { + return toss.functional.v1.billing.authorizations.at( + await TossAsset.connection(input.vendor.store_id), + input.vendor.uid, + { + customerKey: input.source.id, + }, + ); + } } const encrypt = (value: string) => AesPkcs5.encrypt(value, KEY(), IV()); diff --git a/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts b/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts index aa13f8c..62aa611 100644 --- a/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts +++ b/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts @@ -11,103 +11,104 @@ import { PaymentGlobal } from "../../PaymentGlobal"; import { PaymentHistoryProvider } from "./PaymentHistoryProvider"; export namespace PaymentWebhookProvider { - export const process = - (vendor: "iamport" | "toss.payments") => - (config: { - uid: (input: Input) => string | null; - fetch: (history: payment_histories) => Promise; - props: (data: Data) => IPaymentHistory.IProps; - }) => - async (input: Input): Promise => { - // NEED NOT TO DO ANYIHTNG - const vendor_uid: string | null = config.uid(input); - if (vendor_uid === null) return; + export const process = + (vendor: "iamport" | "toss.payments") => + (config: { + uid: (input: Input) => string | null; + fetch: (history: payment_histories) => Promise; + props: (data: Data) => IPaymentHistory.IProps; + }) => + async (input: Input): Promise => { + // NEED NOT TO DO ANYIHTNG + const vendor_uid: string | null = config.uid(input); + if (vendor_uid === null) return; - // GET PREVIOUS HISTORY - const record = - await PaymentGlobal.prisma.payment_histories.findFirstOrThrow({ - where: { - vendor_code: vendor, - vendor_uid: vendor_uid, - }, - ...PaymentHistoryProvider.json.select(), - }); - const previous: IPaymentHistory = - PaymentHistoryProvider.json.transform(record); - if (previous.vendor.code !== vendor) { - throw new InvalidArgument( - `Vendor of the payment is not "${vendor}" but "${record.vendor_code}""`, - ); - } + // GET PREVIOUS HISTORY + const record = + await PaymentGlobal.prisma.payment_histories.findFirstOrThrow({ + where: { + vendor_code: vendor, + vendor_uid: vendor_uid, + }, + ...PaymentHistoryProvider.json.select(), + }); + const previous: IPaymentHistory = + PaymentHistoryProvider.json.transform(record); + if (previous.vendor.code !== vendor) { + throw new InvalidArgument( + `Vendor of the payment is not "${vendor}" but "${record.vendor_code}""`, + ); + } - // UPDATE HISTORY - const data: Data = await config.fetch(record); - const props: IPaymentHistory.IProps = config.props(data); - const current: IPaymentHistory = - await PaymentHistoryProvider.update(previous)(props); + // UPDATE HISTORY + const data: Data = await config.fetch(record); + const props: IPaymentHistory.IProps = config.props(data); + const current: IPaymentHistory = await PaymentHistoryProvider.update( + previous, + )(props); - // DO WEBHOOK - const webhook: payment_history_webhooks = - await PaymentGlobal.prisma.payment_history_webhooks.create({ - data: { - id: v4(), - history: { - connect: { - id: current.id, - }, - }, - previous: JSON.stringify(previous), - current: JSON.stringify(current), - data: JSON.stringify(input), - created_at: new Date(), - }, - }); - const request: IPaymentWebhookHistory = { - id: webhook.id, - source: current.source, - previous: previous, - current: current, - }; - send(current, webhook, request).catch(() => {}); - }; + // DO WEBHOOK + const webhook: payment_history_webhooks = + await PaymentGlobal.prisma.payment_history_webhooks.create({ + data: { + id: v4(), + history: { + connect: { + id: current.id, + }, + }, + previous: JSON.stringify(previous), + current: JSON.stringify(current), + data: JSON.stringify(input), + created_at: new Date(), + }, + }); + const request: IPaymentWebhookHistory = { + id: webhook.id, + source: current.source, + previous: previous, + current: current, + }; + send(current, webhook, request).catch(() => {}); + }; - async function send( - history: IPaymentHistory, - webhook: payment_history_webhooks, - request: IPaymentWebhookHistory, - ): Promise { - let status: number | null = null; - let body: string | null = null; + async function send( + history: IPaymentHistory, + webhook: payment_history_webhooks, + request: IPaymentWebhookHistory, + ): Promise { + let status: number | null = null; + let body: string | null = null; - const encryption = PaymentConfiguration.ENCRYPTION_PASSWORD(); - try { - const response: Response = await fetch(history.webhook_url!, { - method: "POST", - headers: { - "Content-Type": "text/plain", - }, - body: AesPkcs5.encrypt( - JSON.stringify(request), - encryption.key, - encryption.iv, - ), - }); - status = response.status; - body = await response.text(); - } catch {} + const encryption = PaymentConfiguration.ENCRYPTION_PASSWORD(); + try { + const response: Response = await fetch(history.webhook_url!, { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: AesPkcs5.encrypt( + JSON.stringify(request), + encryption.key, + encryption.iv, + ), + }); + status = response.status; + body = await response.text(); + } catch {} - await PaymentGlobal.prisma.payment_history_webhook_responses.create({ - data: { - id: v4(), - webhook: { - connect: { - id: webhook.id, - }, - }, - status: status, - body: body, - created_at: new Date(), - }, - }); - } + await PaymentGlobal.prisma.payment_history_webhook_responses.create({ + data: { + id: v4(), + webhook: { + connect: { + id: webhook.id, + }, + }, + status: status, + body: body, + created_at: new Date(), + }, + }); + } } diff --git a/packages/payment-backend/src/schedulers/Scheduler.ts b/packages/payment-backend/src/schedulers/Scheduler.ts deleted file mode 100644 index 4ccc9b1..0000000 --- a/packages/payment-backend/src/schedulers/Scheduler.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DynamicExecutor } from "@nestia/e2e"; -import { MutexConnector, RemoteMutex } from "mutex-server"; -import { sleep_for } from "tstl/thread/global"; - -import { PaymentGlobal } from "../PaymentGlobal"; - -export namespace Scheduler { - export async function repeat(): Promise { - const critical: MutexConnector = - await PaymentGlobal.critical.get(); - const mutex: RemoteMutex = await critical.getMutex("scheduler"); - - if ((await mutex.try_lock()) === false) return; - - let time: number = 0; - while (true) { - const now: number = Date.now(); - const interval: number = now - time; - time = now; - - await DynamicExecutor.assert({ - prefix: "schedule", - parameters: () => [interval], - })(__dirname + "/features"); - await sleep_for(INTERVAL); - } - } -} - -const INTERVAL = 60_000; diff --git a/packages/payment-backend/src/schedulers/features/schedule_something_in_every_day.ts b/packages/payment-backend/src/schedulers/features/schedule_something_in_every_day.ts deleted file mode 100644 index 88c1668..0000000 --- a/packages/payment-backend/src/schedulers/features/schedule_something_in_every_day.ts +++ /dev/null @@ -1,9 +0,0 @@ -export async function schedule_something_in_every_day( - interval: number, -): Promise { - if (interval < ONE_DAY) return; - - // DO SOMETHING -} - -const ONE_DAY = 24 * 60 * 60 * 1000; diff --git a/packages/payment-backend/src/schedulers/features/schedule_something_in_every_hour.ts b/packages/payment-backend/src/schedulers/features/schedule_something_in_every_hour.ts deleted file mode 100644 index ca63c5a..0000000 --- a/packages/payment-backend/src/schedulers/features/schedule_something_in_every_hour.ts +++ /dev/null @@ -1,9 +0,0 @@ -export async function schedule_something_in_every_hour( - interval: number, -): Promise { - if (interval < ONE_HOUR) return; - - // DO SOMETHING -} - -const ONE_HOUR = 60 * 60 * 1000; diff --git a/packages/payment-backend/src/schedulers/features/schedule_something_in_every_minutes.ts b/packages/payment-backend/src/schedulers/features/schedule_something_in_every_minutes.ts deleted file mode 100644 index 55f10f3..0000000 --- a/packages/payment-backend/src/schedulers/features/schedule_something_in_every_minutes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function schedule_something_in_every_minutes(): Promise { - // DO SOMETHING -} diff --git a/packages/payment-backend/src/services/iamport/IamportAsset.ts b/packages/payment-backend/src/services/iamport/IamportAsset.ts index 410002e..0136e52 100644 --- a/packages/payment-backend/src/services/iamport/IamportAsset.ts +++ b/packages/payment-backend/src/services/iamport/IamportAsset.ts @@ -6,33 +6,31 @@ import { PaymentConfiguration } from "../../PaymentConfiguration"; import { PaymentGlobal } from "../../PaymentGlobal"; export namespace IamportAsset { - export async function connection( - storeId: string, - ): Promise { - const connector: imp.IamportConnector = await singleton.get( - PaymentGlobal.mode, - PaymentGlobal.testing, - storeId, - ); - return await connector.get(); - } + export async function connection(storeId: string): Promise { + const connector: imp.IamportConnector = await singleton.get( + PaymentGlobal.mode, + PaymentGlobal.testing, + storeId, + ); + return await connector.get(); + } - const singleton: VariadicSingleton< - Promise, - [typeof PaymentGlobal.mode, boolean, string] - > = new VariadicSingleton(async (_mode, testing, storeId) => { - if (testing === true) - return new imp.IamportConnector( - `http://127.0.0.1:${fake.FakeIamportConfiguration.API_PORT}`, - { - imp_key: "test_imp_key", - imp_secret: "test_imp_secret", - }, - ); - else - return new imp.IamportConnector( - "https://api.iamport.kr", - PaymentConfiguration.IAMPORT_USER_ACCESSOR(storeId), - ); - }); + const singleton: VariadicSingleton< + Promise, + [typeof PaymentGlobal.mode, boolean, string] + > = new VariadicSingleton(async (_mode, testing, storeId) => { + if (testing === true) + return new imp.IamportConnector( + `http://127.0.0.1:${fake.FakeIamportConfiguration.API_PORT}`, + { + imp_key: "test_imp_key", + imp_secret: "test_imp_secret", + }, + ); + else + return new imp.IamportConnector( + "https://api.iamport.kr", + PaymentConfiguration.IAMPORT_USER_ACCESSOR(storeId), + ); + }); } diff --git a/packages/payment-backend/src/services/iamport/IamportPaymentService.ts b/packages/payment-backend/src/services/iamport/IamportPaymentService.ts index cfcb256..7f7a079 100644 --- a/packages/payment-backend/src/services/iamport/IamportPaymentService.ts +++ b/packages/payment-backend/src/services/iamport/IamportPaymentService.ts @@ -8,75 +8,75 @@ import { ErrorUtil } from "../../utils/ErrorUtil"; import { IamportAsset } from "./IamportAsset"; export namespace IamportPaymentService { - export async function at( - storeId: string, - imp_uid: string, - ): Promise { - const output = await imp.functional.payments.at( - await IamportAsset.connection(storeId), - imp_uid, - {}, - ); - return output.response; - } + export async function at( + storeId: string, + imp_uid: string, + ): Promise { + const output = await imp.functional.payments.at( + await IamportAsset.connection(storeId), + imp_uid, + {}, + ); + return output.response; + } - export async function approve( - storeId: string, - imp_uid: string, - merchant_uid: string, - amount: number, - ): Promise { - const payment: IIamportPayment = await IamportPaymentService.at( - storeId, - imp_uid, - ); - if (amount !== payment.amount) { - await ErrorUtil.log("IamportPaymentService.approve()", { - ...payment, - storeId, - imp_uid, - amount, - }); - await cancel(storeId, { - imp_uid, - reason: "잘못된 금액을 결제함", - merchant_uid, - checksum: null, - amount, - }); - throw new DomainError( - `IamportPaymentService.approve(): wrong paid amount. It must be not ${amount} but ${payment.amount}.`, - ); - } - return payment; + export async function approve( + storeId: string, + imp_uid: string, + merchant_uid: string, + amount: number, + ): Promise { + const payment: IIamportPayment = await IamportPaymentService.at( + storeId, + imp_uid, + ); + if (amount !== payment.amount) { + await ErrorUtil.log("IamportPaymentService.approve()", { + ...payment, + storeId, + imp_uid, + amount, + }); + await cancel(storeId, { + imp_uid, + reason: "잘못된 금액을 결제함", + merchant_uid, + checksum: null, + amount, + }); + throw new DomainError( + `IamportPaymentService.approve(): wrong paid amount. It must be not ${amount} but ${payment.amount}.`, + ); } + return payment; + } - export function parse(data: IIamportPayment): IPaymentHistory.IProps { - return { - currency: data.currency, - price: data.amount, - refund: data.cancel_amount, - paid_at: data.paid_at ? new Date(data.paid_at * 1000) : null, - cancelled_at: data.status === "cancelled" ? new Date() : null, - cancels: data.cancel_history.map((cancel) => ({ - data: cancel, - created_at: new Date(cancel.cancelled_at * 1000), - price: cancel.amount, - reason: cancel.reason, - })), - data, - }; - } + export function parse(data: IIamportPayment): IPaymentHistory.IProps { + return { + currency: data.currency, + price: data.amount, + refund: data.cancel_amount, + paid_at: data.paid_at ? new Date(data.paid_at * 1000) : null, + cancelled_at: data.status === "cancelled" ? new Date() : null, + cancels: data.cancel_history.map((cancel) => ({ + data: cancel, + created_at: new Date(cancel.cancelled_at * 1000), + price: cancel.amount, + reason: cancel.reason, + })), + data, + }; + } - export async function cancel( - storeId: string, - input: IIamportPaymentCancel.IStore, - ): Promise { - const reply = await imp.functional.payments.cancel( - await IamportAsset.connection(storeId), - input, - ); - if (reply.code !== 0) throw new DomainError(reply.message); - return reply.response; - } + export async function cancel( + storeId: string, + input: IIamportPaymentCancel.IStore, + ): Promise { + const reply = await imp.functional.payments.cancel( + await IamportAsset.connection(storeId), + input, + ); + if (reply.code !== 0) throw new DomainError(reply.message); + return reply.response; + } } diff --git a/packages/payment-backend/src/services/toss/TossAsset.ts b/packages/payment-backend/src/services/toss/TossAsset.ts index 6cdeac4..b2ace24 100644 --- a/packages/payment-backend/src/services/toss/TossAsset.ts +++ b/packages/payment-backend/src/services/toss/TossAsset.ts @@ -6,22 +6,20 @@ import { PaymentConfiguration } from "../../PaymentConfiguration"; import { PaymentGlobal } from "../../PaymentGlobal"; export namespace TossAsset { - export async function connection( - storeId: string, - ): Promise { - const host: string = - PaymentGlobal.testing === true - ? `http://127.0.0.1:${fake.FakeTossConfiguration.API_PORT}` - : "https://api.tosspayments.com"; - const token: string = btoa( - PaymentConfiguration.TOSS_SECRET_KEY(storeId) + ":", - ); + export async function connection(storeId: string): Promise { + const host: string = + PaymentGlobal.testing === true + ? `http://127.0.0.1:${fake.FakeTossConfiguration.API_PORT}` + : "https://api.tosspayments.com"; + const token: string = btoa( + PaymentConfiguration.TOSS_SECRET_KEY(storeId) + ":", + ); - return { - host, - headers: { - Authorization: `Basic ${token}`, - }, - }; - } + return { + host, + headers: { + Authorization: `Basic ${token}`, + }, + }; + } } diff --git a/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts b/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts index 4e6cc23..8e03494 100644 --- a/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts +++ b/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts @@ -5,35 +5,35 @@ import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayme import { TossAsset } from "./TossAsset"; export namespace TossPaymentBillingService { - export async function store( - mid: string, - input: ITossBilling.IStore, - ): Promise { - return toss.functional.v1.billing.authorizations.card.store( - await TossAsset.connection(mid), - input, - ); - } + export async function store( + mid: string, + input: ITossBilling.IStore, + ): Promise { + return toss.functional.v1.billing.authorizations.card.store( + await TossAsset.connection(mid), + input, + ); + } - export async function at( - mid: string, - input: ITossBilling.IAccessor, - ): Promise { - return toss.functional.v1.billing.authorizations.at( - await TossAsset.connection(mid), - input.authKey, - input, - ); - } + export async function at( + mid: string, + input: ITossBilling.IAccessor, + ): Promise { + return toss.functional.v1.billing.authorizations.at( + await TossAsset.connection(mid), + input.authKey, + input, + ); + } - export async function pay( - mid: string, - input: ITossBilling.IPaymentStore, - ): Promise { - return toss.functional.v1.billing.pay( - await TossAsset.connection(mid), - input.billingKey, - input, - ); - } + export async function pay( + mid: string, + input: ITossBilling.IPaymentStore, + ): Promise { + return toss.functional.v1.billing.pay( + await TossAsset.connection(mid), + input.billingKey, + input, + ); + } } diff --git a/packages/payment-backend/src/services/toss/TossPaymentCardService.ts b/packages/payment-backend/src/services/toss/TossPaymentCardService.ts index 57a9570..c41f329 100644 --- a/packages/payment-backend/src/services/toss/TossPaymentCardService.ts +++ b/packages/payment-backend/src/services/toss/TossPaymentCardService.ts @@ -4,13 +4,13 @@ import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossC import { TossAsset } from "./TossAsset"; export namespace TossPaymentCardService { - export async function store( - mid: string, - input: ITossCardPayment.IStore, - ): Promise { - return toss.functional.v1.payments.key_in( - await TossAsset.connection(mid), - input, - ); - } + export async function store( + mid: string, + input: ITossCardPayment.IStore, + ): Promise { + return toss.functional.v1.payments.key_in( + await TossAsset.connection(mid), + input, + ); + } } diff --git a/packages/payment-backend/src/services/toss/TossPaymentService.ts b/packages/payment-backend/src/services/toss/TossPaymentService.ts index 09ef126..b4550fb 100644 --- a/packages/payment-backend/src/services/toss/TossPaymentService.ts +++ b/packages/payment-backend/src/services/toss/TossPaymentService.ts @@ -10,90 +10,88 @@ import { TossPaymentCardService } from "./TossPaymentCardService"; import { TossPaymentVirtualAccountService } from "./TossPaymentVirtualAccountService"; export namespace TossPaymentService { - export async function at( - storeId: string, - paymentKey: string, - ): Promise { - try { - return await toss.functional.v1.payments.at( - await TossAsset.connection(storeId), - paymentKey, - ); - } catch (exp) { - await ErrorUtil.log("TossPaymentService.at", exp as any); - throw exp; - } + export async function at( + storeId: string, + paymentKey: string, + ): Promise { + try { + return await toss.functional.v1.payments.at( + await TossAsset.connection(storeId), + paymentKey, + ); + } catch (exp) { + await ErrorUtil.log("TossPaymentService.at", exp as any); + throw exp; } + } - export async function approve( - storeId: string, - paymentKey: string, - input: ITossPayment.IApproval, - ): Promise { - try { - return await toss.functional.v1.payments.approve( - await TossAsset.connection(storeId), - paymentKey, - input, - ); - } catch (exp) { - await ErrorUtil.log("TossPaymentService.approve", exp as any); - throw exp; - } + export async function approve( + storeId: string, + paymentKey: string, + input: ITossPayment.IApproval, + ): Promise { + try { + return await toss.functional.v1.payments.approve( + await TossAsset.connection(storeId), + paymentKey, + input, + ); + } catch (exp) { + await ErrorUtil.log("TossPaymentService.approve", exp as any); + throw exp; } + } - export function parse(data: ITossPayment): IPaymentHistory.IProps { - return { - currency: data.currency, - price: data.totalAmount, - refund: data.cancels?.length - ? data.cancels - .map((c) => c.cancelAmount) - .reduce((a, b) => a + b, 0) - : 0, - paid_at: - (data.status === "DONE" || - data.status === "CANCELED" || - data.status === "PARTIAL_CANCELED") && - data.approvedAt !== null - ? new Date(data.approvedAt) - : null, - cancelled_at: - data.status === "CANCELED" || data.status === "PARTIAL_CANCELED" - ? new Date() - : null, - cancels: (data.cancels ?? []).map((c) => ({ - data: c, - created_at: new Date(c.canceledAt), - price: c.cancelAmount, - reason: c.cancelReason, - })), - data, - }; - } + export function parse(data: ITossPayment): IPaymentHistory.IProps { + return { + currency: data.currency, + price: data.totalAmount, + refund: data.cancels?.length + ? data.cancels.map((c) => c.cancelAmount).reduce((a, b) => a + b, 0) + : 0, + paid_at: + (data.status === "DONE" || + data.status === "CANCELED" || + data.status === "PARTIAL_CANCELED") && + data.approvedAt !== null + ? new Date(data.approvedAt) + : null, + cancelled_at: + data.status === "CANCELED" || data.status === "PARTIAL_CANCELED" + ? new Date() + : null, + cancels: (data.cancels ?? []).map((c) => ({ + data: c, + created_at: new Date(c.canceledAt), + price: c.cancelAmount, + reason: c.cancelReason, + })), + data, + }; + } - /* ---------------------------------------------------------------- + /* ---------------------------------------------------------------- API ---------------------------------------------------------------- */ - export function store( - storeId: string, - input: ITossPayment.IStore, - ): Promise { - if (input.method === "billing") - return TossPaymentBillingService.pay(storeId, input); - else if (input.method === "card") - return TossPaymentCardService.store(storeId, input); - else return TossPaymentVirtualAccountService.store(storeId, input); - } + export function store( + storeId: string, + input: ITossPayment.IStore, + ): Promise { + if (input.method === "billing") + return TossPaymentBillingService.pay(storeId, input); + else if (input.method === "card") + return TossPaymentCardService.store(storeId, input); + else return TossPaymentVirtualAccountService.store(storeId, input); + } - export async function cancel( - storeId: string, - input: ITossPaymentCancel.IStore, - ): Promise { - return toss.functional.v1.payments.cancel( - await TossAsset.connection(storeId), - input.paymentKey, - input, - ); - } + export async function cancel( + storeId: string, + input: ITossPaymentCancel.IStore, + ): Promise { + return toss.functional.v1.payments.cancel( + await TossAsset.connection(storeId), + input.paymentKey, + input, + ); + } } diff --git a/packages/payment-backend/src/services/toss/TossPaymentVirtualAccountService.ts b/packages/payment-backend/src/services/toss/TossPaymentVirtualAccountService.ts index e347a9a..1151442 100644 --- a/packages/payment-backend/src/services/toss/TossPaymentVirtualAccountService.ts +++ b/packages/payment-backend/src/services/toss/TossPaymentVirtualAccountService.ts @@ -4,13 +4,13 @@ import { ITossVirtualAccountPayment } from "toss-payments-server-api/lib/structu import { TossAsset } from "./TossAsset"; export namespace TossPaymentVirtualAccountService { - export async function store( - storeId: string, - input: ITossVirtualAccountPayment.IStore, - ): Promise { - return toss.functional.v1.virtual_accounts.store( - await TossAsset.connection(storeId), - input, - ); - } + export async function store( + storeId: string, + input: ITossVirtualAccountPayment.IStore, + ): Promise { + return toss.functional.v1.virtual_accounts.store( + await TossAsset.connection(storeId), + input, + ); + } } diff --git a/packages/payment-backend/src/utils/ArgumentParser.ts b/packages/payment-backend/src/utils/ArgumentParser.ts index 7ccf2eb..d23a4be 100644 --- a/packages/payment-backend/src/utils/ArgumentParser.ts +++ b/packages/payment-backend/src/utils/ArgumentParser.ts @@ -2,78 +2,72 @@ import commander from "commander"; import * as inquirer from "inquirer"; export namespace ArgumentParser { - export type Inquiry = ( - command: commander.Command, - prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule, - action: (closure: (options: Partial) => Promise) => Promise, - ) => Promise; + export type Inquiry = ( + command: commander.Command, + prompt: (opt?: inquirer.StreamOptions) => inquirer.PromptModule, + action: (closure: (options: Partial) => Promise) => Promise, + ) => Promise; - export interface Prompt { - select: ( - name: string, - ) => ( - message: string, - ) => (choices: Choice[]) => Promise; - boolean: (name: string) => (message: string) => Promise; - } + export interface Prompt { + select: ( + name: string, + ) => ( + message: string, + ) => (choices: Choice[]) => Promise; + boolean: (name: string) => (message: string) => Promise; + } - export const parse = async ( - inquiry: ( - command: commander.Command, - prompt: Prompt, - action: ( - closure: (options: Partial) => Promise, - ) => Promise, - ) => Promise, - ): Promise => { - // TAKE OPTIONS - const action = (closure: (options: Partial) => Promise) => - new Promise((resolve, reject) => { - commander.program.action(async (options) => { - try { - resolve(await closure(options)); - } catch (exp) { - reject(exp); - } - }); - commander.program.parseAsync().catch(reject); - }); + export const parse = async ( + inquiry: ( + command: commander.Command, + prompt: Prompt, + action: (closure: (options: Partial) => Promise) => Promise, + ) => Promise, + ): Promise => { + // TAKE OPTIONS + const action = (closure: (options: Partial) => Promise) => + new Promise((resolve, reject) => { + commander.program.action(async (options) => { + try { + resolve(await closure(options)); + } catch (exp) { + reject(exp); + } + }); + commander.program.parseAsync().catch(reject); + }); - const select = - (name: string) => - (message: string) => - async (choices: Choice[]): Promise => - ( - await inquirer.createPromptModule()({ - type: "list", - name, - message, - choices, - }) - )[name]; - const boolean = (name: string) => async (message: string) => - ( - await inquirer.createPromptModule()({ - type: "confirm", - name, - message, - }) - )[name] as boolean; + const select = + (name: string) => + (message: string) => + async (choices: Choice[]): Promise => + ( + await inquirer.createPromptModule()({ + type: "list", + name, + message, + choices, + }) + )[name]; + const boolean = (name: string) => async (message: string) => + ( + await inquirer.createPromptModule()({ + type: "confirm", + name, + message, + }) + )[name] as boolean; - const output: T | Error = await (async () => { - try { - return await inquiry( - commander.program, - { select, boolean }, - action, - ); - } catch (error) { - return error as Error; - } - })(); + const output: T | Error = await (async () => { + try { + return await inquiry(commander.program, { select, boolean }, action); + } catch (error) { + return error as Error; + } + })(); - // RETURNS - if (output instanceof Error) throw output; - return output; - }; + // RETURNS + if (output instanceof Error) throw output; + return output; + }; } diff --git a/packages/payment-backend/src/utils/BcryptUtil.ts b/packages/payment-backend/src/utils/BcryptUtil.ts index 9009e02..50f65b9 100644 --- a/packages/payment-backend/src/utils/BcryptUtil.ts +++ b/packages/payment-backend/src/utils/BcryptUtil.ts @@ -1,11 +1,11 @@ import * as bcrypt from "bcryptjs"; export namespace BcryptUtil { - export const hash = async (input: string) => { - const salt: string = await bcrypt.genSalt(); - return bcrypt.hash(input, salt); - }; + export const hash = async (input: string) => { + const salt: string = await bcrypt.genSalt(); + return bcrypt.hash(input, salt); + }; - export const equals = async (props: { input: string; hashed: string }) => - bcrypt.compare(props.input, props.hashed); + export const equals = async (props: { input: string; hashed: string }) => + bcrypt.compare(props.input, props.hashed); } diff --git a/packages/payment-backend/src/utils/DateUtil.ts b/packages/payment-backend/src/utils/DateUtil.ts index f9fd38e..4d88046 100644 --- a/packages/payment-backend/src/utils/DateUtil.ts +++ b/packages/payment-backend/src/utils/DateUtil.ts @@ -1,100 +1,94 @@ export namespace DateUtil { - export function to_string(date: Date, hms: boolean = false): string { - let ret: string = `${date.getFullYear()}-${_To_cipher_string( - date.getMonth() + 1, - )}-${_To_cipher_string(date.getDate())}`; - if (hms === true) - ret += ` ${_To_cipher_string(date.getHours())}:${_To_cipher_string( - date.getMinutes(), - )}:${_To_cipher_string(date.getSeconds())}`; - - return ret; + export function to_string(date: Date, hms: boolean = false): string { + let ret: string = `${date.getFullYear()}-${_To_cipher_string( + date.getMonth() + 1, + )}-${_To_cipher_string(date.getDate())}`; + if (hms === true) + ret += ` ${_To_cipher_string(date.getHours())}:${_To_cipher_string( + date.getMinutes(), + )}:${_To_cipher_string(date.getSeconds())}`; + + return ret; + } + + export interface IDifference { + year: number; + month: number; + date: number; + } + + export function diff(x: Date | string, y: Date | string): IDifference { + x = _To_date(x); + y = _To_date(y); + + // FIRST DIFFERENCES + let ret: IDifference = { + year: x.getFullYear() - y.getFullYear(), + month: x.getMonth() - y.getMonth(), + date: x.getDate() - y.getDate(), + }; + + //---- + // HANDLE NEGATIVE ELEMENTS + //---- + // DATE + if (ret.date < 0) { + let last: number = last_date(y.getFullYear(), y.getMonth()); + + --ret.month; + ret.date = x.getDate() + (last - y.getDate()); } - export interface IDifference { - year: number; - month: number; - date: number; + // MONTH + if (ret.month < 0) { + --ret.year; + ret.month = 12 + ret.month; } - - export function diff(x: Date | string, y: Date | string): IDifference { - x = _To_date(x); - y = _To_date(y); - - // FIRST DIFFERENCES - let ret: IDifference = { - year: x.getFullYear() - y.getFullYear(), - month: x.getMonth() - y.getMonth(), - date: x.getDate() - y.getDate(), - }; - - //---- - // HANDLE NEGATIVE ELEMENTS - //---- - // DATE - if (ret.date < 0) { - let last: number = last_date(y.getFullYear(), y.getMonth()); - - --ret.month; - ret.date = x.getDate() + (last - y.getDate()); - } - - // MONTH - if (ret.month < 0) { - --ret.year; - ret.month = 12 + ret.month; - } - return ret; - } - - export function last_date(year: number, month: number): number { - // LEAP MONTH - if ( - month == 1 && - year % 4 == 0 && - !(year % 100 == 0 && year % 400 != 0) - ) - return 29; - else return LAST_DATES[month]; - } - - export function add_years(date: Date, value: number): Date { - date = new Date(date); - date.setFullYear(date.getFullYear() + value); - - return date; - } - - export function add_months(date: Date, value: number): Date { - date = new Date(date); - - let newYear: number = - date.getFullYear() + Math.floor((date.getMonth() + value) / 12); - let newMonth: number = (date.getMonth() + value) % 12; - let lastDate: number = last_date(newYear, newMonth - 1); - - if (lastDate < date.getDate()) date.setDate(lastDate); - - date.setMonth(value - 1); - return date; - } - - export function add_days(date: Date, value: number): Date { - date = new Date(); - date.setDate(date.getDate() + value); - - return date; - } - - function _To_date(date: string | Date): Date { - if (date instanceof Date) return date; - else return new Date(date); - } - function _To_cipher_string(val: number): string { - if (val < 10) return "0" + val; - else return String(val); - } - const LAST_DATES: number[] = [ - 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, - ]; + return ret; + } + + export function last_date(year: number, month: number): number { + // LEAP MONTH + if (month == 1 && year % 4 == 0 && !(year % 100 == 0 && year % 400 != 0)) + return 29; + else return LAST_DATES[month]; + } + + export function add_years(date: Date, value: number): Date { + date = new Date(date); + date.setFullYear(date.getFullYear() + value); + + return date; + } + + export function add_months(date: Date, value: number): Date { + date = new Date(date); + + let newYear: number = + date.getFullYear() + Math.floor((date.getMonth() + value) / 12); + let newMonth: number = (date.getMonth() + value) % 12; + let lastDate: number = last_date(newYear, newMonth - 1); + + if (lastDate < date.getDate()) date.setDate(lastDate); + + date.setMonth(value - 1); + return date; + } + + export function add_days(date: Date, value: number): Date { + date = new Date(); + date.setDate(date.getDate() + value); + + return date; + } + + function _To_date(date: string | Date): Date { + if (date instanceof Date) return date; + else return new Date(date); + } + function _To_cipher_string(val: number): string { + if (val < 10) return "0" + val; + else return String(val); + } + const LAST_DATES: number[] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; } diff --git a/packages/payment-backend/src/utils/ErrorUtil.ts b/packages/payment-backend/src/utils/ErrorUtil.ts index 22d32d7..7191819 100644 --- a/packages/payment-backend/src/utils/ErrorUtil.ts +++ b/packages/payment-backend/src/utils/ErrorUtil.ts @@ -6,53 +6,48 @@ import { Singleton } from "tstl/thread/Singleton"; import { PaymentConfiguration } from "../PaymentConfiguration"; export namespace ErrorUtil { - export function toJSON(err: any): object { - return err instanceof Object && err.toJSON instanceof Function - ? err.toJSON() - : serializeError(err); - } - - export async function log( - prefix: string, - data: string | object | Error, - ): Promise { - try { - if (data instanceof Error) data = toJSON(data); - - const date: Date = new Date(); - const fileName: string = `${date.getFullYear()}${cipher( - date.getMonth() + 1, - )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( - date.getMinutes(), - )}${cipher(date.getSeconds())}.${randint( - 0, - Number.MAX_SAFE_INTEGER, - )}`; - const content: string = JSON.stringify(data, null, 4); - - await directory.get(); - await fs.promises.writeFile( - `${PaymentConfiguration.ROOT}/assets/logs/errors/${prefix}_${fileName}.log`, - content, - "utf8", - ); - } catch {} - } + export function toJSON(err: any): object { + return err instanceof Object && err.toJSON instanceof Function + ? err.toJSON() + : serializeError(err); + } + + export async function log( + prefix: string, + data: string | object | Error, + ): Promise { + try { + if (data instanceof Error) data = toJSON(data); + + const date: Date = new Date(); + const fileName: string = `${date.getFullYear()}${cipher( + date.getMonth() + 1, + )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( + date.getMinutes(), + )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; + const content: string = JSON.stringify(data, null, 4); + + await directory.get(); + await fs.promises.writeFile( + `${PaymentConfiguration.ROOT}/assets/logs/errors/${prefix}_${fileName}.log`, + content, + "utf8", + ); + } catch {} + } } function cipher(val: number): string { - if (val < 10) return "0" + val; - else return String(val); + if (val < 10) return "0" + val; + else return String(val); } const directory = new Singleton(async () => { - try { - await fs.promises.mkdir(`${PaymentConfiguration.ROOT}/assets/logs`); - } catch {} + try { + await fs.promises.mkdir(`${PaymentConfiguration.ROOT}/assets/logs`); + } catch {} - try { - await fs.promises.mkdir( - `${PaymentConfiguration.ROOT}/assets/logs/errors`, - ); - } catch {} + try { + await fs.promises.mkdir(`${PaymentConfiguration.ROOT}/assets/logs/errors`); + } catch {} }); diff --git a/packages/payment-backend/src/utils/Terminal.ts b/packages/payment-backend/src/utils/Terminal.ts index c7174aa..c7fe808 100644 --- a/packages/payment-backend/src/utils/Terminal.ts +++ b/packages/payment-backend/src/utils/Terminal.ts @@ -2,17 +2,17 @@ import cp from "child_process"; import { Pair } from "tstl/utility/Pair"; export namespace Terminal { - export function execute( - ...commands: string[] - ): Promise> { - return new Promise((resolve, reject) => { - cp.exec( - commands.join(" && "), - (error: Error | null, stdout: string, stderr: string) => { - if (error) reject(error); - else resolve(new Pair(stdout, stderr)); - }, - ); - }); - } + export function execute( + ...commands: string[] + ): Promise> { + return new Promise((resolve, reject) => { + cp.exec( + commands.join(" && "), + (error: Error | null, stdout: string, stderr: string) => { + if (error) reject(error); + else resolve(new Pair(stdout, stderr)); + }, + ); + }); + } } diff --git a/packages/payment-backend/src/utils/TokenManager.ts b/packages/payment-backend/src/utils/TokenManager.ts index cdaf857..02daa5f 100644 --- a/packages/payment-backend/src/utils/TokenManager.ts +++ b/packages/payment-backend/src/utils/TokenManager.ts @@ -4,87 +4,86 @@ import { Pair } from "tstl/utility/Pair"; import { v4 } from "uuid"; export namespace TokenManager { - export function generate( - table: string, - id: string, - writable: boolean, - duration: number, - ): string { - // PAYLOAD DATA WITH CONFUSER - const payload: IPayload = { - [v4()]: v4(), - table, - id, - writable, - expired_at: Date.now() + duration, - [v4()]: randint(10000, 100000), - }; + export function generate( + table: string, + id: string, + writable: boolean, + duration: number, + ): string { + // PAYLOAD DATA WITH CONFUSER + const payload: IPayload = { + [v4()]: v4(), + table, + id, + writable, + expired_at: Date.now() + duration, + [v4()]: randint(10000, 100000), + }; - // RETURNS WITH ENCRYPTION - const iv: string = _Get_iv(table); - return AesPkcs5.encrypt(JSON.stringify(payload), ENCRYPT_KEY, iv); - } + // RETURNS WITH ENCRYPTION + const iv: string = _Get_iv(table); + return AesPkcs5.encrypt(JSON.stringify(payload), ENCRYPT_KEY, iv); + } - export function refresh( - table: string, - token: string, - writable: boolean, - duration: number, - ): string | null { - // PARSE PAYLOAD - let iv: string = _Get_iv(table); - let payload: IPayload | null = _Parse(table, token, iv); - if (payload === null) return null; + export function refresh( + table: string, + token: string, + writable: boolean, + duration: number, + ): string | null { + // PARSE PAYLOAD + let iv: string = _Get_iv(table); + let payload: IPayload | null = _Parse(table, token, iv); + if (payload === null) return null; - // RE-GENERATE TOKEN - return generate(table, payload.id, writable, duration); - } + // RE-GENERATE TOKEN + return generate(table, payload.id, writable, duration); + } - export function parse( - table: string, - token: string, - ): Pair | null { - const payload: IPayload | null = _Parse(table, token); - return payload !== null ? new Pair(payload.id, payload.writable) : null; - } + export function parse( + table: string, + token: string, + ): Pair | null { + const payload: IPayload | null = _Parse(table, token); + return payload !== null ? new Pair(payload.id, payload.writable) : null; + } - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- HIDDEN MEMBERS ----------------------------------------------------------- */ - function _Parse( - table: string, - token: string, - iv: string = _Get_iv(table), - ): IPayload | null { - // PARSE PAYLOAD - let payload: IPayload; - try { - let content: string = AesPkcs5.decrypt(token, ENCRYPT_KEY, iv); - payload = JSON.parse(content); - } catch { - return null; - } - - // JUDGEMENT - return payload.table !== table || payload.expired_at < Date.now() - ? null - : payload; + function _Parse( + table: string, + token: string, + iv: string = _Get_iv(table), + ): IPayload | null { + // PARSE PAYLOAD + let payload: IPayload; + try { + let content: string = AesPkcs5.decrypt(token, ENCRYPT_KEY, iv); + payload = JSON.parse(content); + } catch { + return null; } - function _Get_iv(table: string): string { - if (table.length > 16) table = table.substr(0, 16); - else if (table.length < 16) - table = table + "_".repeat(16 - table.length); + // JUDGEMENT + return payload.table !== table || payload.expired_at < Date.now() + ? null + : payload; + } - return table; - } + function _Get_iv(table: string): string { + if (table.length > 16) table = table.substr(0, 16); + else if (table.length < 16) table = table + "_".repeat(16 - table.length); - const ENCRYPT_KEY = "12iDCMJDvwwCjYeJE6TSEDL8CQlJQcgN"; + return table; + } - interface IPayload { - table: string; - id: string; - writable: boolean; - expired_at: number; - } + const ENCRYPT_KEY = "12iDCMJDvwwCjYeJE6TSEDL8CQlJQcgN"; + + interface IPayload { + table: string; + id: string; + writable: boolean; + expired_at: number; + } } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment.ts index fbecca9..9d6b2a8 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment.ts @@ -10,119 +10,115 @@ import { PaymentConfiguration } from "../../../src"; import { IamportAsset } from "../../../src/services/iamport/IamportAsset"; export async function test_api_iamport_card_payment( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - //---- - // 결제의 원천이 되는 주문 정보 - //---- - /** - * 귀하의 백엔드 서버가 발행한 주문 ID. - */ - const yourOrderId: string = v4(); + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); - /** - * 주문 금액. - */ - const yourOrderPrice: number = 20_000; + /** + * 주문 금액. + */ + const yourOrderPrice: number = 20_000; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 결제 내역 등록 ----------------------------------------------------------- */ - /** - * 아임포트 시뮬레이션 - * - * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 카드 결제를 - * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 - * {@link IIamportPayment.imp_uid} 가 전달된다. - * - * 이 {@link IIamportPayment.imp_uid} 와 귀하의 백엔드에서 직접 생성한 - * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 - * {@link IPaymentHistory} 등록에 사용하도록 하자. - */ - const payment: IIamportResponse = - await imp.functional.subscribe.payments.onetime( - await IamportAsset.connection("test-iamport-store-id"), - { - card_number: "1234-1234-1234-1234", - expiry: "2028-12", - birth: "880311", + /** + * 아임포트 시뮬레이션 + * + * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 카드 결제를 + * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 + * {@link IIamportPayment.imp_uid} 가 전달된다. + * + * 이 {@link IIamportPayment.imp_uid} 와 귀하의 백엔드에서 직접 생성한 + * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 + * {@link IPaymentHistory} 등록에 사용하도록 하자. + */ + const payment: IIamportResponse = + await imp.functional.subscribe.payments.onetime( + await IamportAsset.connection("test-iamport-store-id"), + { + card_number: "1234-1234-1234-1234", + expiry: "2028-12", + birth: "880311", - merchant_uid: yourOrderId, - amount: yourOrderPrice, - name: "Fake 주문", - }, - ); - typia.assert(payment); + merchant_uid: yourOrderId, + amount: yourOrderPrice, + name: "Fake 주문", + }, + ); + typia.assert(payment); - /** - * 결제 이력 등록하기. - * - * 앞서 아임포트의 팝업 창을 이용하여 카드 결제를 진행하고 발급받은 - * {@link IIamportPayment.imp_uid}, 그리고 귀하의 백엔드에서 직접 생성한 - * {@link IIamportPayment.merchant_uid yourOrderId} 를 각각 - * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 로 할당하여 - * {@link IPaymentReservation} 레코드를 발행한다. - * - * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 - * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. - */ - const history: IPaymentHistory = - await PaymentAPI.functional.payments.histories.store(connection, { - vendor: { - code: "iamport", - store_id: "test-iamport-store-id", - uid: payment.response.imp_uid, - }, - source: { - schema: "some-schema", - table: "some-table", - id: yourOrderId, - }, - webhook_url: `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ - PaymentAPI.functional.payments.internal.webhook.METADATA.path - }`, - price: yourOrderPrice, - password: "some-password", - }); - typia.assert(history); + /** + * 결제 이력 등록하기. + * + * 앞서 아임포트의 팝업 창을 이용하여 카드 결제를 진행하고 발급받은 + * {@link IIamportPayment.imp_uid}, 그리고 귀하의 백엔드에서 직접 생성한 + * {@link IIamportPayment.merchant_uid yourOrderId} 를 각각 + * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 로 할당하여 + * {@link IPaymentReservation} 레코드를 발행한다. + * + * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 + * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. + */ + const history: IPaymentHistory = + await PaymentAPI.functional.payments.histories.store(connection, { + vendor: { + code: "iamport", + store_id: "test-iamport-store-id", + uid: payment.response.imp_uid, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourOrderId, + }, + webhook_url: `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ + PaymentAPI.functional.payments.internal.webhook.METADATA.path + }`, + price: yourOrderPrice, + password: "some-password", + }); + typia.assert(history); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 결제 내역 조회하기 ----------------------------------------------------------- */ - /** - * 결제 내역 조회하기 by {@link IPaymentHistory.id}. - * - * 앞서 등록한 결제 이력의 상세 정보를 {@link IPaymentHistory.id} 를 이용하여 조회할 - * 수 있다. 하지만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 - * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const read: IPaymentHistory = - await PaymentAPI.functional.payments.histories.at( - connection, - history.id, - { - password: "some-password", - }, - ); - typia.assert(read); - if (read.vendor_code === "iamport") read.data.imp_uid; // if condition 을 통한 하위 타입 특정 + /** + * 결제 내역 조회하기 by {@link IPaymentHistory.id}. + * + * 앞서 등록한 결제 이력의 상세 정보를 {@link IPaymentHistory.id} 를 이용하여 조회할 + * 수 있다. 하지만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 + * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const read: IPaymentHistory = + await PaymentAPI.functional.payments.histories.at(connection, history.id, { + password: "some-password", + }); + typia.assert(read); + if (read.vendor_code === "iamport") read.data.imp_uid; // if condition 을 통한 하위 타입 특정 - /** - * 결제 내역 조회하기 by {@link IPaymentSource}. - * - * 앞서 등록한 결제 이력의 상세 정보는 {@link IPaymentSource} 를 통하여도 조회할 수 - * 있다. 다만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 - * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const gotten: IPaymentHistory = - await PaymentAPI.functional.payments.histories.get(connection, { - schema: "some-schema", - table: "some-table", - id: yourOrderId, - password: "some-password", - }); - typia.assert(gotten); - if (gotten.vendor_code === "iamport") gotten.data.imp_uid; // if condition 을 통한 하위 타입 특정 + /** + * 결제 내역 조회하기 by {@link IPaymentSource}. + * + * 앞서 등록한 결제 이력의 상세 정보는 {@link IPaymentSource} 를 통하여도 조회할 수 + * 있다. 다만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 + * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const gotten: IPaymentHistory = + await PaymentAPI.functional.payments.histories.get(connection, { + schema: "some-schema", + table: "some-table", + id: yourOrderId, + password: "some-password", + }); + typia.assert(gotten); + if (gotten.vendor_code === "iamport") gotten.data.imp_uid; // if condition 을 통한 하위 타입 특정 - return typia.assert(gotten); + return typia.assert(gotten); } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel.ts index 46d1ab0..5b36382 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel.ts @@ -5,11 +5,11 @@ import { validate_payment_cancel } from "../internal/validate_payment_cancel"; import { test_api_iamport_card_payment } from "./test_api_iamport_card_payment"; export async function test_api_iamport_card_payment_cancel( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_iamport_card_payment( - connection, - ); - await validate_payment_cancel(connection, history, () => null); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_iamport_card_payment( + connection, + ); + await validate_payment_cancel(connection, history, () => null); } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel_partial.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel_partial.ts index d51da78..c4edfd5 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel_partial.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel_partial.ts @@ -5,11 +5,11 @@ import { validate_payment_cancel_partial } from "../internal/validate_payment_ca import { test_api_iamport_card_payment } from "./test_api_iamport_card_payment"; export async function test_api_iamport_card_payment_cancel_partial( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_iamport_card_payment( - connection, - ); - await validate_payment_cancel_partial(connection, history, () => null); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_iamport_card_payment( + connection, + ); + await validate_payment_cancel_partial(connection, history, () => null); } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_subscription_payment.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_subscription_payment.ts index d684121..edcade9 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_subscription_payment.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_subscription_payment.ts @@ -9,117 +9,117 @@ import { v4 } from "uuid"; import { IamportAsset } from "../../../src/services/iamport/IamportAsset"; export async function test_api_iamport_subscription_payment( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - /** - * 귀하의 백엔드 서버가 발행한 식별자 ID. - * - * 아임포트는 토스 페이먼츠와 달리, 간편 결제로 등록한 카드에 자체 식별자를 부여하지 - * 않는다. 따라서 귀하의 백엔드 서버가 발행한 식별자 ID 가 곧, 해당 간편 결제 등록 - * 내역의 유일무일한 식별자인 셈. - */ - const yourSourceId: string = v4(); + /** + * 귀하의 백엔드 서버가 발행한 식별자 ID. + * + * 아임포트는 토스 페이먼츠와 달리, 간편 결제로 등록한 카드에 자체 식별자를 부여하지 + * 않는다. 따라서 귀하의 백엔드 서버가 발행한 식별자 ID 가 곧, 해당 간편 결제 등록 + * 내역의 유일무일한 식별자인 셈. + */ + const yourSourceId: string = v4(); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 간편 결제 카드 등록 ----------------------------------------------------------- */ - /** - * 아임포트 시뮬레이션. - * - * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 간편 결제 - * 카드를 등록하는 상황을 시뮬레이션 한다. 고객이 간편 결제 카드 등록을 마치거든, - * 프론트 어플리케이션에 {@link IIamportSubscription.customer_uid yourSourceId} - * 가 전달된다. - * - * 참고로 이 {@link IIamportSubscription.customer_uid yourSourceId} 는 귀하의 - * 백엔드 서버가 발급해 준 식별자 ID 로써, 아임포트는 토스 페이먼츠와 달리, 간편 - * 결제로 등록한 카드에 자체 식별자를 부여하지 않는다. - * - * 어쨋든 이 {@link IIamportSubscription.customer_uid yourSourceId} 를 잘 기억해 - * 두었다가, 이를 다음 단계인 {@link IPaymentReservation} 등록에 사용하도록 하자. - */ - const subscription: IIamportResponse = - await imp.functional.subscribe.customers.store( - await IamportAsset.connection("test-iamport-store-id"), - yourSourceId, - { - customer_uid: yourSourceId, - card_number: "1234-5678-1234-5678", - expiry: "2028-12", - birth: "880311", - }, - ); - typia.assert(subscription); - subscription.response.customer_uid; // 귀하가 발행한 ID 만이 유일한 식별자 + /** + * 아임포트 시뮬레이션. + * + * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 간편 결제 + * 카드를 등록하는 상황을 시뮬레이션 한다. 고객이 간편 결제 카드 등록을 마치거든, + * 프론트 어플리케이션에 {@link IIamportSubscription.customer_uid yourSourceId} + * 가 전달된다. + * + * 참고로 이 {@link IIamportSubscription.customer_uid yourSourceId} 는 귀하의 + * 백엔드 서버가 발급해 준 식별자 ID 로써, 아임포트는 토스 페이먼츠와 달리, 간편 + * 결제로 등록한 카드에 자체 식별자를 부여하지 않는다. + * + * 어쨋든 이 {@link IIamportSubscription.customer_uid yourSourceId} 를 잘 기억해 + * 두었다가, 이를 다음 단계인 {@link IPaymentReservation} 등록에 사용하도록 하자. + */ + const subscription: IIamportResponse = + await imp.functional.subscribe.customers.store( + await IamportAsset.connection("test-iamport-store-id"), + yourSourceId, + { + customer_uid: yourSourceId, + card_number: "1234-5678-1234-5678", + expiry: "2028-12", + birth: "880311", + }, + ); + typia.assert(subscription); + subscription.response.customer_uid; // 귀하가 발행한 ID 만이 유일한 식별자 - /** - * 간편 결제 수단 등록하기. - * - * 아임포트는 간편 결제 수단에 대하여 별도의 식별자 번호를 부여하지 않는다. 따라서 - * 귀하가 발행하였던 {@link IIamportSubscription.customer_uid yourSourceId} 를 - * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 모두 동일하게 - * 할당하여 {@link IPaymentReservation} 레코드를 발행한다. - * - * 참고로 간편 결제 수단을 등혹할 때 반드시 비밀번호를 설정해야 하는데, 이는 향후 - * 간편 결제 수단을 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 - * 한다. - */ - const reservation: IPaymentReservation = - await PaymentAPI.functional.payments.reservations.store(connection, { - vendor: { - code: "iamport", - store_id: "test-iamport-store-id", - uid: yourSourceId, - }, - source: { - schema: "some-schema", - table: "some-table", - id: yourSourceId, - }, - title: "some-title", - password: "some-password", - }); - typia.assert(reservation); + /** + * 간편 결제 수단 등록하기. + * + * 아임포트는 간편 결제 수단에 대하여 별도의 식별자 번호를 부여하지 않는다. 따라서 + * 귀하가 발행하였던 {@link IIamportSubscription.customer_uid yourSourceId} 를 + * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 모두 동일하게 + * 할당하여 {@link IPaymentReservation} 레코드를 발행한다. + * + * 참고로 간편 결제 수단을 등혹할 때 반드시 비밀번호를 설정해야 하는데, 이는 향후 + * 간편 결제 수단을 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 + * 한다. + */ + const reservation: IPaymentReservation = + await PaymentAPI.functional.payments.reservations.store(connection, { + vendor: { + code: "iamport", + store_id: "test-iamport-store-id", + uid: yourSourceId, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourSourceId, + }, + title: "some-title", + password: "some-password", + }); + typia.assert(reservation); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 간편 결제 카드 조회하기 ----------------------------------------------------------- */ - /** - * 간편 결제 수단 조회하기 by {@link IPaymentReservation.id}. - * - * 앞서 등록한 간편 결제 수단의 상세 정보를 {@link IPaymentReservation.id} 를 - * 이용하여 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용했던 - * 비밀번호가 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const read: IPaymentReservation = - await PaymentAPI.functional.payments.reservations.at( - connection, - reservation.id, - { - password: "some-password", - }, - ); - typia.assert(read); + /** + * 간편 결제 수단 조회하기 by {@link IPaymentReservation.id}. + * + * 앞서 등록한 간편 결제 수단의 상세 정보를 {@link IPaymentReservation.id} 를 + * 이용하여 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용했던 + * 비밀번호가 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const read: IPaymentReservation = + await PaymentAPI.functional.payments.reservations.at( + connection, + reservation.id, + { + password: "some-password", + }, + ); + typia.assert(read); - // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. - if (read.vendor_code === "iamport") read.data.customer_uid; + // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. + if (read.vendor_code === "iamport") read.data.customer_uid; - /** - * 간편 결제 수단 조회하기 by {@link IPaymentSource}. - * - * 앞서 등록한 간편 결제 수단의 상세 정보는 {@link IPaymentSource} 를 통하여도 - * 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용햇던 비밀번호가 - * 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const gotten: IPaymentReservation = - await PaymentAPI.functional.payments.reservations.get(connection, { - schema: "some-schema", - table: "some-table", - id: yourSourceId, - password: "some-password", - }); - typia.assert(gotten); + /** + * 간편 결제 수단 조회하기 by {@link IPaymentSource}. + * + * 앞서 등록한 간편 결제 수단의 상세 정보는 {@link IPaymentSource} 를 통하여도 + * 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용햇던 비밀번호가 + * 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const gotten: IPaymentReservation = + await PaymentAPI.functional.payments.reservations.get(connection, { + schema: "some-schema", + table: "some-table", + id: yourSourceId, + password: "some-password", + }); + typia.assert(gotten); - // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. - if (gotten.vendor_code === "iamport") gotten.data.card_number; + // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. + if (gotten.vendor_code === "iamport") gotten.data.card_number; } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment.ts index 92fc8a4..a285213 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment.ts @@ -14,119 +14,119 @@ import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentS import { IamportAsset } from "../../../src/services/iamport/IamportAsset"; export async function test_api_iamport_vbank_payment( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - //---- - // 결제의 원천이 되는 주문 정보 - //---- - /** - * 귀하의 백엔드 서버가 발행한 주문 ID. - */ - const yourOrderId: string = v4(); + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); - /** - * 주문 금액. - */ - const yourOrderPrice: number = 19_900; + /** + * 주문 금액. + */ + const yourOrderPrice: number = 19_900; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 결제 내역 등록 ----------------------------------------------------------- */ - /** - * 아임포트 시뮬레이션. - * - * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 가상 계좌 - * 결제를 하는 상황을 시뮬레이션 한다. 고객이 가상 계좌 발급을 마치거든, 프론트 - * 어플리케이션에 {@link IIamportPayment.imp_uid} 가 전달된다. - * - * 이 {@link IIamportPayment.imp_uid} 와 귀하의 백엔드에서 직접 생성한 - * {@link IIamportPayment.merchant_uid yourOrderId} 를 잘 기억해두었다가, 이를 - * 다음 단계인 {@link IPaymentHistory} 등록에 사용하도록 하자. - */ - const payment: IIamportResponse = - await imp.functional.vbanks.store( - await IamportAsset.connection("test-iamport-store-id"), - { - merchant_uid: yourOrderId, - amount: yourOrderPrice, - vbank_code: "SHINHAN", - vbank_due: Date.now() / 1_000 + 7 * 24 * 60 * 60, - vbank_holder: "Samchon", - }, - ); - typia.assert(payment); + /** + * 아임포트 시뮬레이션. + * + * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 가상 계좌 + * 결제를 하는 상황을 시뮬레이션 한다. 고객이 가상 계좌 발급을 마치거든, 프론트 + * 어플리케이션에 {@link IIamportPayment.imp_uid} 가 전달된다. + * + * 이 {@link IIamportPayment.imp_uid} 와 귀하의 백엔드에서 직접 생성한 + * {@link IIamportPayment.merchant_uid yourOrderId} 를 잘 기억해두었다가, 이를 + * 다음 단계인 {@link IPaymentHistory} 등록에 사용하도록 하자. + */ + const payment: IIamportResponse = + await imp.functional.vbanks.store( + await IamportAsset.connection("test-iamport-store-id"), + { + merchant_uid: yourOrderId, + amount: yourOrderPrice, + vbank_code: "SHINHAN", + vbank_due: Date.now() / 1_000 + 7 * 24 * 60 * 60, + vbank_holder: "Samchon", + }, + ); + typia.assert(payment); - /** - * 웹훅 URL 설정하기. - * - * 웹훅 URL 을 테스트용 API 주소, internal.webhook 으로 설정. - */ - const webhook_url: string = `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ - PaymentAPI.functional.payments.internal.webhook.METADATA.path - }`; + /** + * 웹훅 URL 설정하기. + * + * 웹훅 URL 을 테스트용 API 주소, internal.webhook 으로 설정. + */ + const webhook_url: string = `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ + PaymentAPI.functional.payments.internal.webhook.METADATA.path + }`; - /** - * 결제 이력 등록하기. - * - * 앞서 아임포트의 팝업 창을 이용하여 가상 계좌 결제를 진행하고 발급받은 - * {@link IIamportPayment.imp_uid}, 그리고 귀하의 백엔드에서 직접 생성한 - * {@link IIamportPayment.merchant_uid yourOrderId} 를 각각 - * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 로 할당하여 - * {@link IPaymentReservation} 레코드를 발행한다. - * - * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 - * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. - */ - const history: IPaymentHistory = - await PaymentAPI.functional.payments.histories.store(connection, { - vendor: { - code: "iamport", - store_id: "test-iamport-store-id", - uid: payment.response.imp_uid, - }, - source: { - schema: "some-schema", - table: "some-table", - id: yourOrderId, - }, - webhook_url, // 테스트용 웹훅 URL - price: yourOrderPrice, - password: "some-password", - }); - typia.assert(history); + /** + * 결제 이력 등록하기. + * + * 앞서 아임포트의 팝업 창을 이용하여 가상 계좌 결제를 진행하고 발급받은 + * {@link IIamportPayment.imp_uid}, 그리고 귀하의 백엔드에서 직접 생성한 + * {@link IIamportPayment.merchant_uid yourOrderId} 를 각각 + * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 로 할당하여 + * {@link IPaymentReservation} 레코드를 발행한다. + * + * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 + * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. + */ + const history: IPaymentHistory = + await PaymentAPI.functional.payments.histories.store(connection, { + vendor: { + code: "iamport", + store_id: "test-iamport-store-id", + uid: payment.response.imp_uid, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourOrderId, + }, + webhook_url, // 테스트용 웹훅 URL + price: yourOrderPrice, + password: "some-password", + }); + typia.assert(history); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 웹훅 이벤트 리스닝 ----------------------------------------------------------- */ - /** - * 입금 시뮬레이션하기. - * - * 고객이 자신 앞을 발급된 계좌에, 결제 금액을 입금하는 상황 시뮬레이션. - */ - await imp.functional.internal.deposit( - await IamportAsset.connection("test-iamport-store-id"), - payment.response.imp_uid, - ); + /** + * 입금 시뮬레이션하기. + * + * 고객이 자신 앞을 발급된 계좌에, 결제 금액을 입금하는 상황 시뮬레이션. + */ + await imp.functional.internal.deposit( + await IamportAsset.connection("test-iamport-store-id"), + payment.response.imp_uid, + ); - // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. - await sleep_for(1_000); + // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. + await sleep_for(1_000); - /** - * 웹흑 리스닝 시뮬레이션. - * - * 귀하의 백엔드 서버가 웹훅 이벤트를 수신한 상황을 가정한다. - */ - const webhook: IPaymentWebhookHistory | undefined = - FakePaymentStorage.webhooks.back(); + /** + * 웹흑 리스닝 시뮬레이션. + * + * 귀하의 백엔드 서버가 웹훅 이벤트를 수신한 상황을 가정한다. + */ + const webhook: IPaymentWebhookHistory | undefined = + FakePaymentStorage.webhooks.back(); - // 이하 웹훅 데이터를 통한 입금 여부 검증 - TestValidator.equals("webhook")(!!webhook)(true); - TestValidator.equals("history.id")(history.id)(webhook?.current.id); - TestValidator.equals("paid_at")(!!webhook?.previous.paid_at)(false); - TestValidator.equals("paid_at")(!!webhook?.current.paid_at)(true); + // 이하 웹훅 데이터를 통한 입금 여부 검증 + TestValidator.equals("webhook")(!!webhook)(true); + TestValidator.equals("history.id")(history.id)(webhook?.current.id); + TestValidator.equals("paid_at")(!!webhook?.previous.paid_at)(false); + TestValidator.equals("paid_at")(!!webhook?.current.paid_at)(true); - // 웹훅 데이터 삭제 - FakePaymentStorage.webhooks.pop_back(); + // 웹훅 데이터 삭제 + FakePaymentStorage.webhooks.pop_back(); - return history; + return history; } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel.ts index 711b8d8..9f68e71 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel.ts @@ -5,16 +5,16 @@ import { validate_payment_cancel } from "../internal/validate_payment_cancel"; import { test_api_iamport_vbank_payment } from "./test_api_iamport_vbank_payment"; export async function test_api_iamport_vbank_payment_cancel( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_iamport_vbank_payment( - connection, - ); - await validate_payment_cancel(connection, history, () => ({ - bank: "신한은행", - account: "110-123-456789", - holder: "홍길동", - mobile: "010-1234-5678", - })); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_iamport_vbank_payment( + connection, + ); + await validate_payment_cancel(connection, history, () => ({ + bank: "신한은행", + account: "110-123-456789", + holder: "홍길동", + mobile: "010-1234-5678", + })); } diff --git a/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel_partial.ts b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel_partial.ts index 0f062c9..6b0ab19 100644 --- a/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel_partial.ts +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel_partial.ts @@ -5,16 +5,16 @@ import { validate_payment_cancel_partial } from "../internal/validate_payment_ca import { test_api_iamport_vbank_payment } from "./test_api_iamport_vbank_payment"; export async function test_api_iamport_vbank_payment_cancel_partial( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_iamport_vbank_payment( - connection, - ); - await validate_payment_cancel_partial(connection, history, () => ({ - bank: "신한은행", - account: "110-123-456789", - holder: "홍길동", - mobile: "010-1234-5678", - })); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_iamport_vbank_payment( + connection, + ); + await validate_payment_cancel_partial(connection, history, () => ({ + bank: "신한은행", + account: "110-123-456789", + holder: "홍길동", + mobile: "010-1234-5678", + })); } diff --git a/packages/payment-backend/test/features/internal/validate_payment_cancel.ts b/packages/payment-backend/test/features/internal/validate_payment_cancel.ts index 439386c..5394772 100644 --- a/packages/payment-backend/test/features/internal/validate_payment_cancel.ts +++ b/packages/payment-backend/test/features/internal/validate_payment_cancel.ts @@ -8,53 +8,47 @@ import { sleep_for } from "tstl"; import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentStorage"; export const validate_payment_cancel = async ( - connection: PaymentAPI.IConnection, - history: IPaymentHistory, - banker: () => IPaymentCancelHistory.IBankAccount | null, + connection: PaymentAPI.IConnection, + history: IPaymentHistory, + banker: () => IPaymentCancelHistory.IBankAccount | null, ) => { - // 검증기 준비 - const validate = - (done: boolean) => (record: IPaymentWebhookHistory.IHistory) => { - TestValidator.equals("cancelled_at")(done)(!!record.cancelled_at); - TestValidator.equals("cancels.length")(done)( - !!record.cancels.length, - ); - TestValidator.equals("cancels[0].amount")(done ? history.price : 0)( - done ? record.cancels[0].price : 0, - ); - TestValidator.equals("history.refund")(record.refund)( - done ? history.price : null, - ); - }; - validate(false)(history); + // 검증기 준비 + const validate = + (done: boolean) => (record: IPaymentWebhookHistory.IHistory) => { + TestValidator.equals("cancelled_at")(done)(!!record.cancelled_at); + TestValidator.equals("cancels.length")(done)(!!record.cancels.length); + TestValidator.equals("cancels[0].amount")(done ? history.price : 0)( + done ? record.cancels[0].price : 0, + ); + TestValidator.equals("history.refund")(record.refund)( + done ? history.price : null, + ); + }; + validate(false)(history); - // 결제 취소하기 - const cancelled: IPaymentHistory = - await PaymentAPI.functional.payments.histories.cancel(connection, { - source: history.source, - password: "some-password", - price: history.price, - reason: "some-reason", - account: banker(), - }); - validate(true)(cancelled); + // 결제 취소하기 + const cancelled: IPaymentHistory = + await PaymentAPI.functional.payments.histories.cancel(connection, { + source: history.source, + password: "some-password", + price: history.price, + reason: "some-reason", + account: banker(), + }); + validate(true)(cancelled); - // 웹훅 검증하기 - await sleep_for(1000); - const webhook: IPaymentWebhookHistory | undefined = - FakePaymentStorage.webhooks.back(); - if (webhook === undefined) throw new Error("Webhook history not found."); - validate(true)(webhook.current); - FakePaymentStorage.webhooks.pop_back(); + // 웹훅 검증하기 + await sleep_for(1000); + const webhook: IPaymentWebhookHistory | undefined = + FakePaymentStorage.webhooks.back(); + if (webhook === undefined) throw new Error("Webhook history not found."); + validate(true)(webhook.current); + FakePaymentStorage.webhooks.pop_back(); - // 데이터 불러와 재확인 - const reloaded: IPaymentHistory = - await PaymentAPI.functional.payments.histories.at( - connection, - history.id, - { - password: "some-password", - }, - ); - validate(true)(reloaded); + // 데이터 불러와 재확인 + const reloaded: IPaymentHistory = + await PaymentAPI.functional.payments.histories.at(connection, history.id, { + password: "some-password", + }); + validate(true)(reloaded); }; diff --git a/packages/payment-backend/test/features/internal/validate_payment_cancel_partial.ts b/packages/payment-backend/test/features/internal/validate_payment_cancel_partial.ts index 81d9c21..615382a 100644 --- a/packages/payment-backend/test/features/internal/validate_payment_cancel_partial.ts +++ b/packages/payment-backend/test/features/internal/validate_payment_cancel_partial.ts @@ -8,51 +8,43 @@ import { sleep_for } from "tstl"; import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentStorage"; export const validate_payment_cancel_partial = async ( - connection: PaymentAPI.IConnection, - history: IPaymentHistory, - banker: () => IPaymentCancelHistory.IBankAccount | null, + connection: PaymentAPI.IConnection, + history: IPaymentHistory, + banker: () => IPaymentCancelHistory.IBankAccount | null, ): Promise => { - // 검증기 준비 - const validate = - (count: number) => async (record: IPaymentWebhookHistory.IHistory) => { - const check = (record: IPaymentWebhookHistory.IHistory) => { - TestValidator.equals("cancelled_at")(!!count)( - !!record.cancelled_at, - ); - TestValidator.equals("cancels.length")(count)( - record.cancels.length, - ); - TestValidator.equals("cancels[].amount")( - (history.price / 5) * count, - )( - record.cancels - .map((c) => c.price) - .reduce((a, b) => a + b, 0), - ); - TestValidator.equals("history.refund")(record.refund ?? 0)( - (history.price / 5) * count, - ); - }; - check(record); + // 검증기 준비 + const validate = + (count: number) => async (record: IPaymentWebhookHistory.IHistory) => { + const check = (record: IPaymentWebhookHistory.IHistory) => { + TestValidator.equals("cancelled_at")(!!count)(!!record.cancelled_at); + TestValidator.equals("cancels.length")(count)(record.cancels.length); + TestValidator.equals("cancels[].amount")((history.price / 5) * count)( + record.cancels.map((c) => c.price).reduce((a, b) => a + b, 0), + ); + TestValidator.equals("history.refund")(record.refund ?? 0)( + (history.price / 5) * count, + ); + }; + check(record); - if (count !== 0) { - await sleep_for(1_000); - check(FakePaymentStorage.webhooks.back().current); - FakePaymentStorage.webhooks.pop_back(); - } - }; - await validate(0)(history); + if (count !== 0) { + await sleep_for(1_000); + check(FakePaymentStorage.webhooks.back().current); + FakePaymentStorage.webhooks.pop_back(); + } + }; + await validate(0)(history); - // 결제 취소하기 - await ArrayUtil.asyncRepeat(5)(async (i) => { - const cancelled: IPaymentHistory = - await PaymentAPI.functional.payments.histories.cancel(connection, { - source: history.source, - password: "some-password", - price: history.price / 5, - reason: "some-reason", - account: banker(), - }); - await validate(i + 1)(cancelled); - }); + // 결제 취소하기 + await ArrayUtil.asyncRepeat(5)(async (i) => { + const cancelled: IPaymentHistory = + await PaymentAPI.functional.payments.histories.cancel(connection, { + source: history.source, + password: "some-password", + price: history.price / 5, + reason: "some-reason", + account: banker(), + }); + await validate(i + 1)(cancelled); + }); }; diff --git a/packages/payment-backend/test/features/monitors/test_api_monitor_health_check.ts b/packages/payment-backend/test/features/monitors/test_api_monitor_health_check.ts index 7ea7e95..877e928 100644 --- a/packages/payment-backend/test/features/monitors/test_api_monitor_health_check.ts +++ b/packages/payment-backend/test/features/monitors/test_api_monitor_health_check.ts @@ -1,7 +1,7 @@ import PaymentAPI from "@samchon/payment-api"; export async function test_api_monitor_health_check( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - await PaymentAPI.functional.monitors.health.get(connection); + await PaymentAPI.functional.monitors.health.get(connection); } diff --git a/packages/payment-backend/test/features/monitors/test_api_monitor_system.ts b/packages/payment-backend/test/features/monitors/test_api_monitor_system.ts index da342d9..cfe3a91 100644 --- a/packages/payment-backend/test/features/monitors/test_api_monitor_system.ts +++ b/packages/payment-backend/test/features/monitors/test_api_monitor_system.ts @@ -3,10 +3,10 @@ import { ISystem } from "@samchon/payment-api/lib/structures/monitors/ISystem"; import typia from "typia"; export async function test_api_monitor_system( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - const system: ISystem = await PaymentAPI.functional.monitors.system.get( - connection, - ); - typia.assert(system); + const system: ISystem = await PaymentAPI.functional.monitors.system.get( + connection, + ); + typia.assert(system); } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_card_payment.ts b/packages/payment-backend/test/features/toss/test_api_toss_card_payment.ts index 14fcb33..3b7f8fb 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_card_payment.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_card_payment.ts @@ -10,147 +10,147 @@ import { PaymentConfiguration } from "../../../src"; import { TossAsset } from "../../../src/services/toss/TossAsset"; export async function test_api_toss_card_payment( - connection: api.IConnection, + connection: api.IConnection, ): Promise { - //---- - // 결제의 원천이 되는 주문 정보 - //---- - /** - * 귀하의 백엔드 서버가 발행한 주문 ID. - */ - const yourOrderId: string = v4(); + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); - /** - * 주문 금액. - */ - const yourOrderPrice: number = 20_000; + /** + * 주문 금액. + */ + const yourOrderPrice: number = 20_000; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 결제 내역 등록 ----------------------------------------------------------- */ - /** - * 아임포트 시뮬레이션 - * - * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 카드 결제를 - * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 - * {@link IIamportPayment.imp_uid} 가 전달된다. - * - * 이 {@link IIamportPayment.imp_uid} 와 귀하의 백엔드에서 직접 생성한 - * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 - * {@link IPaymentHistory} 등록에 사용하도록 하자. - */ - /** - * 토스 페이먼츠 시뮬레이션 - * - * 고객이 프론트 어플리케이션에서, 토스 페이먼츠가 제공하는 팝업 창을 이용, 카드 결제를 - * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 - * {@link ITossPayment.paymentKey} 가 전달된다. - * - * 이 {@link ITossPayment.paymentKey} 와 귀하의 백엔드에서 직접 생성한 - * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 - * {@link IPaymentHistory} 등록에 사용하도록 하자. - */ - const payment: ITossPayment = await toss.functional.v1.payments.key_in( - await TossAsset.connection("test-toss-payments-store-id"), - { - // 카드 정보 - method: "card", - cardNumber: "1111222233334444", - cardExpirationYear: "24", - cardExpirationMonth: "03", + /** + * 아임포트 시뮬레이션 + * + * 고객이 프론트 어플리케이션에서, 아임포트가 제공하는 팝업 창을 이용, 카드 결제를 + * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 + * {@link IIamportPayment.imp_uid} 가 전달된다. + * + * 이 {@link IIamportPayment.imp_uid} 와 귀하의 백엔드에서 직접 생성한 + * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 + * {@link IPaymentHistory} 등록에 사용하도록 하자. + */ + /** + * 토스 페이먼츠 시뮬레이션 + * + * 고객이 프론트 어플리케이션에서, 토스 페이먼츠가 제공하는 팝업 창을 이용, 카드 결제를 + * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 + * {@link ITossPayment.paymentKey} 가 전달된다. + * + * 이 {@link ITossPayment.paymentKey} 와 귀하의 백엔드에서 직접 생성한 + * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 + * {@link IPaymentHistory} 등록에 사용하도록 하자. + */ + const payment: ITossPayment = await toss.functional.v1.payments.key_in( + await TossAsset.connection("test-toss-payments-store-id"), + { + // 카드 정보 + method: "card", + cardNumber: "1111222233334444", + cardExpirationYear: "24", + cardExpirationMonth: "03", - // 주문 정보 - orderId: yourOrderId, - amount: yourOrderPrice, + // 주문 정보 + orderId: yourOrderId, + amount: yourOrderPrice, - // FAKE PROPERTY - __approved: false, - }, - ); - typia.assert(payment); + // FAKE PROPERTY + __approved: false, + }, + ); + typia.assert(payment); - /** - * 결제 이력 등록하기. - * - * 앞서 아임포트의 팝업 창을 이용하여 카드 결제를 진행하고 발급받은 - * {@link IIamportPayment.imp_uid}, 그리고 귀하의 백엔드에서 직접 생성한 - * {@link IIamportPayment.merchant_uid yourOrderId} 를 각각 - * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 로 할당하여 - * {@link IPaymentReservation} 레코드를 발행한다. - * - * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 - * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. - */ - /** - * 결제 이력 등록하기. - * - * 앞서 토스 페이먼츠의 팝업 창을 이용하여 카드 결제를 진행하고 발급받은 - * {@link ITossPayment.paymentKey}, 그리고 귀하의 백엔드에서 직접 생성한 - * {@link ITossPayment.orderId yourOrderId} 를 각각 {@link IPaymentVendor.uid} 와 - * {@link IPaymentSource.id} 로 할당하여 {@link IPaymentReservation} 레코드를 - * 발행한다. - * - * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 - * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. - */ - const history: IPaymentHistory = - await api.functional.payments.histories.store(connection, { - vendor: { - code: "toss.payments", - store_id: "test-toss-payments-store-id", - uid: payment.paymentKey, - }, - source: { - schema: "some-schema", - table: "some-table", - id: yourOrderId, - }, - webhook_url: `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ - api.functional.payments.internal.webhook.METADATA.path - }`, - price: yourOrderPrice, - password: "some-password", - }); - typia.assert(history); + /** + * 결제 이력 등록하기. + * + * 앞서 아임포트의 팝업 창을 이용하여 카드 결제를 진행하고 발급받은 + * {@link IIamportPayment.imp_uid}, 그리고 귀하의 백엔드에서 직접 생성한 + * {@link IIamportPayment.merchant_uid yourOrderId} 를 각각 + * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 로 할당하여 + * {@link IPaymentReservation} 레코드를 발행한다. + * + * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 + * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. + */ + /** + * 결제 이력 등록하기. + * + * 앞서 토스 페이먼츠의 팝업 창을 이용하여 카드 결제를 진행하고 발급받은 + * {@link ITossPayment.paymentKey}, 그리고 귀하의 백엔드에서 직접 생성한 + * {@link ITossPayment.orderId yourOrderId} 를 각각 {@link IPaymentVendor.uid} 와 + * {@link IPaymentSource.id} 로 할당하여 {@link IPaymentReservation} 레코드를 + * 발행한다. + * + * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 + * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. + */ + const history: IPaymentHistory = + await api.functional.payments.histories.store(connection, { + vendor: { + code: "toss.payments", + store_id: "test-toss-payments-store-id", + uid: payment.paymentKey, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourOrderId, + }, + webhook_url: `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ + api.functional.payments.internal.webhook.METADATA.path + }`, + price: yourOrderPrice, + password: "some-password", + }); + typia.assert(history); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 결제 내역 조회하기 ----------------------------------------------------------- */ - /** - * 결제 내역 조회하기 by {@link IPaymentHistory.id}. - * - * 앞서 등록한 결제 이력의 상세 정보를 {@link IPaymentHistory.id} 를 이용하여 조회할 - * 수 있다. 하지만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 - * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const read: IPaymentHistory = await api.functional.payments.histories.at( - connection, - history.id, - { - password: "some-password", - }, - ); - typia.assert(read); - if (read.vendor_code === "toss.payments") read.data.paymentKey; // if condition 을 통한 하위 타입 특정 + /** + * 결제 내역 조회하기 by {@link IPaymentHistory.id}. + * + * 앞서 등록한 결제 이력의 상세 정보를 {@link IPaymentHistory.id} 를 이용하여 조회할 + * 수 있다. 하지만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 + * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const read: IPaymentHistory = await api.functional.payments.histories.at( + connection, + history.id, + { + password: "some-password", + }, + ); + typia.assert(read); + if (read.vendor_code === "toss.payments") read.data.paymentKey; // if condition 을 통한 하위 타입 특정 - /** - * 결제 내역 조회하기 by {@link IPaymentSource}. - * - * 앞서 등록한 결제 이력의 상세 정보는 {@link IPaymentSource} 를 통하여도 조회할 수 - * 있다. 다만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 - * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const gotten: IPaymentHistory = await api.functional.payments.histories.get( - connection, - { - schema: "some-schema", - table: "some-table", - id: yourOrderId, - password: "some-password", - }, - ); - typia.assert(gotten); - if (read.vendor_code === "toss.payments") read.data.paymentKey; // if condition 을 통한 하위 타입 특정 + /** + * 결제 내역 조회하기 by {@link IPaymentSource}. + * + * 앞서 등록한 결제 이력의 상세 정보는 {@link IPaymentSource} 를 통하여도 조회할 수 + * 있다. 다만, 이 때 앞서 결제 이력을 등록할 때 사용했던 비밀번호가 필요하니, 부디 + * 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const gotten: IPaymentHistory = await api.functional.payments.histories.get( + connection, + { + schema: "some-schema", + table: "some-table", + id: yourOrderId, + password: "some-password", + }, + ); + typia.assert(gotten); + if (read.vendor_code === "toss.payments") read.data.paymentKey; // if condition 을 통한 하위 타입 특정 - return typia.assert(gotten); + return typia.assert(gotten); } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel.ts b/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel.ts index 38fa353..05e0a3a 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel.ts @@ -5,11 +5,9 @@ import { validate_payment_cancel } from "../internal/validate_payment_cancel"; import { test_api_toss_card_payment } from "./test_api_toss_card_payment"; export async function test_api_toss_card_payment_cancel( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_toss_card_payment( - connection, - ); - await validate_payment_cancel(connection, history, () => null); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_toss_card_payment(connection); + await validate_payment_cancel(connection, history, () => null); } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel_partial.ts b/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel_partial.ts index 48e82b6..b5e063f 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel_partial.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel_partial.ts @@ -5,11 +5,9 @@ import { validate_payment_cancel_partial } from "../internal/validate_payment_ca import { test_api_toss_card_payment } from "./test_api_toss_card_payment"; export async function test_api_toss_card_payment_cancel_partial( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_toss_card_payment( - connection, - ); - await validate_payment_cancel_partial(connection, history, () => null); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_toss_card_payment(connection); + await validate_payment_cancel_partial(connection, history, () => null); } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_subscription_payment.ts b/packages/payment-backend/test/features/toss/test_api_toss_subscription_payment.ts index a5f3ba0..c010cd5 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_subscription_payment.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_subscription_payment.ts @@ -9,126 +9,122 @@ import { v4 } from "uuid"; import { TossAsset } from "../../../src/services/toss/TossAsset"; export async function test_api_toss_subscription_payment( - connection: api.IConnection, + connection: api.IConnection, ): Promise { - /** - * 귀하의 백엔드 서버가 발행한 식별자 ID. - * - * 아임포트는 토스 페이먼츠와 달리, 간편 결제로 등록한 카드에 자체 식별자를 부여하지 - * 않는다. 따라서 귀하의 백엔드 서버가 발행한 식별자 ID 가 곧, 해당 간편 결제 등록 - * 내역의 유일무일한 식별자인 셈. - */ - const yourSourceId: string = v4(); + /** + * 귀하의 백엔드 서버가 발행한 식별자 ID. + * + * 아임포트는 토스 페이먼츠와 달리, 간편 결제로 등록한 카드에 자체 식별자를 부여하지 + * 않는다. 따라서 귀하의 백엔드 서버가 발행한 식별자 ID 가 곧, 해당 간편 결제 등록 + * 내역의 유일무일한 식별자인 셈. + */ + const yourSourceId: string = v4(); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 간편 결제 카드 등록 ----------------------------------------------------------- */ - /** - * 토스 페이먼츠 시뮬레이션. - * - * 고객이 프론트 어플리케이션에서, 토스 페이먼츠가 제공하는 팝업 창을 이용, 간편 결제 - * 카드를 등록하는 상황을 시뮬레이션을 한다. 고객이 간편 결제 카드 등록을 마치거든, - * 프론트 어플리케이션에 {@link ITossBilling.billingKey} 가 전달된다. - * - * 이 {@link ITossBilling.billingKey} 와 귀하의 백엔드 서버에서 직접 생성한 - * {@link ITossBilling.customerKey yourSourceId} 를 잘 기억해두었다가, 이를 다음 - * 단계인 {@link IPaymentReservation} 등록에 사용하도록 하자. - */ - const billing: ITossBilling = - await toss.functional.v1.billing.authorizations.card.store( - await TossAsset.connection("test-toss-payments-store-id"), - { - customerKey: yourSourceId, - cardNumber: "1111222233334444", - cardExpirationYear: "28", - cardExpirationMonth: "03", - cardPassword: "99", - customerBirthday: "880311", - consumerName: "남정호", - }, - ); - typia.assert(billing); + /** + * 토스 페이먼츠 시뮬레이션. + * + * 고객이 프론트 어플리케이션에서, 토스 페이먼츠가 제공하는 팝업 창을 이용, 간편 결제 + * 카드를 등록하는 상황을 시뮬레이션을 한다. 고객이 간편 결제 카드 등록을 마치거든, + * 프론트 어플리케이션에 {@link ITossBilling.billingKey} 가 전달된다. + * + * 이 {@link ITossBilling.billingKey} 와 귀하의 백엔드 서버에서 직접 생성한 + * {@link ITossBilling.customerKey yourSourceId} 를 잘 기억해두었다가, 이를 다음 + * 단계인 {@link IPaymentReservation} 등록에 사용하도록 하자. + */ + const billing: ITossBilling = + await toss.functional.v1.billing.authorizations.card.store( + await TossAsset.connection("test-toss-payments-store-id"), + { + customerKey: yourSourceId, + cardNumber: "1111222233334444", + cardExpirationYear: "28", + cardExpirationMonth: "03", + cardPassword: "99", + customerBirthday: "880311", + consumerName: "남정호", + }, + ); + typia.assert(billing); - /** - * 간편 결제 수단 등록하기. - * - * 아임포트는 간편 결제 수단에 대하여 별도의 식별자 번호를 부여하지 않는다. 따라서 - * 귀하가 발행하였던 {@link IIamportSubscription.customer_uid yourSourceId} 를 - * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 모두 동일하게 - * 할당하여 {@link IPaymentReservation} 레코드를 발행한다. - * - * 참고로 간편 결제 수단을 등혹할 때 반드시 비밀번호를 설정해야 하는데, 이는 향후 - * 간편 결제 수단을 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 - * 한다. - */ - /** - * 간편 결제 수단 등록하기. - * - * 앞서 토스 페이먼츠의 팝업 창을 이용하여 간편 결제 카드를 등록하고 발급받은 - * {@link ITossBilling.billingKey}, 그리고 귀하의 백엔드 서버에서 직접 생성한 - * {@link ITossBilling.customerKey} 를 각각 {@link IPaymentVendor.uid} 와 - * {@link IPaymentSource.id} 로 할당하여 {@link IPaymentReservation} 레코드를 - * 발행한다. - * - * 참고로 간편 결제 수단을 등록할 때 반드시 비밀번호를 설정해야 하는데, 이는 향후 간편 - * 결제 수단을 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. - */ - const reservation: IPaymentReservation = - await api.functional.payments.reservations.store(connection, { - vendor: { - code: "toss.payments", - store_id: "test-toss-payments-store-id", - uid: billing.billingKey, - }, - source: { - schema: "some-schema", - table: "some-table", - id: yourSourceId, - }, - title: "some-title", - password: "some-password", - }); - typia.assert(reservation); + /** + * 간편 결제 수단 등록하기. + * + * 아임포트는 간편 결제 수단에 대하여 별도의 식별자 번호를 부여하지 않는다. 따라서 + * 귀하가 발행하였던 {@link IIamportSubscription.customer_uid yourSourceId} 를 + * {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 모두 동일하게 + * 할당하여 {@link IPaymentReservation} 레코드를 발행한다. + * + * 참고로 간편 결제 수단을 등혹할 때 반드시 비밀번호를 설정해야 하는데, 이는 향후 + * 간편 결제 수단을 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 + * 한다. + */ + /** + * 간편 결제 수단 등록하기. + * + * 앞서 토스 페이먼츠의 팝업 창을 이용하여 간편 결제 카드를 등록하고 발급받은 + * {@link ITossBilling.billingKey}, 그리고 귀하의 백엔드 서버에서 직접 생성한 + * {@link ITossBilling.customerKey} 를 각각 {@link IPaymentVendor.uid} 와 + * {@link IPaymentSource.id} 로 할당하여 {@link IPaymentReservation} 레코드를 + * 발행한다. + * + * 참고로 간편 결제 수단을 등록할 때 반드시 비밀번호를 설정해야 하는데, 이는 향후 간편 + * 결제 수단을 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. + */ + const reservation: IPaymentReservation = + await api.functional.payments.reservations.store(connection, { + vendor: { + code: "toss.payments", + store_id: "test-toss-payments-store-id", + uid: billing.billingKey, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourSourceId, + }, + title: "some-title", + password: "some-password", + }); + typia.assert(reservation); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 간편 결제 카드 조회하기 ----------------------------------------------------------- */ - /** - * 간편 결제 수단 조회하기 by {@link IPaymentReservation.id}. - * - * 앞서 등록한 간편 결제 수단의 상세 정보를 {@link IPaymentReservation.id} 를 - * 이용하여 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용했던 - * 비밀번호가 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const read: IPaymentReservation = - await api.functional.payments.reservations.at( - connection, - reservation.id, - { - password: "some-password", - }, - ); - typia.assert(read); + /** + * 간편 결제 수단 조회하기 by {@link IPaymentReservation.id}. + * + * 앞서 등록한 간편 결제 수단의 상세 정보를 {@link IPaymentReservation.id} 를 + * 이용하여 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용했던 + * 비밀번호가 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const read: IPaymentReservation = + await api.functional.payments.reservations.at(connection, reservation.id, { + password: "some-password", + }); + typia.assert(read); - // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. - if (read.vendor_code === "toss.payments") read.data.billingKey; + // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. + if (read.vendor_code === "toss.payments") read.data.billingKey; - /** - * 간편 결제 수단 조회하기 by {@link IPaymentSource}. - * - * 앞서 등록한 간편 결제 수단의 상세 정보는 {@link IPaymentSource} 를 통하여도 - * 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용햇던 비밀번호가 - * 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. - */ - const gotten: IPaymentReservation = - await api.functional.payments.reservations.get(connection, { - schema: "some-schema", - table: "some-table", - id: yourSourceId, - password: "some-password", - }); - typia.assert(gotten); + /** + * 간편 결제 수단 조회하기 by {@link IPaymentSource}. + * + * 앞서 등록한 간편 결제 수단의 상세 정보는 {@link IPaymentSource} 를 통하여도 + * 조회할 수 있다. 다만, 이 때 앞서 간편 결제 수단을 등록할 때 사용햇던 비밀번호가 + * 필요하니, 부디 귀하의 백엔드 서버에서 이를 저장하였기 바란다. + */ + const gotten: IPaymentReservation = + await api.functional.payments.reservations.get(connection, { + schema: "some-schema", + table: "some-table", + id: yourSourceId, + password: "some-password", + }); + typia.assert(gotten); - // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. - if (gotten.vendor_code === "toss.payments") gotten.data.cardNumber; + // if condition 과 vendor_code 를 이용해 하위 타입을 특정할 수 있다. + if (gotten.vendor_code === "toss.payments") gotten.data.cardNumber; } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment.ts b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment.ts index add5d05..45c8a09 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment.ts @@ -13,126 +13,125 @@ import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentS import { TossAsset } from "../../../src/services/toss/TossAsset"; export async function test_api_toss_vbank_payment( - connection: api.IConnection, + connection: api.IConnection, ): Promise { - //---- - // 결제의 원천이 되는 주문 정보 - //---- - /** - * 귀하의 백엔드 서버가 발행한 주문 ID. - */ - const yourOrderId: string = v4(); + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); - /** - * 주문 금액. - */ - const yourOrderPrice: number = 19_900; + /** + * 주문 금액. + */ + const yourOrderPrice: number = 19_900; - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 결제 내역 등록 ----------------------------------------------------------- */ - /** - * 토스 페이먼츠 시뮬레이션 - * - * 고객이 프론트 어플리케이션에서, 토스 페이먼츠가 제공하는 팝업 창을 이용, 카드 결제를 - * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 - * {@link ITossPayment.paymentKey} 가 전달된다. - * - * 이 {@link ITossPayment.paymentKey} 와 귀하의 백엔드에서 직접 생성한 - * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 - * {@link IPaymentHistory} 등록에 사용하도록 하자. - */ - const payment: ITossPayment = - await toss.functional.v1.virtual_accounts.store( - await TossAsset.connection("test-toss-payments-store-id"), - { - // 가상 계좌 정보 - method: "virtual-account", - bank: "신한", - customerName: "Samchon", + /** + * 토스 페이먼츠 시뮬레이션 + * + * 고객이 프론트 어플리케이션에서, 토스 페이먼츠가 제공하는 팝업 창을 이용, 카드 결제를 + * 하는 상황을 시뮬레이션 한다. 고객이 카드 결제를 마치거든, 프론트 어플리케이션에 + * {@link ITossPayment.paymentKey} 가 전달된다. + * + * 이 {@link ITossPayment.paymentKey} 와 귀하의 백엔드에서 직접 생성한 + * {@link ITossPayment.orderId yourOrderId} 를 잘 기억해두었다가, 이를 다음 단계인 + * {@link IPaymentHistory} 등록에 사용하도록 하자. + */ + const payment: ITossPayment = await toss.functional.v1.virtual_accounts.store( + await TossAsset.connection("test-toss-payments-store-id"), + { + // 가상 계좌 정보 + method: "virtual-account", + bank: "신한", + customerName: "Samchon", - // 주문 정보 - orderId: yourOrderId, - orderName: "something", - amount: yourOrderPrice, + // 주문 정보 + orderId: yourOrderId, + orderName: "something", + amount: yourOrderPrice, - // 고의 미승인 처리 - __approved: false, - }, - ); - typia.assert(payment); + // 고의 미승인 처리 + __approved: false, + }, + ); + typia.assert(payment); - /** - * 웹훅 URL 설정하기. - * - * 웹훅 URL 을 테스트용 API 주소, internal.webhook 으로 설정. - */ - const webhook_url: string = `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ - api.functional.payments.internal.webhook.METADATA.path - }`; + /** + * 웹훅 URL 설정하기. + * + * 웹훅 URL 을 테스트용 API 주소, internal.webhook 으로 설정. + */ + const webhook_url: string = `http://127.0.0.1:${PaymentConfiguration.API_PORT()}${ + api.functional.payments.internal.webhook.METADATA.path + }`; - /** - * 결제 이력 등록하기. - * - * 앞서 토스 페이먼츠의 팝업 창을 이용하여 가상 계좌 결제를 진행하고 발급받은 - * {@link ITossPayment.paymentKey}, 그리고 귀하의 백엔드에서 직접 생성한 - * {@link ITossPayment.orderId yourOrderId} 를 각각 {@link IPaymentVendor.uid} 와 - * {@link IPaymentSource.id} 로 할당하여 {@link IPaymentReservation} 레코드를 - * 발행한다. - * - * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 - * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. - */ - const history: IPaymentHistory = - await api.functional.payments.histories.store(connection, { - vendor: { - code: "toss.payments", - store_id: "test-toss-payments-store-id", - uid: payment.paymentKey, - }, - source: { - schema: "some-schema", - table: "some-table", - id: yourOrderId, - }, - webhook_url, // 테스트용 웹훅 URL - price: yourOrderPrice, - password: "some-password", - }); - typia.assert(history); + /** + * 결제 이력 등록하기. + * + * 앞서 토스 페이먼츠의 팝업 창을 이용하여 가상 계좌 결제를 진행하고 발급받은 + * {@link ITossPayment.paymentKey}, 그리고 귀하의 백엔드에서 직접 생성한 + * {@link ITossPayment.orderId yourOrderId} 를 각각 {@link IPaymentVendor.uid} 와 + * {@link IPaymentSource.id} 로 할당하여 {@link IPaymentReservation} 레코드를 + * 발행한다. + * + * 참고로 결제 이력을 등록할 때 반드시 비밀번호를 설정해야 하는데, 향후 결제 이력을 + * 조회할 때 필요하니, 이를 반드시 귀하의 백엔드 서버에 저장해두도록 한다. + */ + const history: IPaymentHistory = + await api.functional.payments.histories.store(connection, { + vendor: { + code: "toss.payments", + store_id: "test-toss-payments-store-id", + uid: payment.paymentKey, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourOrderId, + }, + webhook_url, // 테스트용 웹훅 URL + price: yourOrderPrice, + password: "some-password", + }); + typia.assert(history); - /* ----------------------------------------------------------- + /* ----------------------------------------------------------- 웹훅 이벤트 리스닝 ----------------------------------------------------------- */ - /** - * 입금 시뮬레이션하기. - * - * 고객이 자신 앞을 발급된 계좌에, 결제 금액을 입금하는 상황 시뮬레이션. - */ - await toss.functional.internal.deposit( - await TossAsset.connection("test-toss-payments-store-id"), - payment.paymentKey, - ); + /** + * 입금 시뮬레이션하기. + * + * 고객이 자신 앞을 발급된 계좌에, 결제 금액을 입금하는 상황 시뮬레이션. + */ + await toss.functional.internal.deposit( + await TossAsset.connection("test-toss-payments-store-id"), + payment.paymentKey, + ); - // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. - await sleep_for(1_000); + // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. + await sleep_for(1_000); - /** - * 웹흑 리스닝 시뮬레이션. - * - * 귀하의 백엔드 서버가 웹훅 이벤트를 수신한 상황을 가정한다. - */ - const webhook: IPaymentWebhookHistory | undefined = - FakePaymentStorage.webhooks.back(); + /** + * 웹흑 리스닝 시뮬레이션. + * + * 귀하의 백엔드 서버가 웹훅 이벤트를 수신한 상황을 가정한다. + */ + const webhook: IPaymentWebhookHistory | undefined = + FakePaymentStorage.webhooks.back(); - // 이하 웹훅 데이터를 통한 입금 여부 검증 - TestValidator.equals("webhook")(!!webhook)(true); - TestValidator.equals("history.id")(history.id)(webhook?.current.id); - TestValidator.equals("paid_at")(!!webhook?.previous.paid_at)(false); - TestValidator.equals("paid_at")(!!webhook?.current.paid_at)(true); + // 이하 웹훅 데이터를 통한 입금 여부 검증 + TestValidator.equals("webhook")(!!webhook)(true); + TestValidator.equals("history.id")(history.id)(webhook?.current.id); + TestValidator.equals("paid_at")(!!webhook?.previous.paid_at)(false); + TestValidator.equals("paid_at")(!!webhook?.current.paid_at)(true); - // 웹훅 데이터 삭제 - FakePaymentStorage.webhooks.pop_back(); + // 웹훅 데이터 삭제 + FakePaymentStorage.webhooks.pop_back(); - return history; + return history; } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel.ts b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel.ts index 61ee14d..3556d64 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel.ts @@ -5,16 +5,16 @@ import { validate_payment_cancel } from "../internal/validate_payment_cancel"; import { test_api_toss_vbank_payment } from "./test_api_toss_vbank_payment"; export async function test_api_toss_vbank_payment_cancel( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_toss_vbank_payment( - connection, - ); - await validate_payment_cancel(connection, history, () => ({ - bank: "신한은행", - account: "110123456789", - holder: "홍길동", - mobile: "01012345678", - })); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_toss_vbank_payment( + connection, + ); + await validate_payment_cancel(connection, history, () => ({ + bank: "신한은행", + account: "110123456789", + holder: "홍길동", + mobile: "01012345678", + })); } diff --git a/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel_partial.ts b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel_partial.ts index c217af0..da6aeef 100644 --- a/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel_partial.ts +++ b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel_partial.ts @@ -5,16 +5,16 @@ import { validate_payment_cancel_partial } from "../internal/validate_payment_ca import { test_api_toss_vbank_payment } from "./test_api_toss_vbank_payment"; export async function test_api_toss_vbank_payment_cancel_partial( - connection: PaymentAPI.IConnection, + connection: PaymentAPI.IConnection, ): Promise { - // 카드 결제하기 - const history: IPaymentHistory = await test_api_toss_vbank_payment( - connection, - ); - await validate_payment_cancel_partial(connection, history, () => ({ - bank: "신한은행", - account: "110123456789", - holder: "홍길동", - mobile: "01012345678", - })); + // 카드 결제하기 + const history: IPaymentHistory = await test_api_toss_vbank_payment( + connection, + ); + await validate_payment_cancel_partial(connection, history, () => ({ + bank: "신한은행", + account: "110123456789", + holder: "홍길동", + mobile: "01012345678", + })); } diff --git a/packages/payment-backend/test/index.ts b/packages/payment-backend/test/index.ts index ff39588..f41cebf 100644 --- a/packages/payment-backend/test/index.ts +++ b/packages/payment-backend/test/index.ts @@ -5,136 +5,135 @@ import { Singleton, randint } from "tstl"; import { sleep_for } from "tstl/thread/global"; import { - PaymentBackend, - PaymentConfiguration, - PaymentGlobal, - PaymentSetupWizard, - PaymentUpdator, + PaymentBackend, + PaymentConfiguration, + PaymentGlobal, + PaymentSetupWizard, + PaymentUpdator, } from "../src"; import { ArgumentParser } from "../src/utils/ArgumentParser"; import { ErrorUtil } from "../src/utils/ErrorUtil"; interface IOptions { - reset: boolean; - include?: string[]; - exclude?: string[]; - trace: boolean; + reset: boolean; + include?: string[]; + exclude?: string[]; + trace: boolean; } const getOptions = () => - ArgumentParser.parse(async (command, prompt, action) => { - command.option("--reset ", "reset local DB or not"); - command.option("--include ", "include feature files"); - command.option("--exclude ", "exclude feature files"); - command.option("--trace ", "trace detailed errors"); - - return action(async (options) => { - if (typeof options.reset === "string") - options.reset = options.reset === "true"; - options.reset ??= await prompt.boolean("reset")("Reset local DB"); - options.trace = options.trace !== ("false" as any); - return options as IOptions; - }); + ArgumentParser.parse(async (command, prompt, action) => { + command.option("--reset ", "reset local DB or not"); + command.option("--include ", "include feature files"); + command.option("--exclude ", "exclude feature files"); + command.option("--trace ", "trace detailed errors"); + + return action(async (options) => { + if (typeof options.reset === "string") + options.reset = options.reset === "true"; + options.reset ??= await prompt.boolean("reset")("Reset local DB"); + options.trace = options.trace !== ("false" as any); + return options as IOptions; }); + }); function cipher(val: number): string { - if (val < 10) return "0" + val; - else return String(val); + if (val < 10) return "0" + val; + else return String(val); } async function handle_error(exp: any): Promise { - try { - const date: Date = new Date(); - const fileName: string = `${date.getFullYear()}${cipher( - date.getMonth() + 1, - )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( - date.getMinutes(), - )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; - const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); - - await directory.get(); - await fs.promises.writeFile( - `${__dirname}/../../assets/logs/errors/${fileName}.log`, - content, - "utf8", - ); - } catch {} + try { + const date: Date = new Date(); + const fileName: string = `${date.getFullYear()}${cipher( + date.getMonth() + 1, + )}${cipher(date.getDate())}${cipher(date.getHours())}${cipher( + date.getMinutes(), + )}${cipher(date.getSeconds())}.${randint(0, Number.MAX_SAFE_INTEGER)}`; + const content: string = JSON.stringify(ErrorUtil.toJSON(exp), null, 4); + + await directory.get(); + await fs.promises.writeFile( + `${__dirname}/../../assets/logs/errors/${fileName}.log`, + content, + "utf8", + ); + } catch {} } async function main(): Promise { - // UNEXPECTED ERRORS - global.process.on("uncaughtException", handle_error); - global.process.on("unhandledRejection", handle_error); - - // CONFIGURE - const options: IOptions = await getOptions(); - PaymentGlobal.testing = true; - - if (options.reset) { - await StopWatch.trace("Reset DB")(PaymentSetupWizard.schema); - await StopWatch.trace("Seed Data")(PaymentSetupWizard.seed); - } - - // OPEN SERVER - const updator = await PaymentUpdator.master(); - const backend: PaymentBackend = new PaymentBackend(); - await backend.open(); - - // DO TEST - const connection: api.IConnection = { - host: `http://127.0.0.1:${PaymentConfiguration.API_PORT()}`, - encryption: { - key: PaymentConfiguration.ENCRYPTION_PASSWORD().key, - iv: PaymentConfiguration.ENCRYPTION_PASSWORD().iv, - }, - }; - const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ - prefix: "test", - parameters: () => [ - { - host: connection.host, - encryption: connection.encryption, - }, - ], - filter: (func) => - (!options.include?.length || - (options.include ?? []).some((str) => func.includes(str))) && - (!options.exclude?.length || - (options.exclude ?? []).every((str) => !func.includes(str))), - })(__dirname + "/features"); - - // TERMINATE - await sleep_for(2500); // WAIT FOR BACKGROUND EVENTS - await backend.close(); - await updator.close(); - - const exceptions: Error[] = report.executions - .filter((exec) => exec.error !== null) - .map((exec) => exec.error!); - if (exceptions.length === 0) { - console.log("Success"); - console.log("Elapsed time", report.time.toLocaleString(), `ms`); - } else { - if (options.trace !== false) - for (const exp of exceptions) console.log(exp); - console.log("Failed"); - console.log("Elapsed time", report.time.toLocaleString(), `ms`); - process.exit(-1); - } + // UNEXPECTED ERRORS + global.process.on("uncaughtException", handle_error); + global.process.on("unhandledRejection", handle_error); + + // CONFIGURE + const options: IOptions = await getOptions(); + PaymentGlobal.testing = true; + + if (options.reset) { + await StopWatch.trace("Reset DB")(PaymentSetupWizard.schema); + await StopWatch.trace("Seed Data")(PaymentSetupWizard.seed); + } + + // OPEN SERVER + const updator = await PaymentUpdator.master(); + const backend: PaymentBackend = new PaymentBackend(); + await backend.open(); + + // DO TEST + const connection: api.IConnection = { + host: `http://127.0.0.1:${PaymentConfiguration.API_PORT()}`, + encryption: { + key: PaymentConfiguration.ENCRYPTION_PASSWORD().key, + iv: PaymentConfiguration.ENCRYPTION_PASSWORD().iv, + }, + }; + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + prefix: "test", + parameters: () => [ + { + host: connection.host, + encryption: connection.encryption, + }, + ], + filter: (func) => + (!options.include?.length || + (options.include ?? []).some((str) => func.includes(str))) && + (!options.exclude?.length || + (options.exclude ?? []).every((str) => !func.includes(str))), + })(__dirname + "/features"); + + // TERMINATE + await sleep_for(2500); // WAIT FOR BACKGROUND EVENTS + await backend.close(); + await updator.close(); + + const exceptions: Error[] = report.executions + .filter((exec) => exec.error !== null) + .map((exec) => exec.error!); + if (exceptions.length === 0) { + console.log("Success"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + } else { + if (options.trace !== false) for (const exp of exceptions) console.log(exp); + console.log("Failed"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + process.exit(-1); + } } main().catch((exp) => { - console.log(exp); - process.exit(-1); + console.log(exp); + process.exit(-1); }); const directory = new Singleton(async () => { - await mkdir(`${__dirname}/../../assets`); - await mkdir(`${__dirname}/../../assets/logs`); - await mkdir(`${__dirname}/../../assets/logs/errors`); + await mkdir(`${__dirname}/../../assets`); + await mkdir(`${__dirname}/../../assets/logs`); + await mkdir(`${__dirname}/../../assets/logs/errors`); }); async function mkdir(path: string): Promise { - try { - await fs.promises.mkdir(path); - } catch {} + try { + await fs.promises.mkdir(path); + } catch {} } diff --git a/packages/payment-backend/test/manual/password.ts b/packages/payment-backend/test/manual/password.ts index 9b4b0ce..1b48974 100644 --- a/packages/payment-backend/test/manual/password.ts +++ b/packages/payment-backend/test/manual/password.ts @@ -3,10 +3,10 @@ import { randint } from "tstl"; const CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const LETTERS: string = "0123456789" + CHARACTERS; const alphaNumeric = (length: number) => - new Array(length) - .fill("") - .map(() => LETTERS[randint(0, LETTERS.length - 1)]) - .join(""); + new Array(length) + .fill("") + .map(() => LETTERS[randint(0, LETTERS.length - 1)]) + .join(""); console.log(alphaNumeric(32)); console.log(alphaNumeric(16)); diff --git a/packages/payment-backend/test/tsconfig.json b/packages/payment-backend/test/tsconfig.json index 87bfe67..ba053f3 100644 --- a/packages/payment-backend/test/tsconfig.json +++ b/packages/payment-backend/test/tsconfig.json @@ -1,7 +1,10 @@ { - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../bin", - }, - "include": [".", "../src"] - } \ No newline at end of file + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../bin", + }, + "include": [ + ".", + "../src", + ], +} \ No newline at end of file diff --git a/packages/payment-backend/tsconfig.json b/packages/payment-backend/tsconfig.json index f013ab0..c2605fb 100644 --- a/packages/payment-backend/tsconfig.json +++ b/packages/payment-backend/tsconfig.json @@ -41,7 +41,6 @@ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ "paths": { - "@modules/*": ["./src/modules/*"], "@samchon/payment-api/lib/*": ["./src/api/*"], "@samchon/payment-api/lib/": ["./src/api"], "@samchon/payment-api": ["./src/api"], diff --git a/packages/payment-backend/webpack.config.js b/packages/payment-backend/webpack.config.js new file mode 100644 index 0000000..fe84a34 --- /dev/null +++ b/packages/payment-backend/webpack.config.js @@ -0,0 +1,79 @@ +const path = require("path"); + +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const WriteFilePlugin = require("write-file-webpack-plugin"); +const { IgnorePlugin } = require("webpack"); + +const lazyImports = [ + "@fastify/static", + "@fastify/view", + "@nestjs/microservices", + "@nestjs/websockets", + "class-transformer", + "class-validator", +]; + +// @reference https://tech-blog.s-yoshiki.com/entry/297 +module.exports = { + // CUSTOMIZE HERE + entry: { + server: "./src/executable/server.ts", + "updator-master": "./src/executable/updator-master.ts", + }, + output: { + path: path.join(__dirname, "dist"), + filename: "[name].js", + }, + optimization: { + minimize: true, + }, + + // JUST KEEP THEM + mode: "production", + target: "node", + module: { + rules: [ + { + test: /\.ts$/, + exclude: /node_modules/, + loader: "ts-loader", + }, + ], + }, + resolve: { + extensions: [".tsx", ".ts", ".js"], + }, + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + { + from: ".env", + to: ".env", + }, + { + from: "./src/schema.prisma", + to: "./schema.prisma", + }, + { + from: "./node_modules/.prisma/client/*.node", + to() { + return Promise.resolve("[name][ext]"); + }, + }, + ], + }), + new WriteFilePlugin(), + new IgnorePlugin({ + checkResource(resource) { + if (lazyImports.some((modulo) => resource.startsWith(modulo))) { + try { + require.resolve(resource); + } catch (err) { + return true; + } + } + return false; + }, + }), + ], +}; diff --git a/packages/toss-payments-server-api/package.json b/packages/toss-payments-server-api/package.json index fe03ab9..eee9947 100644 --- a/packages/toss-payments-server-api/package.json +++ b/packages/toss-payments-server-api/package.json @@ -1,6 +1,6 @@ { "name": "toss-payments-server-api", - "version": "5.0.6", + "version": "5.1.0", "description": "API for Toss Payments Server", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -34,7 +34,7 @@ "typescript": "^5.2.2" }, "dependencies": { - "@nestia/fetcher": "^2.3.4", - "typia": "^5.2.4" + "@nestia/fetcher": "^2.3.9", + "typia": "^5.2.6" } } \ No newline at end of file diff --git a/packages/toss-payments-server-api/swagger.json b/packages/toss-payments-server-api/swagger.json index 65fae13..e3aeda2 100644 --- a/packages/toss-payments-server-api/swagger.json +++ b/packages/toss-payments-server-api/swagger.json @@ -13,7 +13,7 @@ "info": { "title": "Toss Payments API", "description": "Built by [fake-toss-payments-server](https://github.com/samchon/payments/tree/master/packages/toss-payments-server-api) with [nestia](https://github.com/samchon/nestia)", - "version": "5.0.6", + "version": "5.1.0", "license": { "name": "MIT" } @@ -178,7 +178,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts", "textSpan": { - "start": 457, + "start": 415, "length": 19 } } @@ -310,7 +310,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts", "textSpan": { - "start": 457, + "start": 415, "length": 19 } } @@ -528,7 +528,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts", "textSpan": { - "start": 164, + "start": 145, "length": 19 } } @@ -704,7 +704,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts", "textSpan": { - "start": 2399, + "start": 2071, "length": 19 } } @@ -802,7 +802,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts", "textSpan": { - "start": 2399, + "start": 2071, "length": 19 } } @@ -906,7 +906,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts", "textSpan": { - "start": 2399, + "start": 2071, "length": 19 } } @@ -1124,7 +1124,7 @@ "target": { "fileName": "D:/github/samchon/payments/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts", "textSpan": { - "start": 2399, + "start": 2071, "length": 19 } } diff --git a/prettier.config.js b/prettier.config.js index a9391a4..9735d66 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -2,12 +2,9 @@ module.exports = { parser: "typescript", printWidth: 80, semi: true, - tabWidth: 4, + tabWidth: 2, trailingComma: "all", - importOrder: [ - "", - "^[./]", - ], + importOrder: ["", "^[./]"], importOrderSeparation: true, importOrderSortSpecifiers: true, importOrderParserPlugins: ["decorators-legacy", "typescript"],