From 89ef7974684fff26616396da2a801238e3053efe Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Wed, 20 Sep 2023 16:46:46 +0900 Subject: [PATCH] Merge payment repositories into one --- .eslintrc.cjs | 5 +- .eslintrc.test.cjs | 5 - .github/PULL_REQUEST_TEMPLATE.md | 4 +- .github/workflows/build.yml | 18 +- .gitignore | 6 +- .prettierignore | 11 +- .vscode/launch.json | 48 + .vscode/settings.json | 13 +- README.md | 361 +- deploy/index.js | 10 + deploy/publish.js | 77 + package.json | 101 +- packages/api/package.json | 27 - packages/api/swagger.json | 3797 ----------------- packages/api/tsconfig.json | 74 - .../fake-iamport-server/LICENSE | 0 packages/fake-iamport-server/README.md | 396 ++ packages/fake-iamport-server/nestia.config.ts | 34 + packages/fake-iamport-server/package.json | 87 + .../src/FakeIamportBackend.ts | 59 + .../src/FakeIamportConfiguration.ts | 114 + .../fake-iamport-server/src}/api/HttpError.ts | 0 .../src}/api/IConnection.ts | 0 .../src/api/IamportConnector.ts | 84 + .../fake-iamport-server/src}/api/Primitive.ts | 0 .../api/functional/certifications/index.ts | 158 + .../functional/certifications/otp/index.ts | 195 + .../src/api/functional/index.ts | 13 + .../src/api/functional/internal/index.ts | 155 + .../src/api/functional/payments/index.ts | 180 + .../src/api/functional/receipts/index.ts | 233 + .../functional/subscribe/customers/index.ts | 248 ++ .../src/api/functional/subscribe/index.ts | 8 + .../functional/subscribe/payments/index.ts | 198 + .../src/api/functional/users/index.ts | 98 + .../src/api/functional/vbanks/index.ts | 171 + packages/fake-iamport-server/src/api/index.ts | 5 + .../fake-iamport-server/src/api/module.ts | 6 + .../src/api/structures/IIamportCardPayment.ts | 36 + .../api/structures/IIamportCertification.ts | 198 + .../src/api/structures/IIamportPayment.ts | 211 + .../api/structures/IIamportPaymentCancel.ts | 82 + .../src/api/structures/IIamportReceipt.ts | 135 + .../src/api/structures/IIamportResponse.ts | 23 + .../api/structures/IIamportSubscription.ts | 169 + .../api/structures/IIamportTransferPayment.ts | 19 + .../src/api/structures/IIamportUser.ts | 54 + .../api/structures/IIamportVBankPayment.ts | 128 + .../src}/api/typings/Atomic.ts | 8 +- .../src}/api/typings/Writable.ts | 8 +- .../src}/api/utils/NestiaSimulator.ts | 0 .../FakeIamportCertificationsController.ts | 163 + .../FakeIamportInternalController.ts | 72 + .../FakeIamportPaymentsController.ts | 65 + .../FakeIamportReceiptsController.ts | 111 + .../controllers/FakeIamportUsersController.ts | 33 + .../FakeIamportVbanksController.ts | 129 + ...FakeIamportSubscribeCustomersController.ts | 121 + .../FakeIamportSubscribePaymentsController.ts | 198 + .../src/executable/server.ts | 59 + packages/fake-iamport-server/src/index.ts | 5 + packages/fake-iamport-server/src/module.ts | 2 + .../providers/FakeIamportPaymentProvider.ts | 83 + .../providers/FakeIamportResponseProvider.ts | 13 + .../src/providers/FakeIamportStorage.ts | 24 + .../FakeIamportSubscriptionProvider.ts | 33 + .../src/providers/FakeIamportUserAuth.ts | 37 + .../src/utils/AdvancedRandomGenerator.ts | 16 + .../src}/utils/DateUtil.ts | 0 .../src/utils/ErrorUtil.ts | 9 + .../src}/utils/Terminal.ts | 0 .../src/utils/VolatileMap.ts | 86 + .../test/features/test_fake_card_payment.ts | 78 + .../features/test_fake_card_payment_cancel.ts | 69 + .../test_fake_card_payment_cancel_over.ts | 39 + .../test_fake_card_payment_cancel_partial.ts | 60 + .../test/features/test_fake_certification.ts | 61 + .../test/features/test_fake_receipt.ts | 38 + .../test_fake_subscription_payment_again.ts | 91 + .../test_fake_subscription_payment_cancel.ts | 68 + ...t_fake_subscription_payment_cancel_over.ts | 38 + ...ake_subscription_payment_cancel_partial.ts | 59 + .../test_fake_subscription_payment_onetime.ts | 106 + .../test/features/test_fake_vbank_payment.ts | 127 + .../test_fake_vbank_payment_cancel.ts | 73 + .../test_fake_vbank_payment_cancel_over.ts | 39 + .../test_fake_vbank_payment_cancel_partial.ts | 64 + ...ake_vbank_payment_cancel_without_refund.ts | 26 + packages/fake-iamport-server/test/index.ts | 51 + .../fake-iamport-server/test}/tsconfig.json | 0 packages/fake-iamport-server/tsconfig.json | 80 + .../LICENSE | 0 .../README.md | 0 .../nestia.config.ts | 7 +- .../fake-toss-payments-server/package.json | 91 + .../src}/FakeTossBackend.ts | 2 +- .../src}/FakeTossConfiguration.ts | 2 +- .../src/api/HttpError.ts | 1 + .../src/api/IConnection.ts | 1 + .../src/api/Primitive.ts | 1 + .../src}/api/functional/index.ts | 0 .../src}/api/functional/internal/index.ts | 8 +- .../v1/billing/authorizations/card/index.ts | 4 +- .../v1/billing/authorizations/index.ts | 4 +- .../src}/api/functional/v1/billing/index.ts | 4 +- .../api/functional/v1/cash_receipts/index.ts | 8 +- .../src}/api/functional/v1/index.ts | 0 .../src}/api/functional/v1/payments/index.ts | 16 +- .../functional/v1/virtual_accounts/index.ts | 4 +- .../src}/api/index.ts | 5 +- .../src}/api/module.ts | 2 +- .../src}/api/structures/ITossBilling.ts | 59 +- .../src}/api/structures/ITossCardPayment.ts | 72 +- .../src}/api/structures/ITossCashReceipt.ts | 29 +- .../structures/ITossGiftCertificatePayment.ts | 15 +- .../api/structures/ITossMobilePhonePayment.ts | 17 +- .../src}/api/structures/ITossPayment.ts | 79 +- .../src}/api/structures/ITossPaymentCancel.ts | 20 +- .../api/structures/ITossPaymentWebhook.ts | 27 +- .../api/structures/ITossTransferPayment.ts | 15 +- .../structures/ITossVirtualAccountPayment.ts | 64 +- .../src/api/typings/Atomic.ts | 14 + .../src/api/typings/Writable.ts | 12 + .../src/api/utils/NestiaSimulator.ts | 70 + .../controllers/FakeTossBillingController.ts | 9 +- .../FakeTossCashReceiptsController.ts | 7 +- .../controllers/FakeTossInternalController.ts | 5 +- .../controllers/FakeTossPaymentsController.ts | 9 +- .../FakeTossVirtualAccountsController.ts | 5 +- .../src}/executable/server.ts | 0 .../fake-toss-payments-server/src}/index.ts | 0 .../fake-toss-payments-server/src}/module.ts | 0 .../src}/providers/FakeTossPaymentProvider.ts | 3 +- .../src}/providers/FakeTossStorage.ts | 0 .../src}/providers/FakeTossUserAuth.ts | 0 .../src}/providers/FakeTossWebhookProvider.ts | 0 .../src/utils/DateUtil.ts | 129 + .../src}/utils/ErrorUtil.ts | 0 .../src/utils/Terminal.ts | 18 + .../src}/utils/VolatileMap.ts | 0 .../internal/validate_fake_payment_cancel.ts | 5 +- .../validate_fake_payment_cancel_over.ts | 3 +- .../validate_fake_payment_cancel_partial.ts | 5 +- .../features/test_fake_billing_payment.ts | 5 +- .../test_fake_billing_payment_cancel.ts | 0 .../test_fake_billing_payment_cancel_over.ts | 0 ...est_fake_billing_payment_cancel_partial.ts | 0 .../test}/features/test_fake_card_payment.ts | 5 +- .../features/test_fake_card_payment_cancel.ts | 0 .../test_fake_card_payment_cancel_over.ts | 0 .../test_fake_card_payment_cancel_partial.ts | 0 .../test}/features/test_fake_cash_receipt.ts | 3 +- .../features/test_fake_storage_capacity.ts | 7 +- .../test_fake_storage_expiration_time.ts | 7 +- .../test_fake_virtual_account_payment.ts | 5 +- ...est_fake_virtual_account_payment_cancel.ts | 0 ...ake_virtual_account_payment_cancel_over.ts | 0 ..._virtual_account_payment_cancel_partial.ts | 0 .../fake-toss-payments-server/test}/index.ts | 0 .../test}/internal/AdvancedRandomGenerator.ts | 0 .../test}/internal/TestConnection.ts | 0 .../test/tsconfig.json | 7 + .../fake-toss-payments-server/tsconfig.json | 0 packages/iamport-server-api/README.md | 38 + packages/iamport-server-api/package.json | 39 + packages/iamport-server-api/tsconfig.json | 97 + packages/payment-api/README.md | 38 + packages/payment-api/package.json | 39 + packages/payment-api/tsconfig.json | 97 + packages/payment-backend/.env | 26 + packages/payment-backend/INFRASTRUCTURE.md | 164 + packages/payment-backend/LICENSE | 21 + packages/payment-backend/README.md | 888 ++++ packages/payment-backend/docs/ERD.md | 225 + packages/payment-backend/nestia.config.ts | 12 + packages/payment-backend/package.json | 105 + packages/payment-backend/src/PaymentAsset.ts | 19 + .../payment-backend/src/PaymentBackend.ts | 120 + .../src/PaymentConfiguration.ts | 104 + packages/payment-backend/src/PaymentGlobal.ts | 128 + .../payment-backend/src/PaymentSetupWizard.ts | 26 + .../payment-backend/src/PaymentUpdator.ts | 105 + packages/payment-backend/src/api/HttpError.ts | 1 + .../payment-backend/src/api/IConnection.ts | 1 + packages/payment-backend/src/api/Primitive.ts | 1 + .../src/api/functional/index.ts | 8 + .../api/functional/monitors/health/index.ts | 50 + .../src/api/functional/monitors/index.ts | 9 + .../functional/monitors/performance/index.ts | 62 + .../api/functional/monitors/system/index.ts | 62 + .../functional/payments/histories/index.ts | 331 ++ .../src/api/functional/payments/index.ts | 10 + .../api/functional/payments/internal/index.ts | 78 + .../functional/payments/reservations/index.ts | 250 ++ .../api/functional/payments/webhooks/index.ts | 145 + packages/payment-backend/src/api/index.ts | 5 + packages/payment-backend/src/api/module.ts | 5 + .../src/api/structures/common/IEntity.ts | 15 + .../api/structures/monitors/IPerformance.ts | 5 + .../src/api/structures/monitors/ISystem.ts | 75 + .../payments/IPaymentCancelHistory.ts | 101 + .../structures/payments/IPaymentHistory.ts | 171 + .../payments/IPaymentReservation.ts | 116 + .../api/structures/payments/IPaymentSource.ts | 53 + .../api/structures/payments/IPaymentVendor.ts | 51 + .../structures/payments/IPaymentWebhook.ts | 44 + .../payments/IPaymentWebhookHistory.ts | 57 + .../payment-backend/src/api/typings/Atomic.ts | 14 + .../src/api/typings/Writable.ts | 12 + .../src/api/utils/NestiaSimulator.ts | 70 + .../controllers/monitors/HealthController.ts | 7 + .../monitors/PerformanceController.ts | 16 + .../controllers/monitors/SystemController.ts | 19 + .../payments/PaymentHistoriesController.ts | 77 + .../payments/PaymentInternalController.ts | 16 + .../payments/PaymentReservationsController.ts | 55 + .../payments/PaymentWebhooksController.ts | 60 + .../payment-backend/src/executable/master.ts | 9 + .../payment-backend/src/executable/monitor.ts | 30 + .../payment-backend/src/executable/schema.ts | 84 + .../payment-backend/src/executable/server.ts | 68 + .../payment-backend/src/executable/update.ts | 57 + .../src/executable/updator-master.ts | 10 + .../src/executable/updator-slave.ts | 9 + packages/payment-backend/src/index.ts | 5 + .../20230919094622_init/migration.sql | 109 + .../src/migrations/migration_lock.toml | 3 + packages/payment-backend/src/module.ts | 6 + .../payment-backend/src/modules/express.ts | 3 + .../payment-backend/src/modules/nestjs.ts | 3 + .../src/providers/monitors/SystemProvider.ts | 54 + .../providers/payments/FakePaymentStorage.ts | 6 + .../payments/PaymentCancelHistoryProvider.ts | 109 + .../payments/PaymentHistoryProvider.ts | 205 + .../payments/PaymentReservationProvider.ts | 119 + .../payments/PaymentWebhookProvider.ts | 113 + .../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 + packages/payment-backend/src/schema.prisma | 334 ++ .../src/services/iamport/IamportAsset.ts | 38 + .../services/iamport/IamportPaymentService.ts | 82 + .../src/services/toss/TossAsset.ts | 28 + .../toss/TossPaymentBillingService.ts | 39 + .../services/toss/TossPaymentCardService.ts | 16 + .../src/services/toss/TossPaymentService.ts | 99 + .../toss/TossPaymentVirtualAccountService.ts | 16 + .../src/utils/ArgumentParser.ts | 79 + .../payment-backend/src/utils/BcryptUtil.ts | 11 + .../payment-backend/src/utils/DateUtil.ts | 100 + .../payment-backend/src/utils/ErrorUtil.ts | 59 + .../payment-backend/src/utils/Terminal.ts | 18 + .../payment-backend/src/utils/TokenManager.ts | 90 + .../iamport/test_api_iamport_card_payment.ts | 128 + .../test_api_iamport_card_payment_cancel.ts | 15 + ...api_iamport_card_payment_cancel_partial.ts | 15 + .../test_api_iamport_subscription_payment.ts | 125 + .../iamport/test_api_iamport_vbank_payment.ts | 141 + .../test_api_iamport_vbank_payment_cancel.ts | 20 + ...pi_iamport_vbank_payment_cancel_partial.ts | 20 + .../internal/validate_payment_cancel.ts | 60 + .../validate_payment_cancel_partial.ts | 76 + .../monitors/test_api_monitor_health_check.ts | 7 + .../monitors/test_api_monitor_system.ts | 12 + .../toss/test_api_toss_card_payment.ts | 156 + .../toss/test_api_toss_card_payment_cancel.ts | 15 + ...st_api_toss_card_payment_cancel_partial.ts | 15 + .../test_api_toss_subscription_payment.ts | 134 + .../toss/test_api_toss_vbank_payment.ts | 147 + .../test_api_toss_vbank_payment_cancel.ts | 20 + ...t_api_toss_vbank_payment_cancel_partial.ts | 20 + packages/payment-backend/test/index.ts | 139 + .../payment-backend/test/manual/password.ts | 13 + packages/payment-backend/test/tsconfig.json | 7 + packages/payment-backend/tsconfig.json | 77 + packages/toss-payments-server-api/README.md | 38 + .../toss-payments-server-api/package.json | 39 + .../toss-payments-server-api/tsconfig.json | 97 + prettier.config.js | 1 - 280 files changed, 14675 insertions(+), 4653 deletions(-) delete mode 100644 .eslintrc.test.cjs create mode 100644 .vscode/launch.json create mode 100644 deploy/index.js create mode 100644 deploy/publish.js delete mode 100644 packages/api/package.json delete mode 100644 packages/api/swagger.json delete mode 100644 packages/api/tsconfig.json rename LICENSE => packages/fake-iamport-server/LICENSE (100%) create mode 100644 packages/fake-iamport-server/README.md create mode 100644 packages/fake-iamport-server/nestia.config.ts create mode 100644 packages/fake-iamport-server/package.json create mode 100644 packages/fake-iamport-server/src/FakeIamportBackend.ts create mode 100644 packages/fake-iamport-server/src/FakeIamportConfiguration.ts rename {src => packages/fake-iamport-server/src}/api/HttpError.ts (100%) rename {src => packages/fake-iamport-server/src}/api/IConnection.ts (100%) create mode 100644 packages/fake-iamport-server/src/api/IamportConnector.ts rename {src => packages/fake-iamport-server/src}/api/Primitive.ts (100%) create mode 100644 packages/fake-iamport-server/src/api/functional/certifications/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/certifications/otp/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/internal/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/payments/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/receipts/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/subscribe/customers/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/subscribe/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/subscribe/payments/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/users/index.ts create mode 100644 packages/fake-iamport-server/src/api/functional/vbanks/index.ts create mode 100644 packages/fake-iamport-server/src/api/index.ts create mode 100644 packages/fake-iamport-server/src/api/module.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportCertification.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportPayment.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportReceipt.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportResponse.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportUser.ts create mode 100644 packages/fake-iamport-server/src/api/structures/IIamportVBankPayment.ts rename {src => packages/fake-iamport-server/src}/api/typings/Atomic.ts (76%) rename {src => packages/fake-iamport-server/src}/api/typings/Writable.ts (66%) rename {src => packages/fake-iamport-server/src}/api/utils/NestiaSimulator.ts (100%) create mode 100644 packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts create mode 100644 packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts create mode 100644 packages/fake-iamport-server/src/controllers/FakeIamportPaymentsController.ts create mode 100644 packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts create mode 100644 packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts create mode 100644 packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts create mode 100644 packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts create mode 100644 packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts create mode 100644 packages/fake-iamport-server/src/executable/server.ts create mode 100644 packages/fake-iamport-server/src/index.ts create mode 100644 packages/fake-iamport-server/src/module.ts create mode 100644 packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts create mode 100644 packages/fake-iamport-server/src/providers/FakeIamportResponseProvider.ts create mode 100644 packages/fake-iamport-server/src/providers/FakeIamportStorage.ts create mode 100644 packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts create mode 100644 packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts create mode 100644 packages/fake-iamport-server/src/utils/AdvancedRandomGenerator.ts rename {src => packages/fake-iamport-server/src}/utils/DateUtil.ts (100%) create mode 100644 packages/fake-iamport-server/src/utils/ErrorUtil.ts rename {src => packages/fake-iamport-server/src}/utils/Terminal.ts (100%) create mode 100644 packages/fake-iamport-server/src/utils/VolatileMap.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_card_payment.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_card_payment_cancel.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_over.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_partial.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_certification.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_receipt.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_subscription_payment_again.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_over.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_partial.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_subscription_payment_onetime.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_vbank_payment.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_over.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_partial.ts create mode 100644 packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_without_refund.ts create mode 100644 packages/fake-iamport-server/test/index.ts rename {test => packages/fake-iamport-server/test}/tsconfig.json (100%) create mode 100644 packages/fake-iamport-server/tsconfig.json rename packages/{api => fake-toss-payments-server}/LICENSE (100%) rename packages/{api => fake-toss-payments-server}/README.md (100%) rename nestia.config.ts => packages/fake-toss-payments-server/nestia.config.ts (81%) create mode 100644 packages/fake-toss-payments-server/package.json rename {src => packages/fake-toss-payments-server/src}/FakeTossBackend.ts (96%) rename {src => packages/fake-toss-payments-server/src}/FakeTossConfiguration.ts (97%) create mode 100644 packages/fake-toss-payments-server/src/api/HttpError.ts create mode 100644 packages/fake-toss-payments-server/src/api/IConnection.ts create mode 100644 packages/fake-toss-payments-server/src/api/Primitive.ts rename {src => packages/fake-toss-payments-server/src}/api/functional/index.ts (100%) rename {src => packages/fake-toss-payments-server/src}/api/functional/internal/index.ts (97%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/billing/authorizations/card/index.ts (97%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/billing/authorizations/index.ts (97%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/billing/index.ts (98%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/cash_receipts/index.ts (96%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/index.ts (100%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/payments/index.ts (97%) rename {src => packages/fake-toss-payments-server/src}/api/functional/v1/virtual_accounts/index.ts (98%) rename {src => packages/fake-toss-payments-server/src}/api/index.ts (73%) rename {src => packages/fake-toss-payments-server/src}/api/module.ts (69%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossBilling.ts (85%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossCardPayment.ts (82%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossCashReceipt.ts (89%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossGiftCertificatePayment.ts (69%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossMobilePhonePayment.ts (66%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossPayment.ts (84%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossPaymentCancel.ts (87%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossPaymentWebhook.ts (73%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossTransferPayment.ts (69%) rename {src => packages/fake-toss-payments-server/src}/api/structures/ITossVirtualAccountPayment.ts (79%) create mode 100644 packages/fake-toss-payments-server/src/api/typings/Atomic.ts create mode 100644 packages/fake-toss-payments-server/src/api/typings/Writable.ts create mode 100644 packages/fake-toss-payments-server/src/api/utils/NestiaSimulator.ts rename {src => packages/fake-toss-payments-server/src}/controllers/FakeTossBillingController.ts (97%) rename {src => packages/fake-toss-payments-server/src}/controllers/FakeTossCashReceiptsController.ts (96%) rename {src => packages/fake-toss-payments-server/src}/controllers/FakeTossInternalController.ts (96%) rename {src => packages/fake-toss-payments-server/src}/controllers/FakeTossPaymentsController.ts (97%) rename {src => packages/fake-toss-payments-server/src}/controllers/FakeTossVirtualAccountsController.ts (98%) rename {src => packages/fake-toss-payments-server/src}/executable/server.ts (100%) rename {src => packages/fake-toss-payments-server/src}/index.ts (100%) rename {src => packages/fake-toss-payments-server/src}/module.ts (100%) rename {src => packages/fake-toss-payments-server/src}/providers/FakeTossPaymentProvider.ts (99%) rename {src => packages/fake-toss-payments-server/src}/providers/FakeTossStorage.ts (100%) rename {src => packages/fake-toss-payments-server/src}/providers/FakeTossUserAuth.ts (100%) rename {src => packages/fake-toss-payments-server/src}/providers/FakeTossWebhookProvider.ts (100%) create mode 100644 packages/fake-toss-payments-server/src/utils/DateUtil.ts rename {src => packages/fake-toss-payments-server/src}/utils/ErrorUtil.ts (100%) create mode 100644 packages/fake-toss-payments-server/src/utils/Terminal.ts rename {src => packages/fake-toss-payments-server/src}/utils/VolatileMap.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/internal/validate_fake_payment_cancel.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/internal/validate_fake_payment_cancel_over.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/internal/validate_fake_payment_cancel_partial.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_billing_payment.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_billing_payment_cancel.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_billing_payment_cancel_over.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_billing_payment_cancel_partial.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_card_payment.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_card_payment_cancel.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_card_payment_cancel_over.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_card_payment_cancel_partial.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_cash_receipt.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_storage_capacity.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_storage_expiration_time.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_virtual_account_payment.ts (99%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_virtual_account_payment_cancel.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_virtual_account_payment_cancel_over.ts (100%) rename {test => packages/fake-toss-payments-server/test}/features/test_fake_virtual_account_payment_cancel_partial.ts (100%) rename {test => packages/fake-toss-payments-server/test}/index.ts (100%) rename {test => packages/fake-toss-payments-server/test}/internal/AdvancedRandomGenerator.ts (100%) rename {test => packages/fake-toss-payments-server/test}/internal/TestConnection.ts (100%) create mode 100644 packages/fake-toss-payments-server/test/tsconfig.json rename tsconfig.json => packages/fake-toss-payments-server/tsconfig.json (100%) create mode 100644 packages/iamport-server-api/README.md create mode 100644 packages/iamport-server-api/package.json create mode 100644 packages/iamport-server-api/tsconfig.json create mode 100644 packages/payment-api/README.md create mode 100644 packages/payment-api/package.json create mode 100644 packages/payment-api/tsconfig.json create mode 100644 packages/payment-backend/.env create mode 100644 packages/payment-backend/INFRASTRUCTURE.md create mode 100644 packages/payment-backend/LICENSE create mode 100644 packages/payment-backend/README.md create mode 100644 packages/payment-backend/docs/ERD.md create mode 100644 packages/payment-backend/nestia.config.ts create mode 100644 packages/payment-backend/package.json create mode 100644 packages/payment-backend/src/PaymentAsset.ts create mode 100644 packages/payment-backend/src/PaymentBackend.ts create mode 100644 packages/payment-backend/src/PaymentConfiguration.ts create mode 100644 packages/payment-backend/src/PaymentGlobal.ts create mode 100644 packages/payment-backend/src/PaymentSetupWizard.ts create mode 100644 packages/payment-backend/src/PaymentUpdator.ts create mode 100644 packages/payment-backend/src/api/HttpError.ts create mode 100644 packages/payment-backend/src/api/IConnection.ts create mode 100644 packages/payment-backend/src/api/Primitive.ts create mode 100644 packages/payment-backend/src/api/functional/index.ts create mode 100644 packages/payment-backend/src/api/functional/monitors/health/index.ts create mode 100644 packages/payment-backend/src/api/functional/monitors/index.ts create mode 100644 packages/payment-backend/src/api/functional/monitors/performance/index.ts create mode 100644 packages/payment-backend/src/api/functional/monitors/system/index.ts create mode 100644 packages/payment-backend/src/api/functional/payments/histories/index.ts create mode 100644 packages/payment-backend/src/api/functional/payments/index.ts create mode 100644 packages/payment-backend/src/api/functional/payments/internal/index.ts create mode 100644 packages/payment-backend/src/api/functional/payments/reservations/index.ts create mode 100644 packages/payment-backend/src/api/functional/payments/webhooks/index.ts create mode 100644 packages/payment-backend/src/api/index.ts create mode 100644 packages/payment-backend/src/api/module.ts create mode 100644 packages/payment-backend/src/api/structures/common/IEntity.ts create mode 100644 packages/payment-backend/src/api/structures/monitors/IPerformance.ts create mode 100644 packages/payment-backend/src/api/structures/monitors/ISystem.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentHistory.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentSource.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentWebhook.ts create mode 100644 packages/payment-backend/src/api/structures/payments/IPaymentWebhookHistory.ts create mode 100644 packages/payment-backend/src/api/typings/Atomic.ts create mode 100644 packages/payment-backend/src/api/typings/Writable.ts create mode 100644 packages/payment-backend/src/api/utils/NestiaSimulator.ts create mode 100644 packages/payment-backend/src/controllers/monitors/HealthController.ts create mode 100644 packages/payment-backend/src/controllers/monitors/PerformanceController.ts create mode 100644 packages/payment-backend/src/controllers/monitors/SystemController.ts create mode 100644 packages/payment-backend/src/controllers/payments/PaymentHistoriesController.ts create mode 100644 packages/payment-backend/src/controllers/payments/PaymentInternalController.ts create mode 100644 packages/payment-backend/src/controllers/payments/PaymentReservationsController.ts create mode 100644 packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts create mode 100644 packages/payment-backend/src/executable/master.ts create mode 100644 packages/payment-backend/src/executable/monitor.ts create mode 100644 packages/payment-backend/src/executable/schema.ts create mode 100644 packages/payment-backend/src/executable/server.ts create mode 100644 packages/payment-backend/src/executable/update.ts create mode 100644 packages/payment-backend/src/executable/updator-master.ts create mode 100644 packages/payment-backend/src/executable/updator-slave.ts create mode 100644 packages/payment-backend/src/index.ts create mode 100644 packages/payment-backend/src/migrations/20230919094622_init/migration.sql create mode 100644 packages/payment-backend/src/migrations/migration_lock.toml create mode 100644 packages/payment-backend/src/module.ts create mode 100644 packages/payment-backend/src/modules/express.ts create mode 100644 packages/payment-backend/src/modules/nestjs.ts create mode 100644 packages/payment-backend/src/providers/monitors/SystemProvider.ts create mode 100644 packages/payment-backend/src/providers/payments/FakePaymentStorage.ts create mode 100644 packages/payment-backend/src/providers/payments/PaymentCancelHistoryProvider.ts create mode 100644 packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts create mode 100644 packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts create mode 100644 packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts create mode 100644 packages/payment-backend/src/schedulers/Scheduler.ts create mode 100644 packages/payment-backend/src/schedulers/features/schedule_something_in_every_day.ts create mode 100644 packages/payment-backend/src/schedulers/features/schedule_something_in_every_hour.ts create mode 100644 packages/payment-backend/src/schedulers/features/schedule_something_in_every_minutes.ts create mode 100644 packages/payment-backend/src/schema.prisma create mode 100644 packages/payment-backend/src/services/iamport/IamportAsset.ts create mode 100644 packages/payment-backend/src/services/iamport/IamportPaymentService.ts create mode 100644 packages/payment-backend/src/services/toss/TossAsset.ts create mode 100644 packages/payment-backend/src/services/toss/TossPaymentBillingService.ts create mode 100644 packages/payment-backend/src/services/toss/TossPaymentCardService.ts create mode 100644 packages/payment-backend/src/services/toss/TossPaymentService.ts create mode 100644 packages/payment-backend/src/services/toss/TossPaymentVirtualAccountService.ts create mode 100644 packages/payment-backend/src/utils/ArgumentParser.ts create mode 100644 packages/payment-backend/src/utils/BcryptUtil.ts create mode 100644 packages/payment-backend/src/utils/DateUtil.ts create mode 100644 packages/payment-backend/src/utils/ErrorUtil.ts create mode 100644 packages/payment-backend/src/utils/Terminal.ts create mode 100644 packages/payment-backend/src/utils/TokenManager.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_card_payment.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel_partial.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_subscription_payment.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel.ts create mode 100644 packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel_partial.ts create mode 100644 packages/payment-backend/test/features/internal/validate_payment_cancel.ts create mode 100644 packages/payment-backend/test/features/internal/validate_payment_cancel_partial.ts create mode 100644 packages/payment-backend/test/features/monitors/test_api_monitor_health_check.ts create mode 100644 packages/payment-backend/test/features/monitors/test_api_monitor_system.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_card_payment.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel_partial.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_subscription_payment.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_vbank_payment.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel.ts create mode 100644 packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel_partial.ts create mode 100644 packages/payment-backend/test/index.ts create mode 100644 packages/payment-backend/test/manual/password.ts create mode 100644 packages/payment-backend/test/tsconfig.json create mode 100644 packages/payment-backend/tsconfig.json create mode 100644 packages/toss-payments-server-api/README.md create mode 100644 packages/toss-payments-server-api/package.json create mode 100644 packages/toss-payments-server-api/tsconfig.json diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 149401b..167f020 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,7 +12,10 @@ module.exports = { }, overrides: [ { - files: ["src/**/*.ts"], + files: [ + "packages/*/src/**/*.ts", + "packages/*/test/**/*.ts" + ], rules: { "@typescript-eslint/consistent-type-definitions": "off", "@typescript-eslint/no-duplicate-imports": "error", diff --git a/.eslintrc.test.cjs b/.eslintrc.test.cjs deleted file mode 100644 index 40269a8..0000000 --- a/.eslintrc.test.cjs +++ /dev/null @@ -1,5 +0,0 @@ -const options = require("./.eslintrc.cjs"); -options.parserOptions.project = "test/tsconfig.json"; -options.overrides[0].files = ["test/**/*.ts"]; - -module.exports = options; diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0e83eb4..6e7caaf 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -3,8 +3,8 @@ Before submitting a Pull Request, please test your code. If you created a new created a new feature, then create the unit test function, too. ```bash -# COMPILE THE BACKEND SERVER -npm run build +# INSTALL DEPENDENCIES +npm install # RUN THE TEST AUTOMATION PROGRAM npm run test diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3eff9f..fe9b721 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,15 +4,17 @@ on: [push, pull_request] jobs: Ubuntu: runs-on: ubuntu-latest + services: + postgres: + image: postgis/postgis:16-3.4 + env: + POSTGRES_PASSWORD: root + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 - - name: Install Backend-Server - run: npm install - - - name: Compile Program - run: npm run build - - - name: Test Backend-Server - run: npm run test -- --include=fake \ No newline at end of file + - name: Test Packages + run: npm install && npm run test \ No newline at end of file diff --git a/.gitignore b/.gitignore index e27ff51..c8bd6b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ bin/ lib/ node_modules/ -*.DS_Store package-lock.json -.npmrc -*.sh \ No newline at end of file +*.DS_Store +*.log +*.tgz \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 675fe4e..7783ae8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,12 @@ +bin dist lib node_modules -packages -src/api + +packages/*/src/api/functional +packages/*/src/api/typings +packages/*/src/api/utils +packages/*/src/api/*.ts + README.md -tsconfig.json \ No newline at end of file +packages/*/tsconfig.json \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..61b74fe --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Iamport", + "program": "${workspaceRoot}/packages/fake-iamport-server/test/index.ts", + "cwd": "${workspaceRoot}/packages/fake-iamport-server", + "outFiles": ["${workspaceRoot}/packages/fake-iamport-server/lib/**/*.js"], + }, + { + "type": "node", + "request": "launch", + "name": "Toss Payments", + "program": "${workspaceRoot}/packages/fake-toss-payments-server/test/index.ts", + "cwd": "${workspaceRoot}/packages/fake-toss-payments-server", + "outFiles": ["${workspaceRoot}/packages/fake-toss-payments-server/lib/**/*.js"], + }, + { + "type": "node", + "request": "launch", + "name": "Payment Backend", + "program": "${workspaceRoot}/packages/payment-backend/test/index.ts", + "cwd": "${workspaceRoot}/packages/payment-backend", + "args": [ + //---- + // Unable to reset DB in debugging mode. + //---- + // Therefore, reset DB first by running + // `npm run reset-for-debugging` command, + // and run debugging mode later. + //---- + "--reset", "false", + + //---- + // You can run specific test functions + //---- + // "--include", "something", + // "--exclude", "nothing", + ], + "outFiles": ["${workspaceRoot}/packages/payment-backend/bin/**/*.js"], + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ec80d6..c9887a4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,16 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, - } + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + }, + "[prisma]": { + "editor.defaultFormatter": "Prisma.prisma", + "editor.formatOnSave": true + }, } \ No newline at end of file diff --git a/README.md b/README.md index 362d2eb..5d21570 100644 --- a/README.md +++ b/README.md @@ -1,355 +1,12 @@ -# Fake Toss Payments Server -## 1. Outline -[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/fake-toss-payments-server/blob/master/LICENSE) -[![npm version](https://badge.fury.io/js/toss-payments-server-api.svg)](https://www.npmjs.com/package/toss-payments-server-api) -[![Downloads](https://img.shields.io/npm/dm/toss-payments-server-api.svg)](https://www.npmjs.com/package/toss-payments-server-api) -[![Build Status](https://github.com/samchon/fake-toss-payments-server/workflows/build/badge.svg)](https://github.com/samchon/fake-toss-payments-server/actions?query=workflow%3Abuild) +# Payment System +결제 시스템. -`fake-toss-payments-server` 는 토스 페이먼츠 서버의 API 를 흉내내어 만든, 가짜 토스 페이먼츠 서버이다. 토스 페이먼츠 서버와의 보다 원활한 연동 테스트를 위하여 만들었다. 특히 프론트 어플리케이션을 통한 수기 테스트가 아닌, 백엔드 서버 자체의 테스트 자동화 프로그램을 통한 상시 검증에 적합하다. + - fake-iamport-server + - fake-toss-payments-server + - payment-backend -또한, [toss-payments-server-api](https://www.npmjs.com/package/toss-payments-server-api) 는 토스 페이먼츠 서버와 연동할 수 있는 SDK 라이브러리로써, `fake-toss-payments-server` 의 소스코드를 토대로 [Nestia](https://github.com/samchon/nestia) 를 이용하여 빌드하였다. 그리고 이를 통하여 가짜 토스 페이먼츠 서버 뿐 아니라, 진짜 토스 페이먼츠 서버, 양쪽 모두와 연동할 수 있다. +이하 클라이언트용 SDK 라이브러리. -참고로 [Nestia](https://github.com/samchon/nestia) 는 SDK 뿐 아니라 Swagger 또한 빌드할 수 있는데, 이 또한 `fake-toss-payments-server` 의 소스코드 및 DTO 를 컴파일러 수준에서 분석하여 만들어지는 것인지라, 그 퀄리티가 상당하다. 어쩌면 토스 페이먼츠가 공식 제공하는 개발자 가이드 문서보다, `fake-toss-payments-server` 로 생성한 Swagger 가 더 개발자 친화적이고 일목요연할지도? - - - 서버 주소: http://localhost:30771 - - 매뉴얼 - - **Swagger Editor**: [dist/swagger.json](https://editor.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsamchon%2Ffake-toss-payments-server%2Fmaster%2Fdist%2Fswagger.json) - - 자료 구조: [src/api/structures/ITossBilling.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/api/structures/ITossBilling.ts) - - API 함수: [src/api/functional/payments/index.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/api/functional/payments/index.ts) - - 예제 코드 - - 간편 결제: [test_fake_billing_payment.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_billing_payment.ts) - - 카드 결제: [test_fake_card_payment.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_card_payment.ts) - - 가상 계좌 결제: [test_fake_virtual_account_payment.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_virtual_account_payment.ts) - - 현금 영수증 발행: [test_fake_cash_receipt.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_cash_receipt.ts) - - 연관 저장소 - - [samchon/typia](https://github.com/samchon/typia) - Superfast runtime validator - - [samchon/netia](https://github.com/samchon/nestia) - SDK generator for the NestJS - - [samchon/fake-iamport-server](https://github.com/samchon/fake-iamport-server): 가짜 아임포트 서버 - -```typescript -import btoa from "btoa"; -import toss from "toss-payments-server-api"; -import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; -import { assert } from "typia"; - -export async function test_fake_card_payment_approval(): Promise -{ - const connection: toss.IConnection = { - host: "http://127.0.0.1:30771", // FAKE-SERVER - // host: "https://api.tosspayments.com", // REAL-SERVER - headers: { - Authorization: `Basic ${btoa("test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:")}` - } - }; - - const payment: ITossPayment = await toss.functional.v1.payments.key_in - ( - connection, - { - // CARD INFORMATION - method: "card", - cardNumber: "1111222233334444", - cardExpirationYear: "24", - cardExpirationMonth: "03", - - // ORDER INFORMATION - orderId: "some-order-id", - amount: 25_000, - - // FAKE PROPERTY - __approved: false - } - ); - assert(payment); - - const approved: ITossPayment = await toss.functional.v1.payments.approve - ( - connection, - payment.paymentKey, - { - orderId: payment.orderId, - amount: payment.totalAmount, - } - ); - assert(approved); -} -``` - - - - -## 2. Installation -### 2.1. NodeJS -백엔드 서버 프로그램은 TypeScript 로 만들어졌으며, NodeJS 에서 구동된다. - -고로 제일 먼저 할 일은, NodeJS 를 설치하는 것이다. 아래 링크를 열어, NodeJS 프로그램을 다운로드 받은 후 즉각 설치하기 바란다. 참고로 NodeJS 버전은 어지간히 낮은 옛 시대의 버전만 아니면 되니, 구태여 Latest 버전을 설치할 필요는 없으며, Stable 버전만으로도 충분하다. - - - https://nodejs.org/en/ - -### 2.2. Server -NodeJS 의 설치가 끝났다면, 바로 (가짜) 토스 페이먼츠 서버 구동을 시작할 수 있다. - -제일 먼저 `git clone` 을 통하여, 백엔드 프로젝트를 로컬 저장소에 복사하도록 한다. 그리고 해당 폴더로 이동하여 `npm install` 명령어를 실행함으로써, 백엔드 서버를 구동하는 데 필요한 라이브러리들을 다운로드 한다. 그리고 `npm run build` 명령어를 입력하여, 백엔드 서버의 소스 코드를 컴파일한다. - -마지막으로 `npm run start` 명령어를 실행해주면, (가짜) 토스 페이먼츠 서버가 구동된다. 이 가짜 서버를 통하여, 귀하가 개발하는 백엔드 서버가 결제 연동에 관련하여 제대로 구현되었는 지 충분히 검증한 후, 실 서버를 배포할 때 연동 대상 서버를 현재의 가짜 서버에서 진짜 서버로 바꾸어주도록 하자. 구동 중인 가짜 토스 페이먼츠 서버를 중단하고 싶다면, `npm run stop` 명령어를 실행해주면 된다. - -참고로 가짜 토스 페이먼츠 서버가 사용하는 포트 번호나, 가짜 토스 페이먼츠 서버가 이벤트를 전달해주는 Webhook URL 등은 모두 [src/FakeTossConfiguration.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/FakeTossConfiguration.ts) 에 정의되어있으니, 이를 알맞게 수정한 후 컴파일 및 가짜 서버 재 가동을 해 주면 된다. - -```bash -# CLONE REPOSITORY -git clone https://github.com/samchon/fake-toss-payments-server -cd fake-toss-payments-server - -# INSTALLATION & COMPILATION -npm install -npm run build - -# START SERVER & STOP SERVER -npm run start -npm run stop -``` - -[![npm version](https://badge.fury.io/js/fake-toss-payments-server.svg)](https://www.npmjs.com/package/fake-toss-payments-server) -[![Downloads](https://img.shields.io/npm/dm/fake-toss-payments-server.svg)](https://www.npmjs.com/package/fake-toss-payments-server) - -더하여 `fake-toss-payments-server` 는 npm 모듈로 설치하여 구동할 수도 있다. - -귀하의 백엔드 서버 테스트 자동화 프로그램이, `fake-toss-payments-server` 의 설정과 개설 및 폐쇄를 모두 통제하고자 할 때는, github 저장소를 clone 하는 것보다, 이처럼 npm 모듈을 설치하여 import 하는 것이 훨씬 더 알맞다. - -따라서 귀하의 백엔드 서버가 TypeScript 내지 JavaScript 를 사용한다면, github 저장소를 clone 하여 `fake-toss-payments-server` 를 별도 구동하기보다, 귀하의 백엔드 서버에서 `fake-toss-payments-server` 의 개설과 폐쇄를 직접 통제하기를 권장한다. - -```typescript -// npm install --save-dev fake-toss-payments-server -import FakeToss from "fake-toss-payments-server"; - -async function main(): Promise -{ - FakeToss.FakeTossConfiguration.WEBHOOK_URL = "your-backend-webhook-api-url"; - FakeToss.FakeTossConfiguration.authorize = token => - { - return token === "test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy"; - }; - - const fake: FakeToss.FakeTossBackend = new FakeToss.FakeTossBackend(); - await fake.open(); - await fake.close(); -} -``` - -### 2.3. SDK -[![npm version](https://badge.fury.io/js/toss-payments-server-api.svg)](https://www.npmjs.com/package/toss-payments-server-api) -[![Downloads](https://img.shields.io/npm/dm/toss-payments-server-api.svg)](https://www.npmjs.com/package/toss-payments-server-api) - -본 백엔드 프로젝트 `fake-toss-payments-server` 는 비록 토스 페이먼츠의 API 를 흉내내어 만든 가짜이지만, 이것을 통하여 만들어지는 SDK (Software Development Kit) 만큼은 진짜이다. 이 SDK 를 이용하면, `fake-toss-payments-server` 뿐 아니라 진짜 토스 페이먼츠 서버와도 원활히 연동할 수 있기 때문이다. - -고로 토스 페이먼츠와 연동하는 TypeScript 기반 백엔드 서버를 개발함에 있어, 가짜 토스 페이먼츠 서버 `fake-toss-payments-server` 는 직접 이용치 않더라도, 실제 토스 페이먼츠 서버와의 연동을 위하여, SDK 라이브러리만큼은 반드시 설치하기를 권장하는 바이다. - -```bash -npm install --save toss-payments-server-api -``` - -먼저 위 명령어를 통하여, 귀하의 TypeScript 기반 백엔드 서버에, 토스 페이먼츠 서버와의 연동을 위한 SDK 라이브러리를 설치한다. 그리고 가짜 토스 페이먼츠 서버 `fake-toss-payments-server` 를 구동하여, 이 것과의 결제 연동이 제대로 이루어지는 지 충분할 만큼의 검증을 한다. 테스트 자동화 프로그램을 제작, 이 안정성이 상시 검증될 수 있다면 더더욱 좋다. - -마지막으로 실 서버를 배포하며, 연동 대상 서버를 가짜에서 진짜로 교체해주면 된다. - -참고로 [Nestia](https://github.com/samchon/nestia) 는 SDK 뿐 아니라 Swagger 또한 빌드할 수 있는데, 이 또한 `fake-toss-payments-server` 의 소스코드 및 DTO 를 컴파일러 수준에서 분석하여 만들어지는 것인지라, 그 퀄리티가 상당하다. 어쩌면 토스 페이먼츠가 공식 제공하는 개발자 가이드 문서보다, `fake-toss-payments-server` 로 생성한 Swagger 가 더 개발자 친화적이고 일목요연할지도? - - - 서버 주소: http://localhost:30771 - - 매뉴얼 - - **Swagger Editor**: [dist/swagger.json](https://editor.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsamchon%2Ffake-toss-payments-server%2Fmaster%2Fdist%2Fswagger.json) - - 자료 구조: [src/api/structures/ITossBilling.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/api/structures/ITossBilling.ts) - - API 함수: [src/api/functional/payments/index.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/api/functional/payments/index.ts) - - 예제 코드 - - 간편 결제: [test_fake_billing_payment.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_billing_payment.ts) - - 카드 결제: [test_fake_card_payment.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_card_payment.ts) - - 가상 계좌 결제: [test_fake_virtual_account_payment.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_virtual_account_payment.ts) - - 현금 영수증 발행: [test_fake_cash_receipt.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features/examples/test_fake_cash_receipt.ts) - - 연관 저장소 - - [samchon/netia](https://github.com/samchon/nestia) - Automatic SDK generator for the NestJS - - [samchon/fake-iamport-server](https://github.com/samchon/fake-iamport-server): 가짜 아임포트 서버 - -```typescript -import btoa from "btoa"; -import toss from "toss-payments-server-api"; -import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; -import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; -import { assert } from "typia"; - -export async function test_fake_payment_billing_payment(): Promise -{ - const connection: toss.IConnection = { - host: "http://127.0.0.1:30771", // FAKE-SERVER - // host: "https://api.tosspayments.com", // REAL-SERVER - headers: { - Authorization: `Basic ${btoa("test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy:")}` - } - }; - - const billing: ITossBilling = await toss.functional.v1.billing.authorizations.card.store - ( - connection, - { - customerKey: "some-consumer-key", - cardNumber: "1111222233334444", - cardExpirationYear: "28", - cardExpirationMonth: "03", - cardPassword: "99", - customerBirthday: "880311", - consumerName: "남정호" - } - ); - assert(billing); - - const payment: ITossPayment = await toss.functional.v1.billing.pay - ( - connection, - billing.billingKey, - { - method: "billing", - billingKey: billing.billingKey, - customerKey: "some-consumer-key", - orderId: "some-order-id", - amount: 10_000 - } - ); - assert(payment); -} -``` - - - - -## 3. Development -### 3.1. API Interface Definition -백엔드 서버에 새 API 를 추가하고 기능을 변경하는 일 따위는 물론, API 컨트롤러, 즉 [src/controllers](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/controllers) 의 코드를 수정함으로써 이루어진다. 하지만 `fake-toss-payments-server` 는 신규 API 가 필요하거나 혹은 기존 API 의 변경 필요할 때, 대뜸 [Main Program](#33-main-program) 의 코드부터 작성하고 보는 것을 매우 지양한다. 그 대신 `fake-toss-payments-server` 는 API 의 인터페이스만을 먼저 정의하고, [Main Program](#33-main-program) 의 구현은 나중으로 미루는 것을 지향한다. - -따라서 `fake-toss-payments-server` 에 새 API 를 추가하려거든, [src/controllers](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/controllers) 에 새 API 의 인터페이스만을 먼저 정의해준다. 곧이어 `npm run sdk` 나 `npm run api` 명령어를 통하여, API Library 를 빌드한다. 경우에 따라서는 프론트 프로젝트와의 동시 개발을 위하여, 새로이 빌드된 SDK 를 그대로 `npm publish` 해 버려도 좋다. - -이후 로컬에서 새로이 생성된 SDK 와 해당 API 를 이용, 유즈케이스 시나리오를 테스트 자동화 프로그램으로 작성한다. 그리고 Main Program 을 제작하며, 앞서 작성해 둔 테스트 자동화 프로그램으로 상시 검증한다. 마지막으로 Main Program 까지 완성되면 이를 배포하면 된다. - -이하 `fake-toss-payments-server` 의 개략적인 개발 순서를 요약하면 아래와 같다. - - - API Interface Definition - - API Library (SDK) 빌드 - - Test Automation Program 제작 - - Main Program 제작 및 테스트 자동화 프로그램을 이용한 상시 검증 - - DEV 및 REAL 서버에 배포 - -### 3.2. Test Automation Program -```bash -npm run test -``` - -새로이 개발할 [API 인터페이스 정의](#31-api-interface-definition)를 마쳤다면, 그 다음에 할 일은 바로 해당 API 에 대한 유즈케이스 시나리오를 세우고 이를 테스트 자동화 프로그램을 만들어, 향후 [Main Program](#33-main-program) 제작시 이를 상시 검증할 수 있는 수단을 구비해두는 것이다 - TDD (Test Driven Development). - -그리고 본 프로젝트는 `npm run test` 라는 명령어를 통하여, 서버 프로그램의 일체 기능 및 정책 등에 대하여 검증할 수 있는, 테스트 자동화 프로그램을 구동해 볼 수 있다. 만약 새로운 테스트 로직을 추가하고 싶다면, [test/features](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features) 폴더의 적당한 위치에 새 `ts` 파일을 하나 만들고, `test_` 로 시작하는 함수를 하나 만들어 그 안에 테스트 로직을 작성한 후, 이를 `export` 심벌을 이용하여 배출해주면 된다. 이에 대한 자세한 내용은 [test/features](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features) 폴더에 들어있는 모든 `ts` 파일 하나 하나가 다 좋은 예제 격이니, 이를 참고하도록 한다. - -참고로 `npm run test` 명령어를 실행할 때마다, [test/features](https://github1s.com/samchon/fake-toss-payments-server/blob/master/test/features) 폴더 내에 등록된 모든 프로그램을 실행하게 된다. 하지만 이런 식의 *entire level test* 가 매번 필요한 것은 아닐 것이다. 새로 개발한 기능이 극히 일부 요소에 국한되어 부분 테스트가 필요하다면, 아래 옵션값을 참조, `--include` 나 `--exclude` 태그를 사용하여 시간을 절약하도록 하자. - - - options - - `include`: 특정 단어가 포함된 테스트 함수만 실행 - - `exclude`: 특정 단어가 포함된 테스트 함수 제외 - -### 3.3. Main Program -[API 인터페이스를 정의](#31-api-interface-definition)하고 그에 관련된 [테스트 자동화 프로그램](#32-test-automation-program)을 제작하였다면, 마지막으로 남은 일은 바로 서버의 메인 프로그램을 작성, 해당 API 를 완성하는 것이다. 앞서 정의한 [API 인터페이스](#31-api-interface-definition) 메서드 내에, 상세 구현 코드를 작성하고, 이를 [테스트 자동화 프로그램](#32-test-automation-program)을 통하여 상시 검증하도록 하자. - -단, 모든 소스 코드를 전부 API 컨트롤러의 메서드에 작성하는 우는 범하지 않기를 바란다. API 컨트롤러는 단지 매개체 + a 의 역할만을 해야 할 뿐이며, 주 소스 코드는 [src](src) 폴더 내 각 폴더의 분류에 따라 알맞게 나뉘어 작성되어야 한다. - - - - -## 4. Appendix -### 4.1. Expiration -`fake-toss-payments-server` 는 결제 데이터를 메모리에 임시 기록한다. - -왜냐하면 `fake-toss-payments-server` 는 토스 페이먼츠 서버의 API 를 흉내내어 만든 가짜 서버로써, 개발 단계에서 쓰이는 임시 시스템에 불과하기 때문이다. 따라서 `fake-toss-payments-server` 에 생성된 결제 내지 카드 정보들은 모두 테스트 용도로 생성된 임시 레코드가 불과하기에, 구태여 이를 DB 나 로컬 디스크에 저장하여 영구 보존할 이유가 없다. - -이에 `fake-toss-payments-server` 는 결제 데이터를 메모리에 임시로 기록하며, 한 편으로 그 수량 및 보존 기한에 한도를 두어, 쉬이 메모리 부족 현상이 일어나지 않도록 하고 있다. 이러한 임시 데이터 만료 정보는 [src/FakeTossConfiguration.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/FakeTossConfiguration.ts) 파일의 `EXPIRATION` 변수에 정의되어있으며, 결제 및 간편 카드 결제 등록 데이터는 모두 [src/providers/FakeTossStorage.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/providers/FakeTossStorage.ts) 에서 관리된다. - - - 임시 데이터 만료 정보: [src/FakeTossConfiguration.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/FakeTossConfiguration.ts) - - 임시 데이터 저장소: [src/providers/FakeTossStorage.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/providers/FakeTossStorage.ts) - - 임시 데이터 컨테이너: [src/utils/VolatileMap.ts](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/utils/VolatileMap.ts) - -> 혹여 `fake-toss-payments-server` 를 사용하는 백엔드 시스템이 제법 크고 그 네트워크 환경 구성이 매우 복잡하여, `fake-toss-payments-server` 를 독립 서버로 배포하고, 가상의 결제 레코드 또한 DB 에 저장해야 하며, 무중단 배포 시스템 또한 필요하지 않을까? -> -> 설마 위와 같은 경우가 있어봐야 얼마나 있겠나 싶어 공개 저장소에 올려두지는 않았으나, `fake-toss-payments-server` 가 결제 데이터를 [VolatileMap](https://github1s.com/samchon/fake-toss-payments-server/blob/master/src/utils/VolatileMap.ts) 이 아닌 DB 에 저장하고, [폐쇄망에서조차 동작할 수 있는 무중단 업데이트 및 배포 시스템](https://github.com/samchon/backend#41-non-distruptive-update-system)을 따로 구비해 둔 것이 있다. -> -> 따라서 위와 같은 형태의 `fake-toss-payments-server` 가 필요하다면, 얼마든지 연락하기 바란다. 즉시 위 요소를 충당하는 솔루션을 공급해 줄 수 있으며, 만일 이러한 요청이 제법 많은 경우, 별도의 브랜치를 만들어 배포해 볼 요량도 있다. -> ->> ```bash ->> # WHEN STARTING THE MASTE SERVER ->> npm run start:updator:master ->> npm run start ->> ->> # WHEN STARTING A SLAVE SERVER ->> npm run start:updator:slave ->> npm run start ->> ->> # WHEN RUN UPDATE COMMAND IN THE CLIENT SIDE ->> npm run update ->> ``` - -### 4.2. Typia -![Typia Logo](https://typia.io/logo.png) - -[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/typia/blob/master/LICENSE) -[![npm version](https://img.shields.io/npm/v/typia.svg)](https://www.npmjs.com/package/typia) -[![Downloads](https://img.shields.io/npm/dm/typia.svg)](https://www.npmjs.com/package/typia) -[![Build Status](https://github.com/samchon/typia/workflows/build/badge.svg)](https://github.com/samchon/typia/actions?query=workflow%3Abuild) -[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://typia.io/docs/) - -```typescript -// RUNTIME VALIDATORS -export function is(input: unknown): input is T; // returns boolean -export function assert(input: unknown): T; // throws TypeGuardError -export function validate(input: unknown): IValidation; // detailed -export const customValidators: CustomValidatorMap; // can add custom validators - -// JSON -export function application(): IJsonApplication; // JSON schema -export function assertParse(input: string): T; // type safe parser -export function assertStringify(input: T): string; // safe and faster - // +) isParse, validateParse - // +) stringify, isStringify, validateStringify - -// MISC -export function random(g?: Partial): Primitive; -``` - -Typia is a transformer library supporting below features: - - - Super-fast Runtime Validators - - Safe JSON parse and fast stringify functions - - JSON schema generator - - Random data generator - -> **Note** -> -> - **Only one line** required, with pure TypeScript type -> - Runtime validator is **20,000x faster** than `class-validator` -> - JSON serialization is **200x faster** than `class-transformer` - -### 4.3. Nestia -![Nestia Logo](https://nestia.io/logo.png) - -[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) -[![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) -[![Downloads](https://img.shields.io/npm/dm/nestia.svg)](https://www.npmjs.com/package/nestia) -[![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) -[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) - -Nestia is a set of helper libraries for NestJS, supporting below features: - - - `@nestia/core`: super-fast decorators - - `@nestia/sdk`: - - SDK generator for clients - - Swagger generator evolved than ever - - Automatic E2E test functions generator - - `nestia`: just CLI (command line interface) tool - -> **Note** -> -> - **Only one line** required, with pure TypeScript type -> - Runtime validator is **20,000x faster** than `class-validator` -> - JSON serialization is **200x faster** than `class-transformer` -> - SDK is similar with [tRPC](https://trpc.io), but much advanced \ No newline at end of file + - iamport-server-api + - toss-payments-server-api + - payment-api \ No newline at end of file diff --git a/deploy/index.js b/deploy/index.js new file mode 100644 index 0000000..011f257 --- /dev/null +++ b/deploy/index.js @@ -0,0 +1,10 @@ +const { publish } = require("./publish"); + +const tag = process.argv[2]; +const version = process.argv[3]; + +if (tag !== "latest" && tag !== "next" && tag !== "tgz") + throw new Error("Invalid tag"); +if (!version?.length) throw new Error("Invalid version"); + +publish(tag)(version); diff --git a/deploy/publish.js b/deploy/publish.js new file mode 100644 index 0000000..1857bad --- /dev/null +++ b/deploy/publish.js @@ -0,0 +1,77 @@ +const cp = require("child_process"); +const fs = require("fs"); +const path = require("path"); + +const packages = fs.readdirSync(`${__dirname}/../packages`); + +const execute = (cwd, stdio = "ignore") => (command) => { + console.log(command); + cp.execSync(command, { cwd, stdio }); +}; + +const deploy = (tag) => (version) => (name) => { + console.log("-----------------------------------------"); + console.log(name.toUpperCase()); + console.log("-----------------------------------------"); + + // CHANGE PACKAGE.JSON INFO + const directory = `${__dirname}/../packages/${name}`; + const file = `${directory}/package.json`; + const info = JSON.parse(fs.readFileSync(file, "utf8")); + info.version = version; + + for (const record of [info.dependencies ?? {}, info.devDependencies ?? {}]) + for (const key of Object.keys(record)) + if (packages.includes(key)) { + if ( + tag === "tgz" && + fs.existsSync(`${directory}/node_modules/${key}`) + ) + execute(directory)(`npm uninstall ${key}`); + record[key] = + tag === "tgz" + ? path.resolve( + `${__dirname}/../packages/${key}/${key}-${version}.tgz`, + ) + : `^${version}`; + } + + // SETUP UPDATED DEPENDENCIES + fs.writeFileSync(file, JSON.stringify(info, null, 2), "utf8"); + execute(directory)(`npm install`); + execute(directory)(`npm run build`); + + // RUN TEST PROGRAM + if (name === "payment-backend") { + execute(directory)(`npm run schema`); + execute(directory, "inherit")(`npm run test -- --reset true`); + } else if (fs.existsSync(`${directory}/test`)) + execute(directory, "inherit")(`npm run test`); + + // PUBLISH (OR PACK) + if (tag === "tgz") execute(directory)(`npm pack`); + else execute(directory)(`npm publish --tag ${tag}`); + console.log(""); +}; + +const publish = (tag) => (version) => { + // VALIDATE TAG + const dev = version.includes("--dev.") === false; + if (tag !== "latest" && dev === false) + throw new Error(`${tag} tag can only be used for dev versions.`); + else if (tag === "latest" && dev === true) + throw new Error(`latest tag can only be used for non-dev versions.`); + + // DO DEPLOY + for (const pack of [ + "fake-iamport-server", + "fake-toss-payments-server", + "iamport-server-api", + "toss-payments-server-api", + "payment-backend", + "payment-api", + ]) + deploy(tag)(version)(pack); +}; + +module.exports = { publish }; diff --git a/package.json b/package.json index 4612c1b..c031cb8 100644 --- a/package.json +++ b/package.json @@ -1,91 +1,36 @@ { - "name": "fake-toss-payments-server", - "version": "2.0.4", - "description": "Fake toss-payments server for testing", - "main": "lib/index.js", - "typings": "lib/index.d.ts", + "private": true, + "name": "@samchon/payments", + "version": "0.0.0", + "description": "Collection of Payment system of Samchon", "scripts": { - "----------------------------------------------": "", - "build": "npm run build:sdk && npm run build:main && npm run build:test", - "build:api": "rimraf packages/api/lib && nestia sdk && npx copyfiles README.md packages/api && tsc -p packages/api/tsconfig.json", - "build:sdk": "rimraf src/api/functional && nestia sdk", - "build:swagger": "nestia swagger", - "build:main": "rimraf lib && tsc", - "build:test": "rimraf bin && tsc -p test/tsconfig.json", - "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", - "package:next": "npm run package:latest -- --tag next", - "prepare": "ts-patch install", - "-------------------------------------------------": "", - "start": "pm2 start lib/executable/server.js -i 1 --name fake-toss-payments-server --wait-ready --listen-timeout 120000 --kill-timeout 15000", - "start:reload": "pm2 reload fake-toss-payments-server", - "stop": "pm2 delete fake-toss-payments-server", - "--------------------------------------------------": "", - "test": "node bin/test" + "package:latest": "node deploy latest", + "package:next": "node deploy next", + "package:tgz": "node deploy tgz", + "prettier": "prettier packages/*/src/**/*.ts --write && prettier packages/*/test/**/*.ts --write", + "test": "npm run package:tgz 0.0.0-dev.20991231" }, "repository": { "type": "git", - "url": "https://github.com/samchon/fake-toss-payments-server" + "url": "https://github.com/samchon/payments" }, + "keywords": [ + "payment", + "iamport", + "toss", + "toss-payments", + "fake", + "SDK" + ], "author": "Jeongho Nam", "license": "MIT", "bugs": { - "url": "https://github.com/samchon/fake-toss-payments-server/issues" + "url": "https://github.com/samchon/payments/issues" }, - "homepage": "https://github.com/samchon/fake-toss-payments-server", + "homepage": "https://github.com/samchon/payments#readme", "devDependencies": { - "@nestia/e2e": "^0.3.6", - "@nestia/sdk": "^2.0.4", - "@trivago/prettier-plugin-sort-imports": "^4.0.0", - "@types/atob": "^2.1.2", - "@types/btoa": "^1.2.3", - "@types/cli": "^0.11.19", - "@types/node": "^15.6.1", - "@types/uuid": "^9.0.1", - "@typescript-eslint/eslint-plugin": "^5.26.0", - "@typescript-eslint/parser": "^5.26.0", - "cli": "^1.0.1", - "copyfiles": "^2.4.1", - "nestia": "^4.5.0", - "pm2": "^4.5.6", + "@trivago/prettier-plugin-sort-imports": "^4.2.0", "prettier": "^2.6.2", - "rimraf": "^3.0.2", - "sloc": "^0.2.1", - "ts-node": "^10.9.1", - "ts-patch": "^3.0.2", - "typescript": "^5.1.6", - "typescript-transform-paths": "^3.4.6" - }, - "dependencies": { - "@nestia/core": "^2.0.4", - "atob": "^2.1.2", - "btoa": "^1.2.1", - "serialize-error": "^4.1.0", - "source-map-support": "^0.5.19", - "tstl": "^2.5.13", - "typia": "^5.0.4", - "uuid": "^9.0.0" - }, - "keywords": [ - "toss", - "payments", - "server", - "fake", - "test", - "mock" - ], - "files": [ - "package.json", - "README.md", - "LICENSE", - "lib", - "src", - "!lib/test", - "!src/test" - ] + "typescript": "^5.2.2" + } } diff --git a/packages/api/package.json b/packages/api/package.json deleted file mode 100644 index 406b25d..0000000 --- a/packages/api/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "toss-payments-server-api", - "version": "2.0.4", - "description": "Toss Payments Server API", - "main": "lib/index.js", - "typings": "lib/index.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/samchon/fake-toss-payments-server" - }, - "author": "Jeongho Nam", - "license": "MIT", - "bugs": { - "url": "https://github.com/samchon/fake-toss-payments-server/issues" - }, - "homepage": "https://github.com/samchon/fake-toss-payments-server", - "dependencies": { - "@nestia/fetcher": "^2.0.4", - "typia": "^5.0.4" - }, - "keywords": [ - "toss", - "payments", - "sdk", - "api" - ] -} \ No newline at end of file diff --git a/packages/api/swagger.json b/packages/api/swagger.json deleted file mode 100644 index c1f4cd3..0000000 --- a/packages/api/swagger.json +++ /dev/null @@ -1,3797 +0,0 @@ -{ - "openapi": "3.0.1", - "servers": [ - { - "url": "http://localhost:30771", - "description": "fake" - }, - { - "url": "https://api.tosspayments.com", - "description": "real" - } - ], - "info": { - "title": "Toss Payments API", - "description": "Built by [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) with [nestia](https://github.com/samchon/nestia)", - "version": "2.0.4", - "license": { - "name": "MIT" - } - }, - "paths": { - "/v1/billing/authorizations/card": { - "post": { - "tags": [], - "parameters": [], - "requestBody": { - "description": "간편 결제 카드 등록 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossBilling.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "간편 결제 카드 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossBilling" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "간편 결제 카드 등록하기", - "description": "간편 결제 카드 등록하기.\n\n`billing.authorizations.card.store` 는 고객이 자신의 신록 카드를 서버에 등록해두고,\n매번 결제가 필요할 때마다 카드 정보를 반복 입력하는 일 없이 간편하게 결제를\n진행하고자 할 때, 호출되는 API 함수이다.\n\n참고로 `billing.authorizations.card.store` 는 클라이언트 어플리케이션이 토스\n페이먼츠가 제공하는 간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를\n실 서비스에서 호출하는 일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는\n상황을 시뮬레이션하기 위하여, 테스트 자동화 프로그램 수준에서 사용될 수는 있다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.billing.authorizations.card.store", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "간편 결제 카드 등록 정보", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "간편 결제 카드 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/billing/authorizations/{billingKey}": { - "post": { - "tags": [], - "parameters": [ - { - "name": "billingKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "대상 정보의 ", - "required": true - } - ], - "requestBody": { - "description": "고객 식별자 키", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossBilling.ICustomerKey" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "간편 결제 수단 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossBilling" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "간편 결제로 등록한 수단 조회하기", - "description": "간편 결제로 등록한 수단 조회하기.\n\n`billing.authorizations.at` 은 고객이 간편 결제를 위하여 토스 페이먼츠 서버에\n등록한 결제 수단을 조회하는 함수이다.\n\n주로 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 제공하는 결제 창을 사용하는\n경우, 그래서 프론트 어플리케이션이 귀하의 백엔드 서버에 `billingKey` 와` customerKey`\n만을 전달해주어, 상세 간편 결제 수단 정보가 필요할 때 사용한다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.billing.authorizations.at", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "billingKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "대상 정보의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossBilling.billingKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossBilling.ts", - "textSpan": { - "start": 500, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "고객 식별자 키", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "간편 결제 수단 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/billing/{billingKey}": { - "post": { - "tags": [], - "parameters": [ - { - "name": "billingKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "간편 결제에 등록한 수단의 ", - "required": true - } - ], - "requestBody": { - "description": "주문 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossBilling.IPaymentStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "간편 결제에 등록한 수단으로 결제하기", - "description": "간편 결제에 등록한 수단으로 결제하기.\n\n`billing.pay` 는 간편 결제에 등록한 수단으로 결제를 진행하고자 할 때 호출하는 API\n함수이다.\n\n그리고 `billing.pay` 는 결제 수단 중 유일하게, 클라이언트 어플리케이션이 토스\n페이먼츠가 제공하는 결제 창을 사용할 수 없어, 귀하의 백엔드 서버가 토스 페이먼츠의\nAPI 함수를 직접 호출해야 하는 경우에 해당한다. 따라서 간편 결제에 관련하여 토스\n페이먼츠와 연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에\n대한 별도의 설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다.\n\n더하여 `billing.pay` 는 철저히 귀사 백엔드 서버의 판단 아래 호출되는 API 함수인지라,\n이를 통하여 이루어지는 결제는 일절 {@link payments.approve } 가 필요 없다. 다만\n`billing.pay` 는 이처럼 부차적인 승인 과정 필요없이 그 즉시로 결제가 완성되니, 이를\n호출하는 상황에 대하여 세심히 주의를 기울일 필요가 있다", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.billing.pay", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "billingKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "간편 결제에 등록한 수단의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossBilling.billingKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossBilling.ts", - "textSpan": { - "start": 500, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "주문 정보", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/cash-receipts": { - "post": { - "tags": [], - "parameters": [], - "requestBody": { - "description": "입력 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossCashReceipt.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "현금 영수증 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossCashReceipt" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "현금 영수증 발행하기", - "description": "현금 영수증 발행하기.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.cash-receipts.store", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "입력 정보", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "현금 영수증 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/cash-receipts/{receiptKey}/cancel": { - "post": { - "tags": [], - "parameters": [ - { - "name": "receiptKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "현금 영수증의 ", - "required": true - } - ], - "requestBody": { - "description": "취소 입력 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossCashReceipt.ICancel" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "취소된 현금 영수증 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossCashReceipt" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "현금 영수증 취소하기", - "description": "현금 영수증 취소하기.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.cash-receipts.cancel.cancel", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "receiptKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "현금 영수증의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossCashReceipt.receiptKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts", - "textSpan": { - "start": 199, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "취소 입력 정보", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "취소된 현금 영수증 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/internal/webhook": { - "post": { - "tags": [], - "parameters": [], - "requestBody": { - "description": "웹훅 이벤트 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPaymentWebhook" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "", - "x-nestia-encrypted": false - } - }, - "summary": "웹훅 이벤트 더미 리스너", - "description": "웹훅 이벤트 더미 리스너.\n\n`internal.webhook` 은 실제 토스 페이먼츠의 결제 서버에는 존재하지 않는 API 로써,\n`fake-toss-payments-server` 의 {@link Configuration.WEBHOOK_URL } 에 아무런 URL 을\n설정하지 않으면, `fake-toss-payments-server` 로부터 발생하는 모든 종류의 웹훅\n이벤트는 이 곳으로 전달되어 무의미하게 사라진다.\n\n따라서 `fake-toss-payments-server` 를 사용하여 토스 페이먼츠 서버와의 연동을 미리\n검증코자 할 때는, 반드시 {@link Configuration.WEBHOOK_URL } 를 설정하여 웹훅\n이벤트가 귀하의 백엔드 서버로 제대로 전달되도록 하자.", - "x-nestia-namespace": "internal.webhook.webhook", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "웹훅 이벤트 정보", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/internal/{paymentKey}/deposit": { - "get": { - "tags": [], - "parameters": [ - { - "name": "paymentKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "대상 가상 계좌 결제 정보의 ", - "required": true - } - ], - "responses": { - "200": { - "description": "입금 완료된 가상 꼐좌 결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "가상 계좌에 입금하기", - "description": "가상 계좌에 입금하기.\n\n`internal.virtual_accounts.deposit` 은 실제 토스 페이먼츠의 결제 서버에는 존재하지\n않는 API 로써, 가상 계좌 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는\n상황을 시뮬레이션할 수 있는 함수이다.\n\n즉 `internal.virtual_accounts.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에\n입금을 하고, 그에 따라 토스 페이먼츠 서버에서 webhook 이벤트가 발생하여 이를 귀하의\n백엔드 서버로 전송하는 일련의 상황을 테스트하기 위한 함수인 셈이다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "internal.deposit.deposit", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "paymentKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "대상 가상 계좌 결제 정보의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossPayment.paymentKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossPayment.ts", - "textSpan": { - "start": 2480, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "입금 완료된 가상 꼐좌 결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "GET" - } - }, - "/v1/payments/{paymentKey}": { - "get": { - "tags": [], - "parameters": [ - { - "name": "paymentKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "결제 정보의 ", - "required": true - } - ], - "responses": { - "200": { - "description": "결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "결제 정보 조회하기", - "description": "결제 정보 조회하기.\n\n`payments.at` 은 결제 정보를 조회하는 함수이다.\n\n주로 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로 제공하는 결제 창을 사용하는\n경우, 그래서 프론트 어플리케이션이 귀하의 백엔드 서버에 `paymentKey` 등 극히 일부의\n식별자 정보만을 전달해주어, 상세 결제 정보가 필요할 때 사용한다.\n\n참고로 토스 페이먼츠는 다른 결제 PG 사들과 다르게, 클라이언트 어플리케이션에서 토스\n페이먼츠의 결제 창을 이용하여 진행한 결제가 바로 확정되는 것은 아니다. 귀사의 백엔드\n서버가 현재의 `payments.at` 을 통하여 해당 결제 정보를 확인하고, {@link approve } 를\n호출하여 직접 승인하기 전까지, 해당 결제는 확정되지 않으니, 이 점에 유의하기 바란다.", - "x-nestia-namespace": "v1.payments.at", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "paymentKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "결제 정보의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossPayment.paymentKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossPayment.ts", - "textSpan": { - "start": 2480, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "결제 정보", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "GET" - }, - "post": { - "tags": [], - "parameters": [ - { - "name": "paymentKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "대상 결제의 ", - "required": true - } - ], - "requestBody": { - "description": "주문 정보 확인", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPayment.IApproval" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "승인된 결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "결제 승인하기", - "description": "결제 승인하기.\n\n토스 페이먼츠는 귀사의 백엔드에서 일어난 결제가 아닌 프론트 어플리케이션의 결제 창에서\n이루어진 결제의 경우, 해당 서비스으 백엔드 서버로부터 결제를 승인받기 전까지, 이를\n확정하지 않는다. `payments.approve` 는 바로 이러한 상황에서, 해당 결제를 승인해주는\n함수이다.\n\n만일 귀하가 `fake-toss-payments-server` 를 이용하여 결제를 시뮬레이션하는 경우라면,\n결제 관련 API 를 호출함에 있어 {@link ITossCardPayment.IStore.__approved } 내지\n{@link ITossVirtualAccountPayment.IStore.__approved } 를 `false` 로 함으로써, 별도\n승인이 필요한 이러한 상황을 시뮬레이션 할 수 있다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.payments.approve", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "paymentKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "대상 결제의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossPayment.paymentKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossPayment.ts", - "textSpan": { - "start": 2480, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "주문 정보 확인", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "승인된 결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/payments/key-in": { - "post": { - "tags": [], - "parameters": [], - "requestBody": { - "description": "카드 결제 입력 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossCardPayment.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "카드 결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossCardPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "카드로 결제하기", - "description": "카드로 결제하기.\n\n`payments.key_in` 은 카드를 이용한 결제를 할 때 호출되는 API 함수이다.\n\n참고로 `payments.key_in` 는 클라이언트 어플리케이션이 토스 페이먼츠가 자체적으로\n제공하는 결제 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는\n일은 없을 것이다. 다만, 고객이 카드를 통하여 결제하는 상황을 시뮬레이션하기 위하여,\n테스트 자동화 프로그램 수준에서 사용될 수는 있다.\n\n그리고 귀하의 백엔드 서버가 `payments.key-in` 을 직접 호출하는 경우, 토스 페이먼츠\n서버는 이를 완전히 승인된 결제로 보고 바로 확정한다. 때문에 `payments.key-in` 을\n직접 호출하는 경우, 토스 페이먼츠의 결제 창을 이용하여 별도의 {@link approve } 가\n필요한 때 대비, 훨씬 더 세심한 주의가 요구된다.\n\n더하여 만약 귀하의 백엔드 서버가 `fake-toss-payments-server` 를 이용하여 고객의\n카드 결제를 시뮬레이션하는 경우, {@link ITossCardPayment.IStore.__approved } 값을\n`false` 로 하여 카드 결제의 확정을 고의로 회피할 수 있다. 이를 통하여 토스\n페이먼츠의 결제 창을 이용한 카드 결제의 경우, 별도의 {@link approve } 가 필요한\n상황을 시뮬레이션 할 수 있다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.payments.key-in.key_in", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "카드 결제 입력 정보", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "카드 결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/payments/{paymentKey}/cancel": { - "post": { - "tags": [], - "parameters": [ - { - "name": "paymentKey", - "in": "path", - "schema": { - "type": "string" - }, - "description": "결제 정보의 ", - "required": true - } - ], - "requestBody": { - "description": "취소 입력 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPaymentCancel.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "취소된 결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "결제 취소하기", - "description": "결제 취소하기.\n\n`payments.cancel` 은 결제를 취소하는 API 이다.\n\n결제 취소 입력 정보 {@link ITossPaymentCancel.IStore } 에는 취소 사유를 비롯하여,\n고객에게 환불해 줄 금액과 부가세 및 필요시 환불 계좌 정보 등을 입력하게 되어있다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.payments.cancel.cancel", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "paymentKey", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "결제 정보의 ", - "kind": "text" - }, - { - "text": "{@link ", - "kind": "link" - }, - { - "text": "ITossPayment.paymentKey", - "kind": "linkName", - "target": { - "fileName": "D:/github/samchon/fake-toss-payments-server/src/api/structures/ITossPayment.ts", - "textSpan": { - "start": 2480, - "length": 19 - } - } - }, - { - "text": "}", - "kind": "link" - } - ] - }, - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "취소 입력 정보", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "취소된 결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - }, - "/v1/virtual-accounts": { - "post": { - "tags": [], - "parameters": [], - "requestBody": { - "description": "가상 결제 신청 정보.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossVirtualAccountPayment.IStore" - } - } - }, - "required": true, - "x-nestia-encrypted": false - }, - "responses": { - "201": { - "description": "가상 계좌 결제 정보", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ITossVirtualAccountPayment" - } - } - }, - "x-nestia-encrypted": false - } - }, - "summary": "가상 계좌로 결제 신청하기", - "description": "가상 계좌로 결제 신청하기.\n\n`virtual_accounts.store` 는 고객이 결제 수단을 가상 계좌로 선택하는 경우에 호출되는\nAPI 함수이다. 물론 고객이 이처럼 가상 계좌를 선택한 경우, 고객이 지정된 계좌에 돈을\n입금하기 전까지는 결제가 마무리된 것이 아니기에, {@link ITossPayment.status } 값은\n`WAITING_FOR_DEPOSIT` 이 된다.\n\n참고로 `virtual_accounts.store` 는 클라이언트 어플리케이션이 토스 페이먼츠가\n자체적으로 제공하는 결제 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서\n호출하는 일은 없을 것이다. 다만, 고객이 가상 계좌로 결제를 진행하는 상황을\n시뮬레이션하기 위하여, 테스트 자동화 프로그램 수준에서 사용될 수는 있다.\n\n그리고 `virtual_accounts.store` 이후에 고객이 지정된 계좌에 금액을 입금하거든, 토스\n페이먼츠 서버로부터 웹훅 이벤트가 발생되어 귀하의 백엔드 서버로 전송된다. 만약 연동\n대상 토스 페이먼츠 서버가 실제가 아닌 `fake-toss-payments-server` 라면,\n{@link internal.virtual_accounts.deposit } 를 호출하여, 고객이 가상 계좌에 입금하는\n상황을 시뮬레이션 할 수 있다.", - "security": [ - { - "basic": [] - } - ], - "x-nestia-namespace": "v1.virtual-accounts.store", - "x-nestia-jsDocTags": [ - { - "name": "param", - "text": [ - { - "text": "input", - "kind": "parameterName" - }, - { - "text": " ", - "kind": "space" - }, - { - "text": "가상 결제 신청 정보.", - "kind": "text" - } - ] - }, - { - "name": "returns", - "text": [ - { - "text": "가상 계좌 결제 정보", - "kind": "text" - } - ] - }, - { - "name": "security", - "text": [ - { - "text": "basic", - "kind": "text" - } - ] - }, - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ], - "x-nestia-method": "POST" - } - } - }, - "components": { - "schemas": { - "ITossBilling.IStore": { - "type": "object", - "properties": { - "cardNumber": { - "description": "카드 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "[0-9]{16}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"[0-9]{16}\">", - "kind": "pattern", - "value": "[0-9]{16}", - "validate": "/[0-9]{16}/.test($input)", - "exclusive": false - } - ] - }, - "cardExpirationYear": { - "description": "카드 만료 년도 (2 자리).", - "x-typia-required": true, - "x-typia-optional": false, - "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": false - } - ] - }, - "cardExpirationMonth": { - "description": "카드 만료 월 (2 자리).", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "^(0[1-9]|1[012])$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^(0[1-9]|1[012])$\">", - "kind": "pattern", - "value": "^(0[1-9]|1[012])$", - "validate": "/^(0[1-9]|1[012])$/.test($input)", - "exclusive": false - } - ] - }, - "cardPassword": { - "description": "카드 비밀번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "customerBirthday": { - "description": "고객의 생년월일.\n\n표기 형식 YYMMDD.", - "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])$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$\">", - "kind": "pattern", - "value": "^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", - "validate": "/^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/.test($input)", - "exclusive": false - } - ] - }, - "consumerName": { - "description": "고객의 이름.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "customerEmail": { - "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": [ - "pattern" - ] - } - ] - }, - "vbv": { - "$ref": "#/components/schemas/__type" - }, - "customerKey": { - "description": "고객 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "cardNumber", - "cardExpirationYear", - "cardExpirationMonth", - "cardPassword", - "customerBirthday", - "customerKey" - ], - "description": "간편 결제 카드 등록 정보.", - "x-typia-jsDocTags": [] - }, - "__type": { - "type": "object", - "properties": { - "cavv": { - "description": "3D Secure 인증 세션에 대한 인증 값.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "xid": { - "description": "트랜잭션 ID.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "eci": { - "description": "3DS 인증 결과에 대한 코드 값.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "cavv", - "xid", - "eci" - ], - "x-typia-jsDocTags": [] - }, - "ITossBilling": { - "type": "object", - "properties": { - "mId": { - "description": "가맹점 ID.\n\n현재 tosspayments 가 쓰임.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "billingKey": { - "description": "{@link ITossBilling } 의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "method": { - "description": "결제 수단.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "카드" - ] - }, - "cardCompany": { - "description": "카드사 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "cardNumber": { - "description": "카드 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "[0-9]{16}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"[0-9]{16}\">", - "kind": "pattern", - "value": "[0-9]{16}", - "validate": "/[0-9]{16}/.test($input)", - "exclusive": false - } - ] - }, - "authenticatedAt": { - "description": "인증 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "customerKey": { - "description": "고객 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "mId", - "billingKey", - "method", - "cardCompany", - "cardNumber", - "authenticatedAt", - "customerKey" - ], - "description": "간편 결제 등록 수단 정보.\n\n`ITossBilling` 은 간편 결제 등록 수단을 형상화한 자료구조 인터페이스로써, 고객이 자신의\n신용 카드를 서버에 등록해두고, 매번 결제가 필요할 때마다 카드 정보를 반복 입려하는 일\n없이 간편하게 결제를 진행하고자 할 때 사용한다.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossBilling.ICustomerKey": { - "type": "object", - "properties": { - "customerKey": { - "description": "고객 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "customerKey" - ], - "description": "고객 식별자 정보.", - "x-typia-jsDocTags": [] - }, - "ITossBilling.IPaymentStore": { - "type": "object", - "properties": { - "method": { - "description": "결제 수단이 간편 결제임을 의미함.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "billing" - ] - }, - "billingKey": { - "description": "{@link IPaymentStore } 의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "amount": { - "description": "결제 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "customerKey": { - "description": "고객 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "method", - "billingKey", - "orderId", - "amount", - "customerKey" - ], - "description": "간편 결제를 이용한 결제 신청 정보.", - "x-typia-jsDocTags": [] - }, - "ITossPayment": { - "oneOf": [ - { - "$ref": "#/components/schemas/ITossCardPayment" - }, - { - "$ref": "#/components/schemas/ITossGiftCertificatePayment" - }, - { - "$ref": "#/components/schemas/ITossMobilePhonePayment" - }, - { - "$ref": "#/components/schemas/ITossTransferPayment" - }, - { - "$ref": "#/components/schemas/ITossVirtualAccountPayment" - } - ], - "description": "결제 정보.\n\n`ITossPayment` 는 토스 페이먼츠의 결제 정보를 형상화한 자료구조이자 유니언 타입의 \n인터페이스로써, if condition 을 통하여 method 값을 특정하면, 파생 타입이 자동으로\n지정된다.\n\n```typescript\nif (payment.method === \"카드\")\n payment.card; // payment be ITossCardPayment\n```", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossCardPayment": { - "type": "object", - "properties": { - "card": { - "$ref": "#/components/schemas/ITossCardPayment.ICard" - }, - "discount": { - "$ref": "#/components/schemas/ITossCardPayment.IDiscount.Nullable" - }, - "easyPay": { - "description": "간편결제로 결제한 경우 간편결제 타입 정보.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "토스결제", - "페이코", - "삼성페이" - ], - "nullable": true - }, - "method": { - "description": "결제 수단.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "카드" - ] - }, - "type": { - "description": "결제 타입.\n\n - NORMAL: 일반 결제\n - BILLING: 미리 등록한 카드에 의한 간편 결제.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "NORMAL", - "BILLING" - ] - }, - "status": { - "description": "결제 상태.\n\n - READY\n - IN_PROGRESS\n - WAITING_FOR_DEPOSIT\n - DONE\n - CANCELED\n - PARTIAL_CANCELED\n - ABORTED\n - EXPIRED", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "READY", - "IN_PROGRESS", - "WAITING_FOR_DEPOSIT", - "DONE", - "CANCELED", - "PARTIAL_CANCELED", - "ABORTED", - "EXPIRED" - ] - }, - "mId": { - "description": "가맹점 ID.\n\n현재 tosspayments 가 쓰임.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "version": { - "description": "사용 중인 토스 페이먼츠 API 의 버전.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "paymentKey": { - "description": "결제 내역의 식별자 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "transactionKey": { - "description": "거래 건에 대한 고유한 키 값.\n\n{@link paymentKey } 와 달리, 이를 사용할 일은 없더라.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "currency": { - "description": "화폐 단위.\n\n현재 토스 페이먼츠는 KRW 만 사용 가능.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "totalAmount": { - "description": "총 결제 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "balanceAmount": { - "description": "취소할 수 있는 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "suppliedAmount": { - "description": "공급가액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "vat": { - "description": "부가세.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "useEscrow": { - "description": "에스크로 사용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "cultureExpense": { - "description": "문화비 지출 여부.\n\n도석입, 공연 티켓, 박물관/미술관 입장권 등.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "requestedAt": { - "description": "결제 요청 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "approvedAt": { - "description": "결제 승인 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ], - "nullable": true - }, - "cancels": { - "description": "결제 취소 내역.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/ITossPaymentCancel" - }, - "nullable": true - }, - "cashReceipt": { - "$ref": "#/components/schemas/ITossCashReceipt.ISummary.Nullable" - } - }, - "nullable": false, - "required": [ - "card", - "discount", - "easyPay", - "method", - "type", - "status", - "mId", - "version", - "paymentKey", - "orderId", - "transactionKey", - "orderName", - "currency", - "totalAmount", - "balanceAmount", - "suppliedAmount", - "taxFreeAmount", - "vat", - "useEscrow", - "cultureExpense", - "requestedAt", - "approvedAt", - "cancels", - "cashReceipt" - ], - "description": "카드 결제 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossCardPayment.ICard": { - "type": "object", - "properties": { - "company": { - "description": "카드사 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "number": { - "description": "카드 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "[0-9]{16}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"[0-9]{16}\">", - "kind": "pattern", - "value": "[0-9]{16}", - "validate": "/[0-9]{16}/.test($input)", - "exclusive": false - } - ] - }, - "installmentPlanMonths": { - "description": "할부 개월 수.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "isInterestFree": { - "description": "무이자 할부 적용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "approveNo": { - "description": "승인 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "useCardPoint": { - "description": "카드 포인트 사용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean", - "enum": [ - false - ] - }, - "cardType": { - "description": "카드 타입.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "신용", - "체크", - "기프트" - ] - }, - "ownerType": { - "description": "카드의 소유자 타입.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "개인", - "법인" - ] - }, - "acquireStatus": { - "description": "카드 결제의 매입 상태.\n\n - READY: 매입 대기\n - REQUESTED: 매입 요청됨\n - COMPLETED: 매입 완료\n - CANCEL_REQUESTED: 매입 취소 요청됨\n - CANCELD: 매입 취소됨", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "READY", - "CANCELED", - "REQUESTED", - "COMPLETED", - "CANCEL_REQUESTED" - ] - }, - "receiptUrl": { - "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": [ - "pattern" - ] - } - ] - } - }, - "nullable": false, - "required": [ - "company", - "number", - "installmentPlanMonths", - "isInterestFree", - "approveNo", - "useCardPoint", - "cardType", - "ownerType", - "acquireStatus", - "receiptUrl" - ], - "description": "카드 정보.", - "x-typia-jsDocTags": [] - }, - "ITossCardPayment.IDiscount.Nullable": { - "type": "object", - "properties": { - "amount": { - "description": "카드사의 즉시 할인 프로모션을 적용한 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - } - }, - "nullable": true, - "required": [ - "amount" - ], - "description": "카드사의 즉시 할인 프로모션 정보.", - "x-typia-jsDocTags": [] - }, - "ITossPaymentCancel": { - "type": "object", - "properties": { - "cancelAmount": { - "description": "취소 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "cancelReason": { - "description": "취소 사유.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "taxFreeAmount": { - "description": "면세 처리된 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxAmount": { - "description": "과세 처리된 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "refundableAmount": { - "description": "결제 취소 후 환불 가능 잔액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "canceledAt": { - "description": "취소 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - } - }, - "nullable": false, - "required": [ - "cancelAmount", - "cancelReason", - "taxFreeAmount", - "taxAmount", - "refundableAmount", - "canceledAt" - ], - "description": "결제 취소 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossCashReceipt.ISummary.Nullable": { - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/ITossCashReceipt.Type" - }, - "amount": { - "description": "현금 영수증 처리된 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세 처리된 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "issueNumber": { - "description": "현금영수증 발급번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "receiptUrl": { - "description": "현금영수증 조회 페이지 주소.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": true, - "required": [ - "type", - "amount", - "taxFreeAmount", - "issueNumber", - "receiptUrl" - ], - "description": "현금 영수증 요약 정보.", - "x-typia-jsDocTags": [] - }, - "ITossCashReceipt.Type": { - "description": "현금 영수증의 종류.", - "type": "string", - "enum": [ - "소득공제", - "지출증빙" - ] - }, - "ITossGiftCertificatePayment": { - "type": "object", - "properties": { - "giftCertificate": { - "$ref": "#/components/schemas/ITossGiftCertificatePayment.IGiftCertificate" - }, - "method": { - "description": "결제 수단.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "상품권" - ] - }, - "type": { - "description": "결제 타입.\n\n - NORMAL: 일반 결제\n - BILLING: 미리 등록한 카드에 의한 간편 결제.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "NORMAL" - ] - }, - "status": { - "description": "결제 상태.\n\n - READY\n - IN_PROGRESS\n - WAITING_FOR_DEPOSIT\n - DONE\n - CANCELED\n - PARTIAL_CANCELED\n - ABORTED\n - EXPIRED", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "READY", - "IN_PROGRESS", - "WAITING_FOR_DEPOSIT", - "DONE", - "CANCELED", - "PARTIAL_CANCELED", - "ABORTED", - "EXPIRED" - ] - }, - "mId": { - "description": "가맹점 ID.\n\n현재 tosspayments 가 쓰임.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "version": { - "description": "사용 중인 토스 페이먼츠 API 의 버전.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "paymentKey": { - "description": "결제 내역의 식별자 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "transactionKey": { - "description": "거래 건에 대한 고유한 키 값.\n\n{@link paymentKey } 와 달리, 이를 사용할 일은 없더라.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "currency": { - "description": "화폐 단위.\n\n현재 토스 페이먼츠는 KRW 만 사용 가능.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "totalAmount": { - "description": "총 결제 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "balanceAmount": { - "description": "취소할 수 있는 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "suppliedAmount": { - "description": "공급가액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "vat": { - "description": "부가세.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "useEscrow": { - "description": "에스크로 사용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "cultureExpense": { - "description": "문화비 지출 여부.\n\n도석입, 공연 티켓, 박물관/미술관 입장권 등.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "requestedAt": { - "description": "결제 요청 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "approvedAt": { - "description": "결제 승인 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ], - "nullable": true - }, - "cancels": { - "description": "결제 취소 내역.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/ITossPaymentCancel" - }, - "nullable": true - }, - "cashReceipt": { - "$ref": "#/components/schemas/ITossCashReceipt.ISummary.Nullable" - } - }, - "nullable": false, - "required": [ - "giftCertificate", - "method", - "type", - "status", - "mId", - "version", - "paymentKey", - "orderId", - "transactionKey", - "orderName", - "currency", - "totalAmount", - "balanceAmount", - "suppliedAmount", - "taxFreeAmount", - "vat", - "useEscrow", - "cultureExpense", - "requestedAt", - "approvedAt", - "cancels", - "cashReceipt" - ], - "description": "상품권 결제 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossGiftCertificatePayment.IGiftCertificate": { - "type": "object", - "properties": { - "approveNo": { - "description": "승인 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "settlementStatus": { - "description": "정산 상태.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "COMPLETE", - "INCOMPLETE" - ] - } - }, - "nullable": false, - "required": [ - "approveNo", - "settlementStatus" - ], - "description": "상품권 정보.", - "x-typia-jsDocTags": [] - }, - "ITossMobilePhonePayment": { - "type": "object", - "properties": { - "mobilePhone": { - "$ref": "#/components/schemas/ITossMobilePhonePayment.IMobilePhone" - }, - "method": { - "description": "결제 수단.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "휴대폰" - ] - }, - "type": { - "description": "결제 타입.\n\n - NORMAL: 일반 결제\n - BILLING: 미리 등록한 카드에 의한 간편 결제.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "NORMAL" - ] - }, - "status": { - "description": "결제 상태.\n\n - READY\n - IN_PROGRESS\n - WAITING_FOR_DEPOSIT\n - DONE\n - CANCELED\n - PARTIAL_CANCELED\n - ABORTED\n - EXPIRED", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "READY", - "IN_PROGRESS", - "WAITING_FOR_DEPOSIT", - "DONE", - "CANCELED", - "PARTIAL_CANCELED", - "ABORTED", - "EXPIRED" - ] - }, - "mId": { - "description": "가맹점 ID.\n\n현재 tosspayments 가 쓰임.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "version": { - "description": "사용 중인 토스 페이먼츠 API 의 버전.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "paymentKey": { - "description": "결제 내역의 식별자 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "transactionKey": { - "description": "거래 건에 대한 고유한 키 값.\n\n{@link paymentKey } 와 달리, 이를 사용할 일은 없더라.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "currency": { - "description": "화폐 단위.\n\n현재 토스 페이먼츠는 KRW 만 사용 가능.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "totalAmount": { - "description": "총 결제 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "balanceAmount": { - "description": "취소할 수 있는 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "suppliedAmount": { - "description": "공급가액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "vat": { - "description": "부가세.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "useEscrow": { - "description": "에스크로 사용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "cultureExpense": { - "description": "문화비 지출 여부.\n\n도석입, 공연 티켓, 박물관/미술관 입장권 등.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "requestedAt": { - "description": "결제 요청 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "approvedAt": { - "description": "결제 승인 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ], - "nullable": true - }, - "cancels": { - "description": "결제 취소 내역.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/ITossPaymentCancel" - }, - "nullable": true - }, - "cashReceipt": { - "$ref": "#/components/schemas/ITossCashReceipt.ISummary.Nullable" - } - }, - "nullable": false, - "required": [ - "mobilePhone", - "method", - "type", - "status", - "mId", - "version", - "paymentKey", - "orderId", - "transactionKey", - "orderName", - "currency", - "totalAmount", - "balanceAmount", - "suppliedAmount", - "taxFreeAmount", - "vat", - "useEscrow", - "cultureExpense", - "requestedAt", - "approvedAt", - "cancels", - "cashReceipt" - ], - "description": "휴대폰 결제 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossMobilePhonePayment.IMobilePhone": { - "type": "object", - "properties": { - "carrier": { - "description": "통신사.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "customerMobilePhone": { - "description": "고객 휴대폰 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "settlementStatus": { - "description": "정산 상태.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "COMPLETED", - "INCOMPLETED" - ] - } - }, - "nullable": false, - "required": [ - "carrier", - "customerMobilePhone", - "settlementStatus" - ], - "description": "휴대폰 정보.", - "x-typia-jsDocTags": [] - }, - "ITossTransferPayment": { - "type": "object", - "properties": { - "transfer": { - "$ref": "#/components/schemas/ITossTransferPayment.ITransfer" - }, - "method": { - "description": "결제 수단.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "계좌이체" - ] - }, - "type": { - "description": "결제 타입.\n\n - NORMAL: 일반 결제\n - BILLING: 미리 등록한 카드에 의한 간편 결제.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "NORMAL" - ] - }, - "status": { - "description": "결제 상태.\n\n - READY\n - IN_PROGRESS\n - WAITING_FOR_DEPOSIT\n - DONE\n - CANCELED\n - PARTIAL_CANCELED\n - ABORTED\n - EXPIRED", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "READY", - "IN_PROGRESS", - "WAITING_FOR_DEPOSIT", - "DONE", - "CANCELED", - "PARTIAL_CANCELED", - "ABORTED", - "EXPIRED" - ] - }, - "mId": { - "description": "가맹점 ID.\n\n현재 tosspayments 가 쓰임.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "version": { - "description": "사용 중인 토스 페이먼츠 API 의 버전.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "paymentKey": { - "description": "결제 내역의 식별자 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "transactionKey": { - "description": "거래 건에 대한 고유한 키 값.\n\n{@link paymentKey } 와 달리, 이를 사용할 일은 없더라.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "currency": { - "description": "화폐 단위.\n\n현재 토스 페이먼츠는 KRW 만 사용 가능.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "totalAmount": { - "description": "총 결제 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "balanceAmount": { - "description": "취소할 수 있는 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "suppliedAmount": { - "description": "공급가액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "vat": { - "description": "부가세.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "useEscrow": { - "description": "에스크로 사용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "cultureExpense": { - "description": "문화비 지출 여부.\n\n도석입, 공연 티켓, 박물관/미술관 입장권 등.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "requestedAt": { - "description": "결제 요청 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "approvedAt": { - "description": "결제 승인 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ], - "nullable": true - }, - "cancels": { - "description": "결제 취소 내역.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/ITossPaymentCancel" - }, - "nullable": true - }, - "cashReceipt": { - "$ref": "#/components/schemas/ITossCashReceipt.ISummary.Nullable" - } - }, - "nullable": false, - "required": [ - "transfer", - "method", - "type", - "status", - "mId", - "version", - "paymentKey", - "orderId", - "transactionKey", - "orderName", - "currency", - "totalAmount", - "balanceAmount", - "suppliedAmount", - "taxFreeAmount", - "vat", - "useEscrow", - "cultureExpense", - "requestedAt", - "approvedAt", - "cancels", - "cashReceipt" - ], - "description": "계좌 이체 결제 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossTransferPayment.ITransfer": { - "type": "object", - "properties": { - "bank": { - "description": "은행명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "settlementStatus": { - "description": "이체 상태.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "COMPLETED", - "INCOMPLETED" - ] - } - }, - "nullable": false, - "required": [ - "bank", - "settlementStatus" - ], - "description": "계좌 이체 정보.", - "x-typia-jsDocTags": [] - }, - "ITossVirtualAccountPayment": { - "type": "object", - "properties": { - "secret": { - "description": "가상 계좌로 결제할 때 전달되는 입금 콜백을 검증하기 위한 값.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "virtualAccount": { - "$ref": "#/components/schemas/ITossVirtualAccountPayment.IVirtualAccount" - }, - "method": { - "description": "결제 수단.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "가상계좌" - ] - }, - "type": { - "description": "결제 타입.\n\n - NORMAL: 일반 결제\n - BILLING: 미리 등록한 카드에 의한 간편 결제.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "NORMAL" - ] - }, - "status": { - "description": "결제 상태.\n\n - READY\n - IN_PROGRESS\n - WAITING_FOR_DEPOSIT\n - DONE\n - CANCELED\n - PARTIAL_CANCELED\n - ABORTED\n - EXPIRED", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "READY", - "IN_PROGRESS", - "WAITING_FOR_DEPOSIT", - "DONE", - "CANCELED", - "PARTIAL_CANCELED", - "ABORTED", - "EXPIRED" - ] - }, - "mId": { - "description": "가맹점 ID.\n\n현재 tosspayments 가 쓰임.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "version": { - "description": "사용 중인 토스 페이먼츠 API 의 버전.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "paymentKey": { - "description": "결제 내역의 식별자 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "transactionKey": { - "description": "거래 건에 대한 고유한 키 값.\n\n{@link paymentKey } 와 달리, 이를 사용할 일은 없더라.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "currency": { - "description": "화폐 단위.\n\n현재 토스 페이먼츠는 KRW 만 사용 가능.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "totalAmount": { - "description": "총 결제 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "balanceAmount": { - "description": "취소할 수 있는 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "suppliedAmount": { - "description": "공급가액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "vat": { - "description": "부가세.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "useEscrow": { - "description": "에스크로 사용 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "cultureExpense": { - "description": "문화비 지출 여부.\n\n도석입, 공연 티켓, 박물관/미술관 입장권 등.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "requestedAt": { - "description": "결제 요청 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "approvedAt": { - "description": "결제 승인 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ], - "nullable": true - }, - "cancels": { - "description": "결제 취소 내역.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "array", - "items": { - "$ref": "#/components/schemas/ITossPaymentCancel" - }, - "nullable": true - }, - "cashReceipt": { - "$ref": "#/components/schemas/ITossCashReceipt.ISummary.Nullable" - } - }, - "nullable": false, - "required": [ - "secret", - "virtualAccount", - "method", - "type", - "status", - "mId", - "version", - "paymentKey", - "orderId", - "transactionKey", - "orderName", - "currency", - "totalAmount", - "balanceAmount", - "suppliedAmount", - "taxFreeAmount", - "vat", - "useEscrow", - "cultureExpense", - "requestedAt", - "approvedAt", - "cancels", - "cashReceipt" - ], - "description": "가상 계좌 결제 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossVirtualAccountPayment.IVirtualAccount": { - "type": "object", - "properties": { - "accountNumber": { - "description": "계좌 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "accountType": { - "description": "가상 계좌 타입.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "일반", - "고정" - ] - }, - "bank": { - "description": "은행명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "customerName": { - "description": "고객 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "dueDate": { - "description": "입금 기한.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date\">", - "kind": "format", - "value": "date", - "validate": "/^(\\d{4})-(\\d{2})-(\\d{2})$/.test($input)", - "exclusive": [ - "pattern" - ] - } - ] - }, - "expired": { - "description": "가상 계좌 만료 여부.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "boolean" - }, - "settlementStatus": { - "description": "정산 상태.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "COMPLETED", - "INCOMPLETED" - ] - }, - "refundStatus": { - "description": "환불 처리 상태.\n\n - NONE: 해당 없음\n - FAILED: 환불 실패\n - PENDING: 환불 처리중\n - PARTIAL_FAILED: 부분 환불 실패\n - COMPLETED: 환불 완료", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "COMPLETED", - "NONE", - "FAILED", - "PENDING", - "PARTIAL_FAILED" - ] - } - }, - "nullable": false, - "required": [ - "accountNumber", - "accountType", - "bank", - "customerName", - "dueDate", - "expired", - "settlementStatus", - "refundStatus" - ], - "description": "가상 계좌 정보.", - "x-typia-jsDocTags": [] - }, - "ITossCashReceipt.IStore": { - "type": "object", - "properties": { - "type": { - "description": "현금 영수증의 종류.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "소득공제", - "지출증빙" - ] - }, - "paymentKey": { - "description": "귀속 결제의 {@link ITossPayment.paymentKey }.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문의 식별자 ID.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "registrationNumber": { - "description": "현금 영수증 발급을 위한 개인 식별 번호.\n\n현금 영수증의 종류에 따라 휴대폰 번호나 주민등록번호 또는 사업자등록번호 및 \n카드 번호를 입력할 수 있다.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "amount": { - "description": "현금 영수증을 발행할 금액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세 금액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "businessNumber": { - "description": "사업자 등록번호.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - } - }, - "nullable": false, - "required": [ - "type", - "paymentKey", - "orderId", - "orderName", - "registrationNumber", - "amount" - ], - "description": "현금 영수증 입력 정보.", - "x-typia-jsDocTags": [] - }, - "ITossCashReceipt": { - "type": "object", - "properties": { - "receiptKey": { - "description": "현금 영수증의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/ITossCashReceipt.Type" - }, - "orderId": { - "description": "주문의 식별자 ID.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "approvalNumber": { - "description": "현금 영수증 승인 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "approvedAt": { - "description": "현금 영수증 승인 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ] - }, - "canceledAt": { - "description": "현금 영수증 취소 일시.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "format": "date-time", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Format<\"date-time\">", - "kind": "format", - "value": "date-time", - "validate": "!isNaN(new Date($input).getTime())", - "exclusive": [ - "pattern" - ] - } - ], - "nullable": true - }, - "receiptUrl": { - "description": "영수증 URL.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "receiptKey", - "type", - "orderId", - "orderName", - "approvalNumber", - "approvedAt", - "canceledAt", - "receiptUrl" - ], - "description": "현금 영수증 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossCashReceipt.ICancel": { - "type": "object", - "properties": { - "amount": { - "description": "취소 금액.\n\n미 입력시 현금 영수증에 기재된 {@link ITossCashReceipt.amount 총액}이 취소됨.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - } - }, - "nullable": false, - "description": "현금 영수증 취소 입력 정보.", - "x-typia-jsDocTags": [] - }, - "ITossPaymentWebhook": { - "type": "object", - "properties": { - "eventType": { - "description": "이벤트 타입.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "PAYMENT_STATUS_CHANGED" - ] - }, - "data": { - "$ref": "#/components/schemas/ITossPaymentWebhook.IData" - } - }, - "nullable": false, - "required": [ - "eventType", - "data" - ], - "description": "웹훅 이벤트 정보.", - "x-typia-jsDocTags": [ - { - "name": "author", - "text": [ - { - "text": "Jeongho Nam - https://github.com/samchon", - "kind": "text" - } - ] - } - ] - }, - "ITossPaymentWebhook.IData": { - "type": "object", - "properties": { - "paymentKey": { - "description": "{@link ITossPayment } 의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "status": { - "description": "결제 상태.\n\n - DONE: 결제 완료\n - CANCELED: 결제가 취소됨\n - PARTIAL_CANCELED: 결제가 부분 취소됨\n - WAITING_FOR_DEPOSIT: 입금 대기 중", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "WAITING_FOR_DEPOSIT", - "DONE", - "CANCELED", - "PARTIAL_CANCELED" - ] - } - }, - "nullable": false, - "required": [ - "paymentKey", - "orderId", - "status" - ], - "description": "웹훅 이벤트 데이터.", - "x-typia-jsDocTags": [] - }, - "ITossCardPayment.IStore": { - "type": "object", - "properties": { - "method": { - "description": "결제 수단이 신용 카드임을 의미.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "card" - ] - }, - "cardNumber": { - "description": "카드 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "[0-9]{16}", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"[0-9]{16}\">", - "kind": "pattern", - "value": "[0-9]{16}", - "validate": "/[0-9]{16}/.test($input)", - "exclusive": false - } - ] - }, - "cardExpirationYear": { - "description": "카드 만료 년도 (2 자리).", - "x-typia-required": true, - "x-typia-optional": false, - "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": false - } - ] - }, - "cardExpirationMonth": { - "description": "카드 만료 월 (2 자리).", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "^(0[1-9]|1[012])$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^(0[1-9]|1[012])$\">", - "kind": "pattern", - "value": "^(0[1-9]|1[012])$", - "validate": "/^(0[1-9]|1[012])$/.test($input)", - "exclusive": false - } - ] - }, - "cardPassword": { - "description": "카드 비밀번호.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "cardInstallmentPlan": { - "description": "할부 개월 수.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "amount": { - "description": "지불 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세금 총액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string" - }, - "customerBirthday": { - "description": "고객의 생년월일.\n\n표기 형식 YYMMDD.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "string", - "pattern": "^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$\">", - "kind": "pattern", - "value": "^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$", - "validate": "/^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$/.test($input)", - "exclusive": false - } - ] - }, - "customerEmail": { - "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": [ - "pattern" - ] - } - ] - }, - "vbv": { - "$ref": "#/components/schemas/__type.o1" - }, - "__approved": { - "description": "결제 승인 여부.\n\n오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, \n결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 \n어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 \n상황을 시뮬레이션할 수 있다.\n\n본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고,\n토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, \n백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 \n정식 결제로 인청치 아니한다.\n\n반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는\n이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가\n필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "boolean" - } - }, - "nullable": false, - "required": [ - "method", - "cardNumber", - "cardExpirationYear", - "cardExpirationMonth", - "amount", - "orderId" - ], - "description": "신용 카드를 이용한 결제 신청 정보.", - "x-typia-jsDocTags": [] - }, - "__type.o1": { - "type": "object", - "properties": { - "cavv": { - "description": "3D Secure 인증 세션에 대한 인증 값.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "xid": { - "description": "트랜잭션 ID.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "eci": { - "description": "3DS 인증 결과에 대한 코드 값.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "cavv", - "xid", - "eci" - ], - "x-typia-jsDocTags": [] - }, - "ITossPayment.IApproval": { - "type": "object", - "properties": { - "orderId": { - "description": "주문 식별자 키.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "amount": { - "description": "결제 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - } - }, - "nullable": false, - "required": [ - "orderId", - "amount" - ], - "description": "결제 승인 정보.", - "x-typia-jsDocTags": [] - }, - "ITossPaymentCancel.IStore": { - "type": "object", - "properties": { - "paymentKey": { - "description": "{@link ITossPayment } 의 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "cancelReason": { - "description": "취소 사유.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "cancelAmount": { - "description": "취소 총액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "refundReceiveAccount": { - "$ref": "#/components/schemas/__type.o2" - }, - "taxAmount": { - "description": "과세 처리 금액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "taxFreeAmount": { - "description": "면세 처리 금액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - }, - "refundableAmount": { - "description": "결제 취소 후 환불 가능 잔액.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "number" - } - }, - "nullable": false, - "required": [ - "paymentKey", - "cancelReason" - ], - "description": "결제 취소 신청 정보.", - "x-typia-jsDocTags": [] - }, - "__type.o2": { - "type": "object", - "properties": { - "bank": { - "description": "은행 정보.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "accountNumber": { - "description": "계좌 번호.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "pattern": "^[0-9]{0,20}$", - "x-typia-typeTags": [ - { - "target": "string", - "name": "Pattern<\"^[0-9]{0,20}$\">", - "kind": "pattern", - "value": "^[0-9]{0,20}$", - "validate": "/^[0-9]{0,20}$/.test($input)", - "exclusive": false - } - ] - }, - "holderName": { - "description": "예금주.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - } - }, - "nullable": false, - "required": [ - "bank", - "accountNumber", - "holderName" - ], - "x-typia-jsDocTags": [] - }, - "ITossVirtualAccountPayment.IStore": { - "type": "object", - "properties": { - "method": { - "description": "결제 수단이 가상 계좌임을 의미.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string", - "enum": [ - "virtual-account" - ] - }, - "orderId": { - "description": "주문 식별자 번호.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "orderName": { - "description": "주문 이름.\n\n토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "bank": { - "description": "은행명.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "customerName": { - "description": "고객 이름.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "string" - }, - "amount": { - "description": "결제 총액.", - "x-typia-required": true, - "x-typia-optional": false, - "type": "number" - }, - "__approved": { - "description": "결제 승인 여부.\n\n오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, \n결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 \n어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 \n상황을 시뮬레이션할 수 있다.\n\n본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고,\n토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, \n백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 \n정식 결제로 인청치 아니한다.\n\n반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는\n이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가\n필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것.", - "x-typia-required": false, - "x-typia-optional": true, - "type": "boolean" - } - }, - "nullable": false, - "required": [ - "method", - "orderId", - "orderName", - "bank", - "customerName", - "amount" - ], - "description": "가상 계좌를 이용한 결제 신청 정보.", - "x-typia-jsDocTags": [] - } - }, - "securitySchemes": { - "basic": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - } - } - } -} \ No newline at end of file diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json deleted file mode 100644 index dcf2476..0000000 --- a/packages/api/tsconfig.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - "lib": [ - "DOM", - "ES2015" - ], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "./lib", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - "noUnusedLocals": true, /* Report errors on unused locals. */ - "noUnusedParameters": true, /* Report errors on unused parameters. */ - "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - // "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": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - "stripInternal": true, - - /* Advanced Options */ - "skipLibCheck": true, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - }, - "include": ["../../src/api"] - } \ No newline at end of file diff --git a/LICENSE b/packages/fake-iamport-server/LICENSE similarity index 100% rename from LICENSE rename to packages/fake-iamport-server/LICENSE diff --git a/packages/fake-iamport-server/README.md b/packages/fake-iamport-server/README.md new file mode 100644 index 0000000..95360f1 --- /dev/null +++ b/packages/fake-iamport-server/README.md @@ -0,0 +1,396 @@ +# Fake Iamport Server +## 1. Outline +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/fake-iamport-server/blob/master/LICENSE) +[![npm version](https://badge.fury.io/js/iamport-server-api.svg)](https://www.npmjs.com/package/iamport-server-api) +[![Downloads](https://img.shields.io/npm/dm/iamport-server-api.svg)](https://www.npmjs.com/package/iamport-server-api) +[![Build Status](https://github.com/samchon/fake-iamport-server/workflows/build/badge.svg)](https://github.com/samchon/fake-iamport-server/actions?query=workflow%3Abuild) + +`fake-iamport-server` 는 아임포트 서버의 API 를 흉내내어 만든, 가짜 아임포트 서버이다. 아임포트 서버와의 보다 원활한 연동 테스트를 위하여 만들었다. 특히 프론트 어플리케이션을 통한 수기 테스트가 아닌, 백엔드 서버 자체의 테스트 자동화 프로그램을 통한 상시 검증에 적합하다. + +또한, [iamport-server-api](https://www.npmjs.com/package/iamport-server-api) 는 아임포트 서버와 연동할 수 있는 SDK 라이브러리로써, `fake-iamport-server` 의 소스코드를 토대로 [Nestia](https://github.com/samchon/nestia) 를 이용하여 빌드하였다. 그리고 이를 통하여 가짜 아임포트 서버 뿐 아니라, 진짜 아임포트 서버, 양쪽 모두와 연동할 수 있다. + +참고로 [Nestia](https://github.com/samchon/nestia) 는 SDK 뿐 아니라 Swagger 또한 빌드할 수 있는데, 이 또한 `fake-iamport-server` 의 소스코드 및 DTO 를 컴파일러 수준에서 분석하여 만들어지는 것인지라, 그 퀄리티가 상당하다. 실제로 아임포트가 공식 제공하는 Swagger 보다 퀄리티나 문서화 수준이 더 높다. + + - 서버 주소: http://localhost:10851 + - 매뉴얼 + - **Swagger Editor**: [packages/api/swagger.json](https://editor.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsamchon%2Ffake-iamport-server%2Fmaster%2Fpackages%2Fapi%2Fswagger.json) + - 자료 구조: [src/api/structures/IIamportPayment.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/api/structures/IIamportPayment.ts) + - API 함수: [src/api/functional/payments/index.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/api/functional/payments/index.ts) + - 예제 코드 + - 본인 인증: [test_fake_certification.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_certification.ts) + - 가상 계좌 결제: [test_fake_vbank_payment.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_vbank_payment.ts) + - 카드 결제: [test_fake_card_payment.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_card_payment.ts) + - 간편 결제 등록 + - [test_fake_subscription_payment_again.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_subscription_payment_again.ts) + - [test_fake_subscription_payment_onetime.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_subscription_payment_onetime.ts) + - 전체 환불: [test_fake_card_payment_cancel.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_card_payment_cancel.ts) + - 부분 환불: [test_fake_card_payment_cancel_partial.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_card_payment_cancel_partial.ts) + - 현금 영수증 발행하기: [test_fake_receipt.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_receipt.ts) + - 연관 저장소 + - [samchon/typia](https://github.com/samchon/typia) - Superfast runtime validator + - [samchon/nestia](https://github.com/samchon/nestia) - SDK generator for the NestJS + - [samchon/fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server): 가짜 토스 페이먼츠 서버 + +```typescript +import { v4 } from "uuid"; + +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; + +export async function test_fake_card_payment(): Promise +{ + // 커넥터 정보 구성, 토큰 만료시 자동으로 갱신해 줌 + const connector: imp.IamportConnector = new imp.IamportConnector + ( + "http://127.0.0.1:10851", + { + imp_key: "test_imp_key", + imp_secret: "test_imp_secret" + } + ); + + // 카드로 결제하기 + const output: IIamportResponse = + await imp.functional.subscribe.payments.onetime + ( + await connector.get(), + { + card_number: "1111-2222-3333-4444", + expiry: "2028-12", + birth: "880311", + + merchant_uid: v4(), + amount: 25_000, + name: "Fake 주문" + } + ); + + // 결제 내역 조회하기 + const reloaded: IIamportResponse = + await imp.functional.payments.at + ( + await connector.get(), + output.response.imp_uid, + {}, + ); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportPayment = reloaded.response; + if (payment.pay_method !== "card") + throw new Error("Bug on payments.at(): its pay_method must be card."); + else if (!payment.paid_at || payment.status !== "paid") + throw new Error("Bug on payments.at(): its status must be paid."); + + // 첫 번째 if condition 에 의해 자동 다운 캐스팅 된 상태 + payment.card_number; + return payment; +} +``` + + + + +## 2. Installation +### 2.1. NodeJS +백엔드 서버 프로그램은 TypeScript 로 만들어졌으며, NodeJS 에서 구동된다. + +고로 제일 먼저 할 일은, NodeJS 를 설치하는 것이다. 아래 링크를 열어, NodeJS 프로그램을 다운로드 받은 후 즉각 설치하기 바란다. 참고로 NodeJS 버전은 어지간히 낮은 옛 시대의 버전만 아니면 되니, 구태여 Latest 버전을 설치할 필요는 없으며, Stable 버전만으로도 충분하다. + + - https://nodejs.org/en/ + +### 2.2. Server +NodeJS 의 설치가 끝났다면, 바로 (가짜) 아임포트 서버 구동을 시작할 수 있다. + +제일 먼저 `git clone` 을 통하여, 백엔드 프로젝트를 로컬 저장소에 복사하도록 한다. 그리고 해당 폴더로 이동하여 `npm install` 명령어를 실행함으로써, 백엔드 서버를 구동하는 데 필요한 라이브러리들을 다운로드 한다. 그리고 `npm run build` 명령어를 입력하여, 백엔드 서버의 소스 코드를 컴파일한다. + +마지막으로 `npm run start` 명령어를 실행해주면, (가짜) 아임포트 서버가 구동된다. 이 가짜 서버를 통하여, 귀하가 개발하는 백엔드 서버가 결제 연동에 관련하여 제대로 구현되었는 지 충분히 검증한 후, 실 서버를 배포할 때 연동 대상 서버를 현재의 가짜 서버에서 진짜 서버로 바꾸어주도록 하자. 구동 중인 가짜 아임포트 서버를 중단하고 싶다면, `npm run stop` 명령어를 실행해주면 된다. + +참고로 가짜 아임포트 서버가 사용하는 포트 번호나, 가짜 아임포트 서버가 이벤트를 전달해주는 Webhook URL 등은 모두 [src/FakeIamportConfiguration.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/FakeIamportConfiguration.ts) 에 정의되어있으니, 이를 알맞게 수정한 후 컴파일 및 가짜 서버 재 가동을 해 주면 된다. + +```bash +# CLONE REPOSITORY +git clone https://github.com/samchon/fake-iamport-server +cd fake-iamport-server + +# INSTALLATION & COMPILATION +npm install +npm run build + +# START SERVER & STOP SERVER +npm run start +npm run stop +``` + +[![npm version](https://badge.fury.io/js/fake-iamport-server.svg)](https://www.npmjs.com/package/fake-iamport-server) +[![Downloads](https://img.shields.io/npm/dm/fake-iamport-server.svg)](https://www.npmjs.com/package/fake-iamport-server) + +더하여 `fake-iamport-server` 는 npm 모듈로 설치하여 구동할 수도 있다. + +귀하의 백엔드 서버 테스트 자동화 프로그램이 `fake-iamport-server` 의 설정과 개설 및 폐쇄를 모두 통제하고자 할 때는, github 저장소를 clone 하는 것보다, 이처럼 npm 모듈을 설치하여 import 하는 것이 훨씬 더 알맞다. + +따라서 귀하의 백엔드 서버가 TypeScript 내지 JavaScript 를 사용한다면, github 저장소를 clone 하여 `fake-iamport-server` 를 별도 구동하기보다, 귀하의 백엔드 서버에서 `fake-iamport-server` 의 개설과 폐쇄를 직접 통제하기를 권장한다. + +```typescript +// npm install --save-dev fake-iamport-server +import FakeIamport from "fake-iamport-server"; + +async function main(): Promise +{ + FakeIamport.FakeIamportConfiguration.WEBHOOK_URL = "your-backend-webhook-api-url"; + FakeIamport.FakeIamportConfiguration.authorize = accessor => + { + return accessor.imp_key === "test_imp_key" + && accessor.imp_secret === "test_imp_secret"; + }; + + const fake: FakeIamport.FakeIamportBackend = new FakeIamport.FakeIamportBackend(); + await fake.open(); + await fake.close(); +} +``` + +### 2.3. SDK +[![npm version](https://badge.fury.io/js/iamport-server-api.svg)](https://www.npmjs.com/package/iamport-server-api) +[![Downloads](https://img.shields.io/npm/dm/iamport-server-api.svg)](https://www.npmjs.com/package/iamport-server-api) + +본 백엔드 프로젝트 `fake-iamport-server` 는 비록 아임포트의 API 를 흉내내어 만든 가짜이지만, 이것을 통하여 만들어지는 SDK (Software Development Kit) 만큼은 진짜이다. 이 SDK 를 이용하면, `fake-iamport-server` 뿐 아니라 진짜 아임포트 서버와도 원활히 연동할 수 있기 때문이다. + +고로 아임포트와 연동하는 TypeScript 기반 백엔드 서버를 개발함에 있어, 가짜 아임포트 서버 `fake-iamport-server` 는 직접 이용치 않더라도, 실제 아임포트 서버와의 연동을 위하여, SDK 라이브러리만큼은 반드시 설치하기를 권장하는 바이다. + +```bash +npm install --save fake-iamport-server-api +``` + +먼저 위 명령어를 통하여, 귀하의 TypeScript 기반 백엔드 서버에, 아임포트 서버와의 연동을 위한 SDK 라이브러리를 설치한다. 그리고 가짜 아임포트 서버 `fake-iamport-server` 를 구동하여, 이 것과의 결제 연동이 제대로 이루어지는 지 충분할 만큼의 검증을 한다. 테스트 자동화 프로그램을 제작, 이 안정성이 상시 검증될 수 있다면 더더욱 좋다. + +마지막으로 실 서버를 배포하며, 연동 대상 서버를 가짜에서 진짜로 교체해주면 된다. + +참고로 [Nestia](https://github.com/samchon/nestia) 는 SDK 뿐 아니라 Swagger 또한 빌드할 수 있는데, 이 또한 `fake-iamport-server` 의 소스코드 및 DTO 를 컴파일러 수준에서 분석하여 만들어지는 것인지라, 그 퀄리티가 상당하다. 실제로 아임포트가 공식 제공하는 Swagger 보다 퀄리티나 문서화 수준이 더 높다. + + - 서버 주소: http://localhost:10851 + - 매뉴얼 + - **Swagger Editor**: [packages/api/swagger.json](https://editor.swagger.io/?url=https%3A%2F%2Fraw.githubusercontent.com%2Fsamchon%2Ffake-iamport-server%2Fmaster%2Fpackages%2Fapi%2Fswagger.json) + - 자료 구조: [src/api/structures/IIamportPayment.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/api/structures/IIamportPayment.ts) + - API 함수: [src/api/functional/payments/index.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/api/functional/payments/index.ts) + - 예제 코드 + - 본인 인증: [test_fake_certification.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_certification.ts) + - 가상 계좌 결제: [test_fake_vbank_payment.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_vbank_payment.ts) + - 카드 결제: [test_fake_card_payment.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_card_payment.ts) + - 간편 결제 등록 + - [test_fake_subscription_payment_again.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_subscription_payment_again.ts) + - [test_fake_subscription_payment_onetime.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_subscription_payment_onetime.ts) + - 전체 환불: [test_fake_card_payment_cancel.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_card_payment_cancel.ts) + - 부분 환불: [test_fake_card_payment_cancel_partial.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_card_payment_cancel_partial.ts) + - 현금 영수증 발행하기: [test_fake_receipt.ts](https://github.com/samchon/fake-iamport-server/blob/master/test/features/test_fake_receipt.ts) + - 연관 저장소 + - [samchon/netia](https://github.com/samchon/nestia) - Automatic SDK generator for the NestJS + - [samchon/fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server): 가짜 토스 페이먼츠 서버 + +```typescript +export async function test_fake_subscription_payment_again(): Promise +{ + // 고객 (간편 결제로 등록할 카드) 의 식별자 키 + const customer_uid: string = v4(); + + // 커넥터 정보 구성, 토큰 만료시 자동으로 갱신해 줌 + const connector: imp.IamportConnector = new imp.IamportConnector + ( + "http://127.0.0.1:10851", + { + imp_key: "test_imp_key", + imp_secret: "test_imp_secret" + } + ); + + // 간편 결제 카드 등록하기 + await imp.functional.subscribe.customers.store + ( + await connector.get(), + customer_uid, + { + customer_uid, + card_number: "1111-2222-3333-4444", + expiry: "2028-12", + birth: "880311", + } + ); + + // 간편 결제 카드로 결제하기 + const output: IIamportResponse = + await imp.functional.subscribe.payments.again + ( + await connector.get(), + { + customer_uid, + merchant_uid: v4(), + amount: 10_000, + name: "Fake 주문", + } + ); + + // 결제 내역 조회하기 + const reloaded: IIamportResponse = + await imp.functional.payments.at + ( + await connector.get(), + output.response.imp_uid, + {}, + ); + + // 결제 방식 및 완료 여부 확인 + const payment: IIamportPayment = reloaded.response; + if (payment.pay_method !== "card") + throw new Error("Bug on payments.at(): its pay_method must be card."); + else if (!payment.paid_at || payment.status !== "paid") + throw new Error("Bug on payments.at(): its status must be paid."); + + // 첫 번째 if condition 에 의해 자동 다운 캐스팅 된 상태 + payment.card_number; + return payment; +} +``` + + + + +## 3. Development +### 3.1. API Interface Definition +백엔드 서버에 새 API 를 추가하고 기능을 변경하는 일 따위는 물론, API 컨트롤러, 즉 [src/controllers](https://github.com/samchon/fake-iamport-server/blob/master/src/controllers) 의 코드를 수정함으로써 이루어진다. 하지만 `fake-iamport-server` 는 신규 API 가 필요하거나 혹은 기존 API 의 변경 필요할 때, 대뜸 [Main Program](#33-main-program) 의 코드부터 작성하고 보는 것을 매우 지양한다. 그 대신 `fake-iamport-server` 는 API 의 인터페이스만을 먼저 정의하고, [Main Program](#33-main-program) 의 구현은 나중으로 미루는 것을 지향한다. + +따라서 `fake-iamport-server` 에 새 API 를 추가하려거든, [src/controllers](https://github.com/samchon/fake-iamport-server/blob/master/src/controllers) 에 새 API 의 인터페이스만을 먼저 정의해준다. 곧이어 `npm run sdk` 나 `npm run api` 명령어를 통하여, API Library 를 빌드한다. 경우에 따라서는 프론트 프로젝트와의 동시 개발을 위하여, 새로이 빌드된 SDK 를 그대로 `npm publish` 해 버려도 좋다. + +이후 로컬에서 새로이 생성된 SDK 와 해당 API 를 이용, 유즈케이스 시나리오를 테스트 자동화 프로그램으로 작성한다. 그리고 Main Program 을 제작하며, 앞서 작성해 둔 테스트 자동화 프로그램으로 상시 검증한다. 마지막으로 Main Program 까지 완성되면 이를 배포하면 된다. + +이하 `fake-iamport-server` 의 개략적인 개발 순서를 요약하면 아래와 같다. + + - API Interface Definition + - API Library (SDK) 빌드 + - Test Automation Program 제작 + - Main Program 제작 및 테스트 자동화 프로그램을 이용한 상시 검증 + - DEV 및 REAL 서버에 배포 + +### 3.2. Test Automation Program +```bash +npm run test +``` + +새로이 개발할 [API 인터페이스 정의](#31-api-interface-definition)를 마쳤다면, 그 다음에 할 일은 바로 해당 API 에 대한 유즈케이스 시나리오를 세우고 이를 테스트 자동화 프로그램을 만들어, 향후 [Main Program](#33-main-program) 제작시 이를 상시 검증할 수 있는 수단을 구비해두는 것이다 - TDD (Test Driven Development). + +그리고 본 프로젝트는 `npm run test` 라는 명령어를 통하여, 서버 프로그램의 일체 기능 및 정책 등에 대하여 검증할 수 있는, 테스트 자동화 프로그램을 구동해 볼 수 있다. 만약 새로운 테스트 로직을 추가하고 싶다면, [test/features](https://github.com/samchon/fake-iamport-server/blob/master/test/features) 폴더의 적당한 위치에 새 `ts` 파일을 하나 만들고, `test_` 로 시작하는 함수를 하나 만들어 그 안에 테스트 로직을 작성한 후, 이를 `export` 심벌을 이용하여 배출해주면 된다. 이에 대한 자세한 내용은 [test/features](https://github.com/samchon/fake-iamport-server/blob/master/test/features) 폴더에 들어있는 모든 `ts` 파일 하나 하나가 다 좋은 예제 격이니, 이를 참고하도록 한다. + +참고로 `npm run test` 명령어를 실행할 때마다, [test/features](https://github.com/samchon/fake-iamport-server/blob/master/test/features) 폴더 내에 등록된 모든 프로그램을 실행하게 된다. 하지만 이런 식의 *entire level test* 가 매번 필요한 것은 아닐 것이다. 새로 개발한 기능이 극히 일부 요소에 국한되어 부분 테스트가 필요하다면, 아래 옵션값을 참조, `--include` 나 `--exclude` 태그를 사용하여 시간을 절약하도록 하자. + + - options + - `include`: 특정 단어가 포함된 테스트 함수만 실행 + - `exclude`: 특정 단어가 포함된 테스트 함수 제외 + +### 3.3. Main Program +[API 인터페이스를 정의](#31-api-interface-definition)하고 그에 관련된 [테스트 자동화 프로그램](#32-test-automation-program)을 제작하였다면, 마지막으로 남은 일은 바로 서버의 메인 프로그램을 작성, 해당 API 를 완성하는 것이다. 앞서 정의한 [API 인터페이스](#31-api-interface-definition) 메서드 내에, 상세 구현 코드를 작성하고, 이를 [테스트 자동화 프로그램](#32-test-automation-program)을 통하여 상시 검증하도록 하자. + +단, 모든 소스 코드를 전부 API 컨트롤러의 메서드에 작성하는 우는 범하지 않기를 바란다. API 컨트롤러는 단지 매개체 + a 의 역할만을 해야 할 뿐이며, 주 소스 코드는 [src](src) 폴더 내 각 폴더의 분류에 따라 알맞게 나뉘어 작성되어야 한다. + + + + +## 4. Appendix +### 4.1. Expiration +`fake-iamport-server` 는 결제 데이터를 메모리에 임시 기록한다. + +왜냐하면 `fake-iamport-server` 는 아임포트 서버의 API 를 흉내내어 만든 가짜 서버로써, 개발 단계에서 쓰이는 임시 시스템에 불과하기 때문이다. 따라서 `fake-iamport-server` 에 생성된 결제 내지 카드 정보들은 모두 테스트 용도로 생성된 임시 레코드가 불과하기에, 구태여 이를 DB 나 로컬 디스크에 저장하여 영구 보존할 이유가 없다. + +이에 `fake-iamport-server` 는 결제 데이터를 메모리에 임시로 기록하며, 한 편으로 그 수량 및 보존 기한에 한도를 두어, 쉬이 메모리 부족 현상이 일어나지 않도록 하고 있다. 이러한 임시 데이터 만료 정보는 [src/FakeIamportConfiguration.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/FakeIamportConfiguration.ts) 파일의 `EXPIRATION` 변수에 정의되어있으며, 결제 및 간편 카드 결제 등록 데이터는 모두 [src/providers/FakeIamportStorage.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/providers/FakeIamportStorage.ts) 에서 관리된다. + + - 임시 데이터 만료 정보: [src/FakeIamportConfiguration.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/FakeIamportConfiguration.ts) + - 임시 데이터 저장소: [src/providers/FakeIamportStorage.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/providers/FakeIamportStorage.ts) + - 임시 데이터 컨테이너: [src/utils/VolatileMap.ts](https://github.com/samchon/fake-iamport-server/blob/master/src/utils/VolatileMap.ts) + +> 혹여 `fake-iamport-server` 를 사용하는 백엔드 시스템이 제법 크고 그 네트워크 환경 구성이 매우 복잡하여, `fake-iamport-server` 를 독립 서버로 배포하고, 가상의 결제 레코드 또한 DB 에 저장해야 하며, 무중단 배포 시스템 또한 필요하지 않을까? +> +> 설마 위와 같은 경우가 있어봐야 얼마나 있겠나 싶어 공개 저장소에 올려두지는 않았으나, `fake-iamport-server` 가 결제 데이터를 [VolatileMap](https://github.com/samchon/fake-iamport-server/blob/master/src/utils/VolatileMap.ts) 이 아닌 DB 에 저장하고, [폐쇄망에서조차 동작할 수 있는 무중단 업데이트 및 배포 시스템](https://github.com/samchon/fake-iamport-server#41-non-distruptive-update-system)을 따로 구비해 둔 것이 있다. +> +> 따라서 위와 같은 형태의 `fake-iamport-server` 가 필요하다면, 얼마든지 연락하기 바란다. 즉시 위 요소를 충당하는 솔루션을 공급해 줄 수 있으며, 만일 이러한 요청이 제법 많은 경우, 별도의 브랜치를 만들어 배포해 볼 요량도 있다. +> +>> ```bash +>> # WHEN STARTING THE MASTE SERVER +>> npm run start:updator:master +>> npm run start +>> +>> # WHEN STARTING A SLAVE SERVER +>> npm run start:updator:slave +>> npm run start +>> +>> # WHEN RUN UPDATE COMMAND IN THE CLIENT SIDE +>> npm run update +>> ``` + +### 4.2. Typia +![Typia Logo](https://typia.io/logo.png) + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/typia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/typia.svg)](https://www.npmjs.com/package/typia) +[![Downloads](https://img.shields.io/npm/dm/typia.svg)](https://www.npmjs.com/package/typia) +[![Build Status](https://github.com/samchon/typia/workflows/build/badge.svg)](https://github.com/samchon/typia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://typia.io/docs/) + +```typescript +// RUNTIME VALIDATORS +export function is(input: unknown): input is T; // returns boolean +export function assert(input: unknown): T; // throws TypeGuardError +export function validate(input: unknown): IValidation; // detailed +export const customValidators: CustomValidatorMap; // can add custom validators + +// JSON +export function application(): IJsonApplication; // JSON schema +export function assertParse(input: string): T; // type safe parser +export function assertStringify(input: T): string; // safe and faster + // +) isParse, validateParse + // +) stringify, isStringify, validateStringify + +// MISC +export function random(g?: Partial): Primitive; +``` + +Typia is a transformer library supporting below features: + + - Super-fast Runtime Validators + - Safe JSON parse and fast stringify functions + - JSON schema generator + - Random data generator + +> **Note** +> +> - **Only one line** required, with pure TypeScript type +> - Runtime validator is **20,000x faster** than `class-validator` +> - JSON serialization is **200x faster** than `class-transformer` + +### 4.3. Nestia +![Nestia Logo](https://nestia.io/logo.png) + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) +[![Downloads](https://img.shields.io/npm/dm/nestia.svg)](https://www.npmjs.com/package/nestia) +[![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) + +Nestia is a set of helper libraries for NestJS, supporting below features: + + - `@nestia/core`: super-fast decorators + - `@nestia/sdk`: + - SDK generator for clients + - Swagger generator evolved than ever + - Automatic E2E test functions generator + - Mockup Simulator for client applications + - `@nestia/migrate`: Swagger to NestJS + - `nestia`: just CLI (command line interface) tool + +> **Note** +> +> - **Only one line** required, with pure TypeScript type +> - Enhance performance **30x** up +> - Runtime validator is **20,000x faster** than `class-validator` +> - JSON serialization is **200x faster** than `class-transformer` +> - Software Development Kit +> - SDK is similar with [tRPC](https://trpc.io), but much advanced +> - Mockup simulator like [msw](https://mswjs.io/), but fully automated \ No newline at end of file diff --git a/packages/fake-iamport-server/nestia.config.ts b/packages/fake-iamport-server/nestia.config.ts new file mode 100644 index 0000000..56912db --- /dev/null +++ b/packages/fake-iamport-server/nestia.config.ts @@ -0,0 +1,34 @@ +import type { INestiaConfig } from "@nestia/sdk"; + +const NESTIA_CONFIG: INestiaConfig = { + input: "src/controllers", + output: "src/api", + simulate: true, + distribute: "../iamport-server-api", + swagger: { + output: "../iamport-server-api/swagger.json", + info: { + title: "Iamport API", + description: + "Built by [fake-iamport-server](https://github.com/samchon/fake-iamport-server) with [nestia](https://github.com/samchon/nestia)", + }, + servers: [ + { + url: "http://localhost:10851", + description: "fake", + }, + { + url: "https://api.iamport.kr", + description: "real", + }, + ], + security: { + bearer: { + type: "apiKey", + name: "Authorization", + in: "header", + }, + }, + }, +}; +export default NESTIA_CONFIG; diff --git a/packages/fake-iamport-server/package.json b/packages/fake-iamport-server/package.json new file mode 100644 index 0000000..091f67d --- /dev/null +++ b/packages/fake-iamport-server/package.json @@ -0,0 +1,87 @@ +{ + "name": "fake-iamport-server", + "version": "4.0.0-dev.20230920", + "description": "Fake iamport server for testing", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "----------------------------------------------": "", + "build": "npm run build:sdk && npm run build:main && npm run build:test", + "build:api": "rimraf packages/api/lib && nestia sdk && npx copyfiles README.md packages/api && tsc -p packages/api/tsconfig.json", + "build:main": "rimraf lib && tsc", + "build:sdk": "rimraf src/api/functional && nestia sdk", + "build:swagger": "nestia swagger", + "build:test": "rimraf bin && tsc -p test/tsconfig.json", + "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", + "package:next": "npm run package:latest -- --tag next", + "prepare": "ts-patch install", + "-------------------------------------------------": "", + "start": "pm2 start lib/executable/server.js -i 1 --name fake-iamport-server --wait-ready --listen-timeout 120000 --kill-timeout 15000", + "start:reload": "pm2 reload fake-iamport-server", + "stop": "pm2 delete fake-iamport-server", + "--------------------------------------------------": "", + "test": "node bin/test" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/fake-iamport-server" + }, + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/fake-iamport-server/issues" + }, + "homepage": "https://github.com/samchon/fake-iamport-server", + "devDependencies": { + "@nestia/sdk": "^2.0.3", + "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@types/atob": "^2.1.2", + "@types/btoa": "^1.2.3", + "@types/cli": "^0.11.19", + "@types/node": "^15.6.1", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.26.0", + "@typescript-eslint/parser": "^5.26.0", + "cli": "^1.0.1", + "copyfiles": "^2.4.1", + "nestia": "^4.5.0", + "pm2": "^4.5.6", + "prettier": "^2.6.2", + "rimraf": "^3.0.2", + "sloc": "^0.2.1", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", + "typescript": "^5.2.2" + }, + "dependencies": { + "@nestia/core": "^2.0.3", + "@nestia/e2e": "^0.3.6", + "@nestia/fetcher": "^2.0.3", + "source-map-support": "^0.5.19", + "tstl": "^2.5.13", + "typescript-transform-paths": "^3.4.6", + "typia": "^5.0.4", + "uuid": "^9.0.0" + }, + "keywords": [ + "toss", + "payments", + "server", + "fake", + "test", + "mock" + ], + "files": [ + "package.json", + "README.md", + "LICENSE", + "lib", + "src" + ] +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/FakeIamportBackend.ts b/packages/fake-iamport-server/src/FakeIamportBackend.ts new file mode 100644 index 0000000..74f773c --- /dev/null +++ b/packages/fake-iamport-server/src/FakeIamportBackend.ts @@ -0,0 +1,59 @@ +import core from "@nestia/core"; +import { NestFactory } from "@nestjs/core"; +import { + FastifyAdapter, + NestFastifyApplication, +} from "@nestjs/platform-fastify"; + +import { FakeIamportConfiguration } from "./FakeIamportConfiguration"; + +/** + * Fake 아임포트 서버의 백엔드 프로그램. + * + * @author Samchon + */ +export class FakeIamportBackend { + private application_?: NestFastifyApplication; + + /** + * 서버 개설. + */ + public async open(): Promise { + //---- + // OPEN THE BACKEND SERVER + //---- + // MOUNT CONTROLLERS + this.application_ = await NestFactory.create( + await core.DynamicModule.mount(`${__dirname}/controllers`), + 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 new file mode 100644 index 0000000..1f48681 --- /dev/null +++ b/packages/fake-iamport-server/src/FakeIamportConfiguration.ts @@ -0,0 +1,114 @@ +import core from "@nestia/core"; +import * as nest 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); +if (EXTENSION === "js") require("source-map-support").install(); + +/** + * Fake 아임포트 서버의 설정 정보. + * + * @author Samchon + */ +export namespace FakeIamportConfiguration { + /** + * @internal + */ + export const ASSETS = __dirname + "/../assets"; + + /** + * 유저 토큰의 유효 시간. + */ + export const USER_EXPIRATION_TIME: number = -3 * 60 * 1000; + + /** + * 임시 저장소의 레코드 만료 기한. + */ + export const STORAGE_EXPIRATION: IExpiration = { + time: 3 * 60 * 1000, + capacity: 1000, + }; + + /** + * 서버가 사용할 포트 번호. + */ + 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 { + /** + * API 키. + */ + imp_key: string; + + /** + * Secret 키. + */ + imp_secret: string; + } + + /** + * 임시 저장소의 레코드 만료 기한. + */ + export interface IExpiration { + /** + * 만료 시간. + */ + time: number; + + /** + * 최대 수용량. + */ + capacity: number; + } +} + +// CUSTOM EXCEPTIION CONVERSION +core.ExceptionManager.insert( + OutOfRange, + (exp) => new nest.NotFoundException(exp.message), +); +core.ExceptionManager.insert( + InvalidArgument, + (exp) => new nest.ConflictException(exp.message), +); +core.ExceptionManager.insert( + DomainError, + (exp) => new nest.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, + }), +); diff --git a/src/api/HttpError.ts b/packages/fake-iamport-server/src/api/HttpError.ts similarity index 100% rename from src/api/HttpError.ts rename to packages/fake-iamport-server/src/api/HttpError.ts diff --git a/src/api/IConnection.ts b/packages/fake-iamport-server/src/api/IConnection.ts similarity index 100% rename from src/api/IConnection.ts rename to packages/fake-iamport-server/src/api/IConnection.ts diff --git a/packages/fake-iamport-server/src/api/IamportConnector.ts b/packages/fake-iamport-server/src/api/IamportConnector.ts new file mode 100644 index 0000000..1b45151 --- /dev/null +++ b/packages/fake-iamport-server/src/api/IamportConnector.ts @@ -0,0 +1,84 @@ +import { SharedLock } from "tstl/thread/SharedLock"; +import { SharedMutex } from "tstl/thread/SharedMutex"; +import { UniqueLock } from "tstl/thread/UniqueLock"; + +import { IConnection } from "./IConnection"; +import { users } from "./functional"; +import { IIamportResponse } from "./structures/IIamportResponse"; +import { IIamportUser } from "./structures/IIamportUser"; + +/** + * 아임포트 커넥터. + * + * 아임포트가 발급해주는 유저 인증 토큰에는, 유효 시간 {@link IIamportUser.expired_at} 이 + * 존재하여, 해당 시간을 초과하거든 기 발급 토큰이 만료되어 더 이상 쓸 수 없게 된다. 때문에 + * 아임포트 API 를 호출할 때마다, 늘 토큰의 만료 시간을 신경 써줘야해서 매우 번거롭다. + * + * `IamportConnector` 는 이러한 번거로움을 없애기 위하여, 아임포트의 API 를 호출하기 위하여 + * {@link IConnection} 정보하기 위한 {@link IamportConnector.connect} 메서드를 호출할 때마다, + * 아임포트 유저 인증 토큰의 만료 시간의 도래 여부를 계산하여 자동으로 갱신해주는 클래스이다. + * + * 따라서 아임포트 유저 토큰 고유의 시간 제한에 구애받지 않고, 아임포트의 API 들을 편하게 호출하고 + * 싶다면, 현재 `IamportConnector` 의 적극적인 사용을 권하는 바이다. + * + * @author Samchon + */ +export class IamportConnector { + 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; + } + + /** + * 커넥션 정보 구성하기. + * + * 아임포트 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; + } +} diff --git a/src/api/Primitive.ts b/packages/fake-iamport-server/src/api/Primitive.ts similarity index 100% rename from src/api/Primitive.ts rename to packages/fake-iamport-server/src/api/Primitive.ts diff --git a/packages/fake-iamport-server/src/api/functional/certifications/index.ts b/packages/fake-iamport-server/src/api/functional/certifications/index.ts new file mode 100644 index 0000000..d72d588 --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/certifications/index.ts @@ -0,0 +1,158 @@ +/** + * @packageDocumentation + * @module api.functional.certifications + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportCertification } from "../../structures/IIamportCertification"; +import type { IIamportResponse } from "../../structures/IIamportResponse"; +import { NestiaSimulator } from "../../utils/NestiaSimulator"; + +export * as otp from "./otp"; + +/** + * 본인인증 정보 열람하기. + * + * `certiciations.at` 은 본인인증 정보를 열람할 때 사용하는 API 함수이다. + * + * 다만 이 API 함수를 통하여 열람한 본인인증 정보 {@link IIamportCertification } 이 + * 곧 OTP 인증까지 마쳐 본인인증을 모두 마친 레코드라는 보장은 없다. 본인인증의 완결 + * 여부는 오직, {@link IIamportCertification.certified } 값을 직접 검사해봐야만 알 + * 수 있기 때문이다. + * + * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} + * @returns 본인인증 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportCertificationsController.at + * @path GET /certifications/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function at( + connection: IConnection, + imp_uid: string, +): Promise { + return !!connection.simulate + ? at.simulate( + connection, + imp_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...at.METADATA, + path: at.path(imp_uid), + } as const, + ); +} +export namespace at { + export type Output = Primitive>; + + export const METADATA = { + method: "GET", + path: "/certifications/:imp_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/certifications/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 본인인증 정보 삭제하기. + * + * @param imp_uid 대상 본인인증 정보의 {@link IIamportCertification.imp_uid} + * @returns 삭제된 본인인증 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportCertificationsController.erase + * @path DELETE /certifications/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function erase( + connection: IConnection, + imp_uid: string, +): Promise { + return !!connection.simulate + ? erase.simulate( + connection, + imp_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...erase.METADATA, + path: erase.path(imp_uid), + } as const, + ); +} +export namespace erase { + export type Output = Primitive>; + + export const METADATA = { + method: "DELETE", + path: "/certifications/:imp_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/certifications/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/certifications/otp/index.ts b/packages/fake-iamport-server/src/api/functional/certifications/otp/index.ts new file mode 100644 index 0000000..77e1a6b --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/certifications/otp/index.ts @@ -0,0 +1,195 @@ +/** + * @packageDocumentation + * @module api.functional.certifications.otp + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportCertification } from "../../../structures/IIamportCertification"; +import type { IIamportResponse } from "../../../structures/IIamportResponse"; +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * 본인인증 요청하기. + * + * `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 + * + * @controller FakeIamportCertificationsController.request + * @path POST /certifications/otp/request + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function request( + connection: IConnection, + input: request.Input, +): Promise { + return !!connection.simulate + ? request.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...request.METADATA, + path: request.path(), + } as const, + input, + ); +} +export namespace request { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/certifications/otp/request", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/certifications/otp/request`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: request.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 본인인증 시 발급된 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 + * + * @controller FakeIamportCertificationsController.confirm + * @path POST /certifications/otp/confirm/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function confirm( + connection: IConnection, + imp_uid: string, + input: confirm.Input, +): Promise { + return !!connection.simulate + ? confirm.simulate( + connection, + imp_uid, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...confirm.METADATA, + path: confirm.path(imp_uid), + } as const, + input, + ); +} +export namespace confirm { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/certifications/otp/confirm/:imp_uid", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/certifications/otp/confirm/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + input: confirm.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/index.ts b/packages/fake-iamport-server/src/api/functional/index.ts new file mode 100644 index 0000000..a7a4411 --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/index.ts @@ -0,0 +1,13 @@ +/** + * @packageDocumentation + * @module api.functional + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +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 diff --git a/packages/fake-iamport-server/src/api/functional/internal/index.ts b/packages/fake-iamport-server/src/api/functional/internal/index.ts new file mode 100644 index 0000000..e0567a6 --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/internal/index.ts @@ -0,0 +1,155 @@ +/** + * @packageDocumentation + * @module api.functional.internal + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportPayment } from "../../structures/IIamportPayment"; +import { NestiaSimulator } from "../../utils/NestiaSimulator"; + +/** + * 웹훅 이벤트 더미 리스너. + * + * `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 + * + * @controller FakeIamportInternalController.webhook + * @path POST /internal/webhook + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function webhook( + connection: IConnection, + input: webhook.Input, +): Promise { + return !!connection.simulate + ? webhook.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...webhook.METADATA, + path: webhook.path(), + } as const, + input, + ); +} +export namespace webhook { + export type Input = Primitive; + + export const METADATA = { + method: "POST", + path: "/internal/webhook", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/internal/webhook`; + } + export const simulate = async ( + connection: IConnection, + input: webhook.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + } +} + +/** + * 가상 계좌에 입금하기. + * + * `internal.deposit` 은 실제 아임포트 결제 서버에는 존재하지 않는 API 로써, 가상 계좌 + * 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 상황을 시뮬레이션 할 수 있는 + * 함수이다. + * + * 즉, `internal.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 입금을 하고, 그에 따라 + * 아임포트 서버에서 webhook 이벤트가 발생, 이를 귀하의 백엔드 서버로 전송하는 일련의 상황을 + * 시뮬레이션하기 위하여 설계된 테스트 함수다. + * + * @param imp_uid 대상 결제의 {@link IIamportVBankPayment.imp_uid } + * @security bearer + * @author Samchon + * + * @controller FakeIamportInternalController.deposit + * @path GET /internal/deposit/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function deposit( + connection: IConnection, + imp_uid: string, +): Promise { + return !!connection.simulate + ? deposit.simulate( + connection, + imp_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...deposit.METADATA, + path: deposit.path(imp_uid), + } as const, + ); +} +export namespace deposit { + + export const METADATA = { + method: "GET", + path: "/internal/deposit/:imp_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/internal/deposit/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const simulate = async ( + connection: IConnection, + imp_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/payments/index.ts b/packages/fake-iamport-server/src/api/functional/payments/index.ts new file mode 100644 index 0000000..d98b24d --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/payments/index.ts @@ -0,0 +1,180 @@ +/** + * @packageDocumentation + * @module api.functional.payments + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive, Resolved } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportPayment } from "../../structures/IIamportPayment"; +import type { IIamportPaymentCancel } from "../../structures/IIamportPaymentCancel"; +import type { IIamportResponse } from "../../structures/IIamportResponse"; +import { NestiaSimulator } from "../../utils/NestiaSimulator"; + +/** + * 결제 기록 열람하기. + * + * 아임포트를 통하여 발생한 결제 기록을 열람한다. + * + * @param imp_uid 대상 결제 기록의 {@link IIamportPayment.imp_uid} + * @param query 결제 수단이 페이팔인 경우에 사용 + * @returns 결제 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportPaymentsController.at + * @path GET /payments/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function at( + connection: IConnection, + imp_uid: string, + query: at.Query, +): Promise { + return !!connection.simulate + ? at.simulate( + connection, + imp_uid, + query, + ) + : PlainFetcher.fetch( + connection, + { + ...at.METADATA, + path: at.path(imp_uid, query), + } as const, + ); +} +export namespace at { + export type Query = Resolved; + export type Output = Primitive>; + + export const METADATA = { + method: "GET", + path: "/payments/:imp_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string, query: at.Query): string => { + const variables: Record = query as any; + const search: URLSearchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(variables)) + if (value === undefined) continue; + else if (Array.isArray(value)) + value.forEach((elem) => search.append(key, String(elem))); + else + search.set(key, String(value)); + const encoded: string = search.toString(); + return `/payments/${encodeURIComponent(imp_uid ?? "null")}${encoded.length ? `?${encoded}` : ""}`;; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + query: at.Query, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid, query), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + assert.query(() => typia.assert(query)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 결제 취소하기. + * + * 만약 가상 계좌를 통한 결제였다면, 반드시 환불 계좌 정보를 입력해줘야 한다. + * + * @param input 결제 취소 입력 정보 + * @returns 취소된 결제 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportPaymentsController.cancel + * @path POST /payments/cancel + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function cancel( + connection: IConnection, + input: cancel.Input, +): Promise { + return !!connection.simulate + ? cancel.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...cancel.METADATA, + path: cancel.path(), + } as const, + input, + ); +} +export namespace cancel { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/payments/cancel", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/cancel`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: cancel.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/receipts/index.ts b/packages/fake-iamport-server/src/api/functional/receipts/index.ts new file mode 100644 index 0000000..21d4973 --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/receipts/index.ts @@ -0,0 +1,233 @@ +/** + * @packageDocumentation + * @module api.functional.receipts + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportReceipt } from "../../structures/IIamportReceipt"; +import type { IIamportResponse } from "../../structures/IIamportResponse"; +import { NestiaSimulator } from "../../utils/NestiaSimulator"; + +/** + * 현금 영수증 조회하기. + * + * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} + * @returns 현금 영수증 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportReceiptsController.at + * @path GET /receipts/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function at( + connection: IConnection, + imp_uid: string, +): Promise { + return !!connection.simulate + ? at.simulate( + connection, + imp_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...at.METADATA, + path: at.path(imp_uid), + } as const, + ); +} +export namespace at { + export type Output = Primitive>; + + export const METADATA = { + method: "GET", + path: "/receipts/:imp_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/receipts/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 현금 영수증 발행하기. + * + * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} + * @param input 현금 영수증 입력 정보 + * @returns 현금 영수증 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportReceiptsController.store + * @path POST /receipts/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function store( + connection: IConnection, + imp_uid: string, + input: store.Input, +): Promise { + return !!connection.simulate + ? store.simulate( + connection, + imp_uid, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...store.METADATA, + path: store.path(imp_uid), + } as const, + input, + ); +} +export namespace store { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/receipts/:imp_uid", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/receipts/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + input: store.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 현금 영수증 취소하기. + * + * @param imp_uid 귀속 결제의 {@link IIamportPayment.imp_uid} + * @returns 취소된 현금 영수증 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportReceiptsController.erase + * @path DELETE /receipts/:imp_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function erase( + connection: IConnection, + imp_uid: string, +): Promise { + return !!connection.simulate + ? erase.simulate( + connection, + imp_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...erase.METADATA, + path: erase.path(imp_uid), + } as const, + ); +} +export namespace erase { + export type Output = Primitive>; + + export const METADATA = { + method: "DELETE", + path: "/receipts/:imp_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (imp_uid: string): string => { + return `/receipts/${encodeURIComponent(imp_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + imp_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(imp_uid), + contentType: "application/json", + }); + assert.param("imp_uid")(() => typia.assert(imp_uid)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/subscribe/customers/index.ts b/packages/fake-iamport-server/src/api/functional/subscribe/customers/index.ts new file mode 100644 index 0000000..a5c667d --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/subscribe/customers/index.ts @@ -0,0 +1,248 @@ +/** + * @packageDocumentation + * @module api.functional.subscribe.customers + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportResponse } from "../../../structures/IIamportResponse"; +import type { IIamportSubscription } from "../../../structures/IIamportSubscription"; +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * 간편 결제 카드 정보 조회하기. + * + * `subscribe.customers.at` 은 고객이 {@link store } 나 혹은 아임포트가 제공하는 + * 간편 결제 카드 등록 창을 이용하여 저장한 간편 결제 카드 정보를 조회하는 API + * 함수이다. + * + * @param customer_uid 고객 (간편 결제 카드) 식별자 키 + * @returns 간편 결제 카드 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportSubscribeCustomersController.at + * @path GET /subscribe/customers/:customer_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function at( + connection: IConnection, + customer_uid: string, +): Promise { + return !!connection.simulate + ? at.simulate( + connection, + customer_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...at.METADATA, + path: at.path(customer_uid), + } as const, + ); +} +export namespace at { + export type Output = Primitive>; + + export const METADATA = { + method: "GET", + path: "/subscribe/customers/:customer_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (customer_uid: string): string => { + return `/subscribe/customers/${encodeURIComponent(customer_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + customer_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(customer_uid), + contentType: "application/json", + }); + assert.param("customer_uid")(() => typia.assert(customer_uid)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 간편 결제 카드 등록하기. + * + * `subscribe.customers.stoer` 는 고객이 자신의 카드를 서버에 등록해두고, 매번 결제가 + * 필요할 때마다 카드 정보를 반복 입력하는 일 없이, 간편하게 결제를 진행하고자 할 때 + * 사용하는 API 함수이다. + * + * 참고로 `subscribe.customers.store` 는 클라이언트 어플리케이션이 아임포트가 제공하는 + * 간편 결제 카드 등록 창을 사용하는 경우, 귀하의 백엔드 서버가 이를 실 서비스에서 호출하는 + * 일은 없을 것이다. 다만, 고객이 간편 결제 카드를 등록하는 상황을 시뮬레이션하기 위하여, + * 테스트 자동화 프로그램 수준에서 사용될 수는 있다. + * + * @param customer_uid 고객 (간편 결제 카드) 식별자 키 + * @param input 카드 입력 정보 + * @returns 간편 결제 카드 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportSubscribeCustomersController.store + * @path POST /subscribe/customers/:customer_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function store( + connection: IConnection, + customer_uid: string, + input: store.Input, +): Promise { + return !!connection.simulate + ? store.simulate( + connection, + customer_uid, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...store.METADATA, + path: store.path(customer_uid), + } as const, + input, + ); +} +export namespace store { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/subscribe/customers/:customer_uid", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (customer_uid: string): string => { + return `/subscribe/customers/${encodeURIComponent(customer_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + customer_uid: string, + input: store.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(customer_uid), + contentType: "application/json", + }); + assert.param("customer_uid")(() => typia.assert(customer_uid)); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 간편 결제 카드 삭제하기. + * + * 간편 결제를 위하여 등록한 카드를 제거한다. + * + * @param customer_uid 고객 (간편 결제 카드) 식별자 키 + * @returns 삭제된 간편 결제 카드 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportSubscribeCustomersController.erase + * @path DELETE /subscribe/customers/:customer_uid + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function erase( + connection: IConnection, + customer_uid: string, +): Promise { + return !!connection.simulate + ? erase.simulate( + connection, + customer_uid, + ) + : PlainFetcher.fetch( + connection, + { + ...erase.METADATA, + path: erase.path(customer_uid), + } as const, + ); +} +export namespace erase { + export type Output = Primitive>; + + export const METADATA = { + method: "DELETE", + path: "/subscribe/customers/:customer_uid", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (customer_uid: string): string => { + return `/subscribe/customers/${encodeURIComponent(customer_uid ?? "null")}`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + customer_uid: string, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(customer_uid), + contentType: "application/json", + }); + assert.param("customer_uid")(() => typia.assert(customer_uid)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/subscribe/index.ts b/packages/fake-iamport-server/src/api/functional/subscribe/index.ts new file mode 100644 index 0000000..9803052 --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/subscribe/index.ts @@ -0,0 +1,8 @@ +/** + * @packageDocumentation + * @module api.functional.subscribe + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as customers from "./customers"; +export * as payments from "./payments"; \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/subscribe/payments/index.ts b/packages/fake-iamport-server/src/api/functional/subscribe/payments/index.ts new file mode 100644 index 0000000..3e9fb8d --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/subscribe/payments/index.ts @@ -0,0 +1,198 @@ +/** + * @packageDocumentation + * @module api.functional.subscribe.payments + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportCardPayment } from "../../../structures/IIamportCardPayment"; +import type { IIamportResponse } from "../../../structures/IIamportResponse"; +import type { IIamportSubscription } from "../../../structures/IIamportSubscription"; +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * 카드로 결제하기, 더불어 간편 결제용으로 등록 가능. + * + * `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 + * + * @controller FakeIampotSubscribePaymentsController.onetime + * @path POST /subscribe/payments/onetime + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function onetime( + connection: IConnection, + input: onetime.Input, +): Promise { + return !!connection.simulate + ? onetime.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...onetime.METADATA, + path: onetime.path(), + } as const, + input, + ); +} +export namespace onetime { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/subscribe/payments/onetime", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/subscribe/payments/onetime`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: onetime.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 간편 결제에 등록된 카드로 결제하기. + * + * `subscribe.payments.again` 은 고객이 간편 결제에 등록한 카드로 결제를 진행하고자 할 때 + * 호출하는 API 함수이다. 이는 간편하고 불편하고를 떠나, 본질적으로 카드 결제의 일환이기에, + * 리턴값은 일반적인 카드 결제 때와 동일한 {@link IIamportCardPayment } 이다. + * + * 그리고 `subscribe.payments.again` 은 결제 수단 중 유일하게, 클라이언트 어플리케이션이 + * 아임포트가 제공하는 결체 창을 사용할 수 없어, 오직 귀하의 백엔드 서버가 아임포트의 API + * 함수를 직접 호출해야하는 경우에 해당한다. 따라서 간편 결제에 관하여 아임포트 서버와 + * 연동하는 백엔드 서버 및 프론트 어플리케이션을 개발할 때, 반드시 이 상황에 대한 별도의 + * 설계 및 개발이 필요하니, 이 점을 염두에 두기 바란다. + * + * @param input 미리 등록한 카드를 이용한 결제 신청 정보 + * @returns 카드 결제 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIampotSubscribePaymentsController.again + * @path POST /subscribe/payments/again + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function again( + connection: IConnection, + input: again.Input, +): Promise { + return !!connection.simulate + ? again.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...again.METADATA, + path: again.path(), + } as const, + input, + ); +} +export namespace again { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/subscribe/payments/again", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/subscribe/payments/again`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: again.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/users/index.ts b/packages/fake-iamport-server/src/api/functional/users/index.ts new file mode 100644 index 0000000..864cef1 --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/users/index.ts @@ -0,0 +1,98 @@ +/** + * @packageDocumentation + * @module api.functional.users + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportResponse } from "../../structures/IIamportResponse"; +import type { IIamportUser } from "../../structures/IIamportUser"; +import { NestiaSimulator } from "../../utils/NestiaSimulator"; + +/** + * 유저 인증 토큰 발행하기. + * + * 아임포트에 가입하여 부여받은 API 및 secret 키를 토대로, 유저 인증 토큰을 발행한다. + * + * 단, 아임포트가 발급해주는 유저 인증 토큰에는 유효 시간 {@link IIamportUser.expired_at } + * 이 있어, 해당 시간이 지나거든 기 발급 토큰이 만료되어 더 이상 쓸 수 없게 된다. 때문에 + * 아임포트의 이러한 시간 제한에 구애받지 않고 자유로이 아임포트의 API 를 이용하고 싶다면, + * `iamport-server-api` 에서 제공해주는 {@link IamportConnector } 를 활용하도록 하자. + * + * @param input 아임포트의 API 및 secret 키 정보 + * @returns 유저 인증 토큰 정보 + * @author Samchon + * + * @controller FakeIamportUsersController.getToken + * @path POST /users/getToken + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function getToken( + connection: IConnection, + input: getToken.Input, +): Promise { + return !!connection.simulate + ? getToken.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...getToken.METADATA, + path: getToken.path(), + } as const, + input, + ); +} +export namespace getToken { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/users/getToken", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/users/getToken`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: getToken.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/functional/vbanks/index.ts b/packages/fake-iamport-server/src/api/functional/vbanks/index.ts new file mode 100644 index 0000000..0e81e5f --- /dev/null +++ b/packages/fake-iamport-server/src/api/functional/vbanks/index.ts @@ -0,0 +1,171 @@ +/** + * @packageDocumentation + * @module api.functional.vbanks + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import typia from "typia"; + +import type { IIamportResponse } from "../../structures/IIamportResponse"; +import type { IIamportVBankPayment } from "../../structures/IIamportVBankPayment"; +import { NestiaSimulator } from "../../utils/NestiaSimulator"; + +/** + * 가상 계좌 발급하기. + * + * @param input 가상 계좌 입력 정보 + * @returns 가상 계좌 결제 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportVbanksController.store + * @path POST /vbanks + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function store( + connection: IConnection, + input: store.Input, +): Promise { + return !!connection.simulate + ? store.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...store.METADATA, + path: store.path(), + } as const, + input, + ); +} +export namespace store { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "POST", + path: "/vbanks", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/vbanks`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: store.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 가상 계좌 편집하기. + * + * @param input 가상 계좌 편집 입력 정보 + * @returns 편집된 가상 계좌 결제 정보 + * @security bearer + * @author Samchon + * + * @controller FakeIamportVbanksController.update + * @path PUT /vbanks + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function update( + connection: IConnection, + input: update.Input, +): Promise { + return !!connection.simulate + ? update.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...update.METADATA, + path: update.path(), + } as const, + input, + ); +} +export namespace update { + export type Input = Primitive; + export type Output = Primitive>; + + export const METADATA = { + method: "PUT", + path: "/vbanks", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/vbanks`; + } + export const random = (g?: Partial): Primitive> => + typia.random>>(g); + export const simulate = async ( + connection: IConnection, + input: update.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/fake-iamport-server/src/api/index.ts b/packages/fake-iamport-server/src/api/index.ts new file mode 100644 index 0000000..372a512 --- /dev/null +++ b/packages/fake-iamport-server/src/api/index.ts @@ -0,0 +1,5 @@ +import * as imp from "./module"; + +export * from "./module"; + +export default imp; diff --git a/packages/fake-iamport-server/src/api/module.ts b/packages/fake-iamport-server/src/api/module.ts new file mode 100644 index 0000000..99f77e0 --- /dev/null +++ b/packages/fake-iamport-server/src/api/module.ts @@ -0,0 +1,6 @@ +export type * from "./IConnection"; +export * from "./IamportConnector"; +export type * from "./Primitive"; +export * from "./HttpError"; + +export * as functional from "./functional"; diff --git a/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts new file mode 100644 index 0000000..697c7e9 --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportCardPayment.ts @@ -0,0 +1,36 @@ +import { tags } from "typia"; + +import { IIamportPayment } from "./IIamportPayment"; + +/** + * 카드 결제 정보. + * + * @author Samchon + */ +export interface IIamportCardPayment + extends IIamportPayment.IBase<"card" | "samsung"> { + /** + * 카드 식별자 코드. + */ + card_code: string; + + /** + * 카드 이름. + */ + card_name: string; + + /** + * 카드 번호. + */ + card_number: string & tags.Pattern<"\\d{4}-\\d{4}-\\d{4}-\\d{4}">; + + /** + * 할부 개월 수. + */ + card_quota: number & tags.Type<"uint32">; + + /** + * 카드사 승인번호. + */ + 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 new file mode 100644 index 0000000..95755c2 --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportCertification.ts @@ -0,0 +1,198 @@ +import { tags } from "typia"; + +/** + * 본인 인증 내역. + * + * `IIamportCertification` 은 아임포트의 본인인증 정보를 형상화한 자료구조 인터페이스이다. + * + * 단, `IIamportCertification` 레코드의 존재가 곧 본인인증의 완결을 뜻하는 것은 아니다. + * {@link IIamportCertification.certified} 값이 `true` 여야만이 비로소, 본인인증 + * 대상자가 자신의 핸드폰 번호로 전송된 OTP 를 아임포트의 본인인증 팝업창에 정확히 적어, + * 본인인증을 완료했음을 의미한다. + * + * @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; + } + + /** + * 본인 인증 입력 정보. + */ + 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; + } + + /** + * 본인인증 승인을 위한 입력 정보. + */ + export interface IConfirm { + /** + * SMS 로 전송된 본인인증 번호. + */ + otp: string; + } +} diff --git a/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts new file mode 100644 index 0000000..c0799b9 --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportPayment.ts @@ -0,0 +1,211 @@ +import { tags } from "typia"; + +import { IIamportCardPayment } from "./IIamportCardPayment"; +import { IIamportPaymentCancel } from "./IIamportPaymentCancel"; +import { IIamportTransferPayment } from "./IIamportTransferPayment"; +import { IIamportVBankPayment } from "./IIamportVBankPayment"; + +/** + * 결제 정보. + * + * `IIamportPayment` 는 아임포트의 결제 정보를 형상화한 자료구조이자 유니언 타입의 + * 인터페이스로써, if condition 을 통하여 method 값을 특정하면, 파생 타입이 자동으로 + * 지정된다. + * + * ```typescript + * if (payment.pay_method === "card") + * payment.card_number; // payment be IIamportCardPayment + * ``` + * + * @author Samchon + */ +export type IIamportPayment = + | IIamportCardPayment + | IIamportTransferPayment + | IIamportVBankPayment + | IIamportPayment.IBase< + Exclude< + IIamportPayment.PayMethod, + "card" | "samsung" | "trans" | "vbank" + > + >; + +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 { + /** + * 페이팔의 경우, 이 값을 `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"; +} diff --git a/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts b/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts new file mode 100644 index 0000000..579ad62 --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportPaymentCancel.ts @@ -0,0 +1,82 @@ +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">; +} +export namespace IIamportPaymentCancel { + /** + * 결제 취소 입력 정보. + */ + export interface IStore { + /** + * 결제 정보 {@link IIamportPayment} 의 식별자 키. + */ + imp_uid: string; + + /** + * 주문 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. + */ + merchant_uid: string; + + /** + * 취소 금액, 부분 취소도 가능하다. + * + * 누락시 전액 취소. + */ + amount?: number; + + /** + * 취소 트랜잭션 수행 전, 현재 시점의 취소 가능한 잔액. + * + * API요청자가 기록하고 있는 취소가능 잔액과 아임포트가 기록하고 있는 취소가능 잔액이 + * 일치하는지 사전에 검증하고, 검증에 실패하면 트랜잭션을 수행하지 않는다. + * + * `null` 인 경우에는 검증 프로세스를 생략. + */ + checksum: null | (number & tags.Minimum<0>); + + /** + * 취소 사유. + */ + reason: string; + + /** + * 취소요청금액 중 면세금액. + * + * @default 0 + */ + tax_free?: number; + + /** + * 환불계좌 예금주. + */ + refund_holder?: string; + + /** + * 환불계좌 은행 코드. + */ + refund_bank?: string; + + /** + * 환불계좌 계좌번호. + */ + refund_account?: 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 new file mode 100644 index 0000000..cb4cb2a --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportReceipt.ts @@ -0,0 +1,135 @@ +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; + + /** + * 현금영수증 발생대상 식별정보. + * + * - 국세청현금영수증카드 + * - 휴대폰번호 + * - 주민등록번호 + * - 사업자등록번호 + */ + 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; + } +} diff --git a/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts b/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts new file mode 100644 index 0000000..ae1c39b --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportResponse.ts @@ -0,0 +1,23 @@ +/** + * 아임포트 고유의 응답 데이터. + * + * @author Samchon + */ +export interface IIamportResponse { + /** + * 에러 코드. + * + * 값이 0 이면 오류가 없다는 뜻. + */ + code: number; + + /** + * 성공 또는 오류 메시지. + */ + message: string; + + /** + * 응답 데이터, 사실상 본문. + */ + response: T; +} diff --git a/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts b/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts new file mode 100644 index 0000000..238ea3c --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportSubscription.ts @@ -0,0 +1,169 @@ +import { tags } from "typia"; + +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; +} +export namespace IIamportSubscription { + /** + * {@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">; + } +} diff --git a/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts b/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts new file mode 100644 index 0000000..06a58ed --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportTransferPayment.ts @@ -0,0 +1,19 @@ +import { IIamportPayment } from "./IIamportPayment"; + +/** + * 계좌 이체 결제 정보. + * + * @author Samchon + */ +export interface IIamportTransferPayment + extends IIamportPayment.IBase<"trans"> { + /** + * 은행 식별자 코드. + */ + bank_code: 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 new file mode 100644 index 0000000..f9bf1c4 --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportUser.ts @@ -0,0 +1,54 @@ +/** + * 아임포트 유저 인증 정보. + * + * 아임포트는 고객사에게 API 및 secret 키 정보, {@link IIamportUser.IAccessor} 를 발급해준다. + * + * 하지만 이를 곧장 아임포트의 유저 인증에 사용할 수는 없고, 해당 API 및 secret 키를 토대로 유저 + * 인증 토큰을 발급받아야 하는데, 이 유저 인증 토큰에는 하필이면 만로 시간이라는 게 존재한다. + * `IIamportUser` 는 바로 이러한 유저 인증 토큰 및 그것의 만료 시간을 형상화한 자료구조 + * 인터페이스이다. + * + * 더하여 이처럼 만료 시간이 존재하는 아임포트의 유저 인증 토큰의 특성상, 이것의 만료 시간이 + * 초과되지 않도록 관리하는 것은 매우 힘든 일이다. 이에 `iamport-server-api` 에서는 아임포트 + * 유저 인증 토큰이 만료될 때마다 자동 갱신해주는, {@link IamportConnector} 클래스를 제공한다. + * + * @author Samchon + */ +export interface IIamportUser { + /** + * 토큰 발행 시간. + */ + now: number; + + /** + * 토큰 만료 시간. + * + * 리눅스 타임이 기준이며, 이를 JS 에서 사용하려거든, 아래와 같이 변환해야 한다. + * + * ```typescript + * new Date(user.expired_at * 1_000); + * ``` + */ + expired_at: number; + + /** + * 유저 인증 토큰. + */ + access_token: string; +} +export namespace IIamportUser { + /** + * 아임포트에서 부여해 준 API 및 secret 키. + */ + export interface IAccessor { + /** + * API 키. + */ + imp_key: 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 new file mode 100644 index 0000000..cee0269 --- /dev/null +++ b/packages/fake-iamport-server/src/api/structures/IIamportVBankPayment.ts @@ -0,0 +1,128 @@ +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 { + /** + * 주문 식별자 키. + * + * 아임포트 서버가 아닌, 이를 사용하는 서비스가 자체적으로 발급하고 관리한다. + */ + 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; + } + + /** + * 가상 계좌 결제의 수정 입력 정보. + * + * 아직 입금되지 않은 가상계좌의 입금기한 또는 입금금액을 수정할 수 있다. + * + * 다만, 세틀뱅크 혹은 `fake-iamport-server` 만 가능. + */ + export interface IUpdate { + /** + * 대상 결제 기록의 {@link IIamportPayment.imp_uid}. + */ + imp_uid: string; + + /** + * 수정할 결제 금액. + */ + amount?: number; + + /** + * 수정할 가상계좌 입금 기한. + */ + vbank_due?: number; + } +} diff --git a/src/api/typings/Atomic.ts b/packages/fake-iamport-server/src/api/typings/Atomic.ts similarity index 76% rename from src/api/typings/Atomic.ts rename to packages/fake-iamport-server/src/api/typings/Atomic.ts index 1e492db..61a6dee 100644 --- a/src/api/typings/Atomic.ts +++ b/packages/fake-iamport-server/src/api/typings/Atomic.ts @@ -5,12 +5,10 @@ //================================================================ /** * 객체 정의로부터 원자 멤버들만의 타입을 추려냄. - * + * * @template Instance 대상 객체의 타입 * @author Samchon */ export type Atomic = { - [P in keyof Instance]: Instance[P] extends object - ? never - : Instance[P]; -}; \ No newline at end of file + [P in keyof Instance]: Instance[P] extends object ? never : Instance[P]; +}; diff --git a/src/api/typings/Writable.ts b/packages/fake-iamport-server/src/api/typings/Writable.ts similarity index 66% rename from src/api/typings/Writable.ts rename to packages/fake-iamport-server/src/api/typings/Writable.ts index 767ea33..0ed6231 100644 --- a/src/api/typings/Writable.ts +++ b/packages/fake-iamport-server/src/api/typings/Writable.ts @@ -3,12 +3,10 @@ * @module api.typings */ //================================================================ -export type Writable = -{ +export type Writable = { -readonly [P in keyof T]: T[P]; }; -export function Writable(elem: Readonly): Writable -{ +export function Writable(elem: Readonly): Writable { return elem; -} \ No newline at end of file +} diff --git a/src/api/utils/NestiaSimulator.ts b/packages/fake-iamport-server/src/api/utils/NestiaSimulator.ts similarity index 100% rename from src/api/utils/NestiaSimulator.ts rename to packages/fake-iamport-server/src/api/utils/NestiaSimulator.ts diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts new file mode 100644 index 0000000..cfb7b14 --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/FakeIamportCertificationsController.ts @@ -0,0 +1,163 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +import { IIamportCertification } from "iamport-server-api/lib/structures/IIamportCertification"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { randint } from "tstl"; +import { v4 } from "uuid"; + +import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; +import { FakeIamportStorage } from "../providers/FakeIamportStorage"; +import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; + +@nest.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); + } +} diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts new file mode 100644 index 0000000..805c084 --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/FakeIamportInternalController.ts @@ -0,0 +1,72 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; + +import { FakeIamportPaymentProvider } from "../providers/FakeIamportPaymentProvider"; +import { FakeIamportStorage } from "../providers/FakeIamportStorage"; +import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; + +@nest.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.deposit` 은 실제 아임포트 결제 서버에는 존재하지 않는 API 로써, 가상 계좌 + * 결제를 신청한 고객이, 이후 가상 계좌에 목표 금액을 입금하는 상황을 시뮬레이션 할 수 있는 + * 함수이다. + * + * 즉, `internal.deposit` 는 고객이 스스로에게 가상으로 발급된 계좌에 입금을 하고, 그에 따라 + * 아임포트 서버에서 webhook 이벤트가 발생, 이를 귀하의 백엔드 서버로 전송하는 일련의 상황을 + * 시뮬레이션하기 위하여 설계된 테스트 함수다. + * + * @param imp_uid 대상 결제의 {@link IIamportVBankPayment.imp_uid} + * + * @security bearer + * @author Samchon + */ + @core.TypedRoute.Get("deposit/:imp_uid") + public deposit( + @nest.Request() request: fastify.FastifyRequest, + @core.TypedParam("imp_uid") imp_uid: string, + ): void { + // AUTHORIZE + FakeIamportUserAuth.authorize(request); + + // 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; + + // 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 new file mode 100644 index 0000000..b312d0b --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/FakeIamportPaymentsController.ts @@ -0,0 +1,65 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; + +import { IIamportPaymentCancel } from "../api/structures/IIamportPaymentCancel"; +import { FakeIamportPaymentProvider } from "../providers/FakeIamportPaymentProvider"; +import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; +import { FakeIamportStorage } from "../providers/FakeIamportStorage"; +import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; + +@nest.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); + + 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); + } +} diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts new file mode 100644 index 0000000..c2625dc --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/FakeIamportReceiptsController.ts @@ -0,0 +1,111 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +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 { v4 } from "uuid"; + +import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; +import { FakeIamportStorage } from "../providers/FakeIamportStorage"; +import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; + +@nest.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); + + 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( + @nest.Request() request: fastify.FastifyRequest, + @core.TypedParam("imp_uid") imp_uid: string, + @core.TypedBody() input: IIamportReceipt.IStore, + ): IIamportResponse { + FakeIamportUserAuth.authorize(request); + + 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."); + } + + 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; + + 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 nest.UnprocessableEntityException("Already cancelled."); + + payment.cash_receipt_issue = false; + receipt.cancelled_at = Date.now() / 1000; + + return FakeIamportResponseProvider.success(receipt); + } +} diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts new file mode 100644 index 0000000..2612ef8 --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/FakeIamportUsersController.ts @@ -0,0 +1,33 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; + +import { FakeIamportResponseProvider } from "../providers/FakeIamportResponseProvider"; +import { FakeIamportUserAuth } from "../providers/FakeIamportUserAuth"; + +@nest.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); + } +} diff --git a/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts b/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts new file mode 100644 index 0000000..99a7e19 --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/FakeIamportVbanksController.ts @@ -0,0 +1,129 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; +import { randint } from "tstl/algorithm/random"; +import { v4 } from "uuid"; + +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") +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); + + // 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, + + // 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", + + // 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); + + // RETURNS + return FakeIamportResponseProvider.success(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); + + // 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); + } +} diff --git a/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts new file mode 100644 index 0000000..702ad15 --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribeCustomersController.ts @@ -0,0 +1,121 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { v4 } from "uuid"; + +import { FakeIamportResponseProvider } from "../../providers/FakeIamportResponseProvider"; +import { FakeIamportStorage } from "../../providers/FakeIamportStorage"; +import { FakeIamportUserAuth } from "../../providers/FakeIamportUserAuth"; +import { AdvancedRandomGenerator } from "../../utils/AdvancedRandomGenerator"; + +@nest.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); + + // GET SUBSCRIPTION RECORD + const subscription = FakeIamportStorage.subscriptions.get(customer_uid); + + // 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( + @nest.Request() request: fastify.FastifyRequest, + @core.TypedParam("customer_uid") customer_uid: string, + @core.TypedBody() input: IIamportSubscription.IStore, + ): IIamportResponse { + // AUTHORIZE + FakeIamportUserAuth.authorize(request); + + // 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); + + // 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); + } +} diff --git a/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts new file mode 100644 index 0000000..22cf3f8 --- /dev/null +++ b/packages/fake-iamport-server/src/controllers/subscribe/FakeIamportSubscribePaymentsController.ts @@ -0,0 +1,198 @@ +import core from "@nestia/core"; +import * as nest from "@nestjs/common"; +import * as fastify from "fastify"; +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 { v4 } from "uuid"; + +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") +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); + } +} diff --git a/packages/fake-iamport-server/src/executable/server.ts b/packages/fake-iamport-server/src/executable/server.ts new file mode 100644 index 0000000..7112e92 --- /dev/null +++ b/packages/fake-iamport-server/src/executable/server.ts @@ -0,0 +1,59 @@ +import fs from "fs"; +import { randint } from "tstl/algorithm/random"; +import { Singleton } from "tstl/thread/Singleton"; + +import { FakeIamportBackend } from "../FakeIamportBackend"; +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`); +}); + +function cipher(val: number): string { + if (val < 10) return "0" + val; + else return String(val); +} + +async function mkdir(path: string): Promise { + 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 {} +} + +async function main(): Promise { + // 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); +} +main().catch((exp) => { + console.log(exp); + process.exit(-1); +}); diff --git a/packages/fake-iamport-server/src/index.ts b/packages/fake-iamport-server/src/index.ts new file mode 100644 index 0000000..88d9fa4 --- /dev/null +++ b/packages/fake-iamport-server/src/index.ts @@ -0,0 +1,5 @@ +import * as FakeIamport from "./module"; + +export * from "./module"; + +export default FakeIamport; diff --git a/packages/fake-iamport-server/src/module.ts b/packages/fake-iamport-server/src/module.ts new file mode 100644 index 0000000..a3224ca --- /dev/null +++ b/packages/fake-iamport-server/src/module.ts @@ -0,0 +1,2 @@ +export * from "./FakeIamportBackend"; +export * from "./FakeIamportConfiguration"; diff --git a/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts b/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts new file mode 100644 index 0000000..9c46754 --- /dev/null +++ b/packages/fake-iamport-server/src/providers/FakeIamportPaymentProvider.ts @@ -0,0 +1,83 @@ +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportPaymentCancel } from "iamport-server-api/lib/structures/IIamportPaymentCancel"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; +import { DomainError } from "tstl/exception/DomainError"; + +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 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; + + 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, + }); + + // 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); + + 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 new file mode 100644 index 0000000..8bf0fa0 --- /dev/null +++ b/packages/fake-iamport-server/src/providers/FakeIamportResponseProvider.ts @@ -0,0 +1,13 @@ +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; + +export namespace FakeIamportResponseProvider { + 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 new file mode 100644 index 0000000..0a9f4b7 --- /dev/null +++ b/packages/fake-iamport-server/src/providers/FakeIamportStorage.ts @@ -0,0 +1,24 @@ +import { IIamportCertification } from "iamport-server-api/lib/structures/IIamportCertification"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportReceipt } from "iamport-server-api/lib/structures/IIamportReceipt"; +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; + +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); +} diff --git a/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts b/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts new file mode 100644 index 0000000..d6e74c0 --- /dev/null +++ b/packages/fake-iamport-server/src/providers/FakeIamportSubscriptionProvider.ts @@ -0,0 +1,33 @@ +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { v4 } from "uuid"; + +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); + + return subscription; + } +} diff --git a/packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts b/packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts new file mode 100644 index 0000000..7779ca1 --- /dev/null +++ b/packages/fake-iamport-server/src/providers/FakeIamportUserAuth.ts @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..1b25195 --- /dev/null +++ b/packages/fake-iamport-server/src/utils/AdvancedRandomGenerator.ts @@ -0,0 +1,16 @@ +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("-"), +}; diff --git a/src/utils/DateUtil.ts b/packages/fake-iamport-server/src/utils/DateUtil.ts similarity index 100% rename from src/utils/DateUtil.ts rename to packages/fake-iamport-server/src/utils/DateUtil.ts diff --git a/packages/fake-iamport-server/src/utils/ErrorUtil.ts b/packages/fake-iamport-server/src/utils/ErrorUtil.ts new file mode 100644 index 0000000..d187213 --- /dev/null +++ b/packages/fake-iamport-server/src/utils/ErrorUtil.ts @@ -0,0 +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); + } +} diff --git a/src/utils/Terminal.ts b/packages/fake-iamport-server/src/utils/Terminal.ts similarity index 100% rename from src/utils/Terminal.ts rename to packages/fake-iamport-server/src/utils/Terminal.ts diff --git a/packages/fake-iamport-server/src/utils/VolatileMap.ts b/packages/fake-iamport-server/src/utils/VolatileMap.ts new file mode 100644 index 0000000..eb9244a --- /dev/null +++ b/packages/fake-iamport-server/src/utils/VolatileMap.ts @@ -0,0 +1,86 @@ +import { HashMap } from "tstl/container/HashMap"; +import { TreeMap } from "tstl/container/TreeMap"; +import { OutOfRange } from "tstl/exception/OutOfRange"; +import { equal_to } from "tstl/functional/comparators"; +import { hash } from "tstl/functional/hash"; + +export class VolatileMap { + 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 clear(): void { + this.dict_.clear(); + this.timepoints_.clear(); + } + + /* ----------------------------------------------------------- + ACCESSORS + ----------------------------------------------------------- */ + public size(): number { + return this.dict_.size(); + } + + public get(key: Key): T { + return this.dict_.get(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; + } + + /* ----------------------------------------------------------- + ELEMENTS I/O + ----------------------------------------------------------- */ + 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); + } + } + + public erase(key: Key): number { + return this.dict_.erase(key); + } +} +export namespace VolatileMap { + 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 new file mode 100644 index 0000000..5cc7c58 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment.ts @@ -0,0 +1,78 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; +import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; + +export async function test_fake_card_payment( + 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", + + 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); + + // 결제 방식 및 완료 여부 확인 + 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 new file mode 100644 index 0000000..97467fa --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel.ts @@ -0,0 +1,69 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import typia from "typia"; + +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, +): Promise { + // 카드 결제하기 + 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 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); + + // 웹훅 검증 + 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); +} 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 new file mode 100644 index 0000000..4ffcad1 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_over.ts @@ -0,0 +1,39 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +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, +): Promise { + // 카드 결제하기 + 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: "테스트 결제 취소", + }), + ); + + // 결제 내역 재 조회하여 다시 검증 + 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 new file mode 100644 index 0000000..e3c0bd5 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_card_payment_cancel_partial.ts @@ -0,0 +1,60 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; + +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, +): Promise { + // 카드 결제하기 + 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; + + // 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); + }); +} diff --git a/packages/fake-iamport-server/test/features/test_fake_certification.ts b/packages/fake-iamport-server/test/features/test_fake_certification.ts new file mode 100644 index 0000000..cee19e0 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_certification.ts @@ -0,0 +1,61 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCertification } from "iamport-server-api/lib/structures/IIamportCertification"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import typia from "typia"; + +export async function test_fake_certification( + 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); + + /** + * 본인인증 상세 레코드 조회. + * + * 고객의 휴대폰 번호로 본인인증을 위한 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); +} diff --git a/packages/fake-iamport-server/test/features/test_fake_receipt.ts b/packages/fake-iamport-server/test/features/test_fake_receipt.ts new file mode 100644 index 0000000..5c18de2 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_receipt.ts @@ -0,0 +1,38 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +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 typia from "typia"; + +import { test_fake_card_payment } from "./test_fake_card_payment"; + +export async function test_fake_receipt( + 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 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 new file mode 100644 index 0000000..303de9a --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_again.ts @@ -0,0 +1,91 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; +import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; + +export async function test_fake_subscription_payment_again( + connector: imp.IamportConnector, +): Promise { + /** + * 간편 결제 카드 등록을 위한 고객 식별자 키. + * + * 이는 전적으로 아임포트를 사용하는 서비스에서 발급하여 관리하며, + * 아임포트는 이를 고객 식별자 키라고 이름 지었지만, + * 실제 역할은 간편 결제로 등록한 카드 식별자 키로써 역할한다. + */ + 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); + + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@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"); + + /** + * 결제 내역 조회하기. + * + * 위에서 발행한 간편 카드 결제 내역 및 웹훅 이벤트 데이터를 토대로, 아임포트 + * 서버로부터 {@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, + ); + TestValidator.predicate("paid_at")(() => payment.paid_at !== 0); + TestValidator.equals("status")(payment.status)("paid"); + + 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 new file mode 100644 index 0000000..03017ec --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel.ts @@ -0,0 +1,68 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import typia from "typia"; + +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, +): Promise { + // 구독을 통한 결제하기 + 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 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); + + // 웹훅 검증 + 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); +} 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 new file mode 100644 index 0000000..d9bdded --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_over.ts @@ -0,0 +1,38 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +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, +): Promise { + // 카드 결제하기 + 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: "테스트 결제 취소", + }), + ); + + // 결제 내역 재 조회하여 다시 검증 + 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 new file mode 100644 index 0000000..5f847ee --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_cancel_partial.ts @@ -0,0 +1,59 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; + +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, +): Promise { + // 구독을 통한 결제하기 + 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; + + // 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); + }); +} 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 new file mode 100644 index 0000000..bccf653 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_subscription_payment_onetime.ts @@ -0,0 +1,106 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportCardPayment } from "iamport-server-api/lib/structures/IIamportCardPayment"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; +import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; + +export async function test_fake_subscription_payment_onetime( + connector: imp.IamportConnector, +): Promise { + /** + * 간편 결제 카드 등록을 위한 고객 식별자 키. + * + * 이는 전적으로 아임포트를 사용하는 서비스에서 발급하여 관리하며, + * 아임포트는 이를 고객 식별자 키라고 이름 지었지만, + * 실제 역할은 간편 결제로 등록한 카드 식별자 키로써 역할한다. + */ + 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, + + 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); + + /** + * 간편 결제 카드로 결제하기. + * + * 앞서 {@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, +): 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 함수를 호출하여 재 조회해보면, 카드 결제가 + * 무사히 완료되었음을, 그리고 관련 결제 정보 {@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, + ); + TestValidator.predicate("paid_at")(() => payment.paid_at !== 0); + TestValidator.equals("status")(payment.status)("paid"); + + 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 new file mode 100644 index 0000000..dcc5f2f --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment.ts @@ -0,0 +1,127 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { FakeIamportStorage } from "../../src/providers/FakeIamportStorage"; +import { AdvancedRandomGenerator } from "../../src/utils/AdvancedRandomGenerator"; + +export async function test_fake_vbank_payment( + connector: imp.IamportConnector, +): Promise { + // 가상 결제 발행하기 + const ready: IIamportVBankPayment = await issue(connector); + + // 입금 시뮬레이션 + return deposit(connector, ready); +} + +async function issue( + 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"); + + /** + * 아임포트 서버로부터의 웹훅 데이터. + * + * 다만 이 때 보내주는 정보는 최소한의 식별자 및 상태값 정보로써, 해당 결제 건에 + * 대하여 자세히 알고 싶다면, {@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, + ); + TestValidator.equals("imp_uid")(payment.imp_uid)(output.response.imp_uid); + TestValidator.equals("status")(payment.status)("ready"); + + // FOR THE NEXT STEP + return payment; +} + +async function deposit( + connector: imp.IamportConnector, + ready: IIamportVBankPayment, +): Promise { + /** + * 입금 시뮬레이션. + * + * 고객이 자신 앞으로 발급된 가상 계좌에, 결제 금액을 입금함. + * + * 이 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 reloaded: IIamportResponse = + await imp.functional.payments.at( + await connector.get(), + webhook.imp_uid, + {}, + ); + 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 new file mode 100644 index 0000000..f2f9a15 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel.ts @@ -0,0 +1,73 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; +import typia from "typia"; + +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, +): Promise { + // 가상 계좌를 통한 결제하기 + 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 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); + + // 웹훅 검증 + 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); +} 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 new file mode 100644 index 0000000..8e4ebd6 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_over.ts @@ -0,0 +1,39 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; +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, +): Promise { + // 카드 결제하기 + 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: "테스트 결제 취소", + }), + ); + + // 결제 내역 재 조회하여 다시 검증 + 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 new file mode 100644 index 0000000..8b11777 --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_partial.ts @@ -0,0 +1,64 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; + +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, +): Promise { + // 카드 결제하기 + 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; + + // 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); + }); +} 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 new file mode 100644 index 0000000..41d791a --- /dev/null +++ b/packages/fake-iamport-server/test/features/test_fake_vbank_payment_cancel_without_refund.ts @@ -0,0 +1,26 @@ +import { TestValidator } from "@nestia/e2e"; +import imp from "iamport-server-api"; +import { IIamportVBankPayment } from "iamport-server-api/lib/structures/IIamportVBankPayment"; + +import { test_fake_vbank_payment } from "./test_fake_vbank_payment"; + +export async function test_fake_vbank_payment_cancel_without_refund( + 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", + }), + ); +} diff --git a/packages/fake-iamport-server/test/index.ts b/packages/fake-iamport-server/test/index.ts new file mode 100644 index 0000000..524f75a --- /dev/null +++ b/packages/fake-iamport-server/test/index.ts @@ -0,0 +1,51 @@ +import { DynamicExecutor } from "@nestia/e2e"; +import { IamportConnector } from "iamport-server-api"; + +import { FakeIamportBackend } from "../src/FakeIamportBackend"; +import { FakeIamportConfiguration } from "../src/FakeIamportConfiguration"; +import { ErrorUtil } from "../src/utils/ErrorUtil"; + +async function handle_error(exp: any): Promise { + ErrorUtil.toJSON(exp); +} + +async function main(): Promise { + // 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); + + // DO TEST + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + prefix: "test", + parameters: () => [connector], + })(__dirname + "/features"); + + // 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); + } +} +main().catch((exp) => { + console.log(exp); + process.exit(-1); +}); diff --git a/test/tsconfig.json b/packages/fake-iamport-server/test/tsconfig.json similarity index 100% rename from test/tsconfig.json rename to packages/fake-iamport-server/test/tsconfig.json diff --git a/packages/fake-iamport-server/tsconfig.json b/packages/fake-iamport-server/tsconfig.json new file mode 100644 index 0000000..ff34750 --- /dev/null +++ b/packages/fake-iamport-server/tsconfig.json @@ -0,0 +1,80 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./lib", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + + /* Module Resolution Options */ + // "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": { + "iamport-server-api": ["./src/api"], + "iamport-server-api/lib/*": ["./src/api/*"], + }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "stripInternal": true, + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "plugins": [ + { "transform": "@nestia/core/lib/transform" }, + { "transform": "typia/lib/transform" }, + { "transform": "typescript-transform-paths" } + ] + }, + "include": ["src"] +} diff --git a/packages/api/LICENSE b/packages/fake-toss-payments-server/LICENSE similarity index 100% rename from packages/api/LICENSE rename to packages/fake-toss-payments-server/LICENSE diff --git a/packages/api/README.md b/packages/fake-toss-payments-server/README.md similarity index 100% rename from packages/api/README.md rename to packages/fake-toss-payments-server/README.md diff --git a/nestia.config.ts b/packages/fake-toss-payments-server/nestia.config.ts similarity index 81% rename from nestia.config.ts rename to packages/fake-toss-payments-server/nestia.config.ts index 04263a3..b6c4291 100644 --- a/nestia.config.ts +++ b/packages/fake-toss-payments-server/nestia.config.ts @@ -1,15 +1,16 @@ import type { INestiaConfig } from "@nestia/sdk"; const NESTIA_CONFIG: INestiaConfig = { + simulate: true, input: "src/controllers", output: "src/api", - simulate: true, + distribute: "../toss-payments-server-api", swagger: { - output: "packages/api/swagger.json", + output: "../toss-payments-server-api/swagger.json", info: { title: "Toss Payments API", description: - "Built by [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) with [nestia](https://github.com/samchon/nestia)", + "Built by [fake-toss-payments-server](https://github.com/samchon/payments) with [nestia](https://github.com/samchon/nestia)", }, servers: [ { diff --git a/packages/fake-toss-payments-server/package.json b/packages/fake-toss-payments-server/package.json new file mode 100644 index 0000000..148d64c --- /dev/null +++ b/packages/fake-toss-payments-server/package.json @@ -0,0 +1,91 @@ +{ + "name": "fake-toss-payments-server", + "version": "4.0.0-dev.20230920", + "description": "Fake toss-payments server for testing", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "----------------------------------------------": "", + "build": "npm run build:sdk && npm run build:main && npm run build:test", + "build:api": "rimraf packages/api/lib && nestia sdk && npx copyfiles README.md packages/api && tsc -p packages/api/tsconfig.json", + "build:sdk": "rimraf src/api/functional && nestia sdk", + "build:swagger": "nestia swagger", + "build:main": "rimraf lib && tsc", + "build:test": "rimraf bin && tsc -p test/tsconfig.json", + "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", + "package:next": "npm run package:latest -- --tag next", + "prepare": "ts-patch install", + "-------------------------------------------------": "", + "start": "pm2 start lib/executable/server.js -i 1 --name fake-toss-payments-server --wait-ready --listen-timeout 120000 --kill-timeout 15000", + "start:reload": "pm2 reload fake-toss-payments-server", + "stop": "pm2 delete fake-toss-payments-server", + "--------------------------------------------------": "", + "test": "node bin/test" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/fake-toss-payments-server" + }, + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/fake-toss-payments-server/issues" + }, + "homepage": "https://github.com/samchon/fake-toss-payments-server", + "devDependencies": { + "@nestia/e2e": "^0.3.6", + "@nestia/sdk": "^2.0.4", + "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@types/atob": "^2.1.2", + "@types/btoa": "^1.2.3", + "@types/cli": "^0.11.19", + "@types/node": "^15.6.1", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^5.26.0", + "@typescript-eslint/parser": "^5.26.0", + "cli": "^1.0.1", + "copyfiles": "^2.4.1", + "nestia": "^4.5.0", + "pm2": "^4.5.6", + "prettier": "^2.6.2", + "rimraf": "^3.0.2", + "sloc": "^0.2.1", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", + "typescript": "^5.1.6", + "typescript-transform-paths": "^3.4.6" + }, + "dependencies": { + "@nestia/core": "^2.0.4", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "serialize-error": "^4.1.0", + "source-map-support": "^0.5.19", + "tstl": "^2.5.13", + "typia": "^5.0.4", + "uuid": "^9.0.0" + }, + "keywords": [ + "toss", + "payments", + "server", + "fake", + "test", + "mock" + ], + "files": [ + "package.json", + "README.md", + "LICENSE", + "lib", + "src", + "!lib/test", + "!src/test" + ] +} \ No newline at end of file diff --git a/src/FakeTossBackend.ts b/packages/fake-toss-payments-server/src/FakeTossBackend.ts similarity index 96% rename from src/FakeTossBackend.ts rename to packages/fake-toss-payments-server/src/FakeTossBackend.ts index 3725925..f4f8c3a 100644 --- a/src/FakeTossBackend.ts +++ b/packages/fake-toss-payments-server/src/FakeTossBackend.ts @@ -10,7 +10,7 @@ import { TossFakeConfiguration } from "./FakeTossConfiguration"; /** * Fake 토스 페이먼츠 서버의 백엔드 프로그램. * - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ export class FakeTossBackend { private application_?: NestFastifyApplication; diff --git a/src/FakeTossConfiguration.ts b/packages/fake-toss-payments-server/src/FakeTossConfiguration.ts similarity index 97% rename from src/FakeTossConfiguration.ts rename to packages/fake-toss-payments-server/src/FakeTossConfiguration.ts index 0b0f05b..1e1198d 100644 --- a/src/FakeTossConfiguration.ts +++ b/packages/fake-toss-payments-server/src/FakeTossConfiguration.ts @@ -12,7 +12,7 @@ if (EXTENSION === "js") require("source-map-support").install(); /** * Fake 토스 페이먼츠 서버의 설정 정보. * - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ export namespace TossFakeConfiguration { /** diff --git a/packages/fake-toss-payments-server/src/api/HttpError.ts b/packages/fake-toss-payments-server/src/api/HttpError.ts new file mode 100644 index 0000000..5df328a --- /dev/null +++ b/packages/fake-toss-payments-server/src/api/HttpError.ts @@ -0,0 +1 @@ +export { HttpError } from "@nestia/fetcher"; diff --git a/packages/fake-toss-payments-server/src/api/IConnection.ts b/packages/fake-toss-payments-server/src/api/IConnection.ts new file mode 100644 index 0000000..107bdb8 --- /dev/null +++ b/packages/fake-toss-payments-server/src/api/IConnection.ts @@ -0,0 +1 @@ +export type { IConnection } from "@nestia/fetcher"; diff --git a/packages/fake-toss-payments-server/src/api/Primitive.ts b/packages/fake-toss-payments-server/src/api/Primitive.ts new file mode 100644 index 0000000..60d3944 --- /dev/null +++ b/packages/fake-toss-payments-server/src/api/Primitive.ts @@ -0,0 +1 @@ +export type { Primitive } from "@nestia/fetcher"; diff --git a/src/api/functional/index.ts b/packages/fake-toss-payments-server/src/api/functional/index.ts similarity index 100% rename from src/api/functional/index.ts rename to packages/fake-toss-payments-server/src/api/functional/index.ts diff --git a/src/api/functional/internal/index.ts b/packages/fake-toss-payments-server/src/api/functional/internal/index.ts similarity index 97% rename from src/api/functional/internal/index.ts rename to packages/fake-toss-payments-server/src/api/functional/internal/index.ts index 069c56b..0ee4777 100644 --- a/src/api/functional/internal/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/internal/index.ts @@ -25,9 +25,9 @@ import { NestiaSimulator } from "../../utils/NestiaSimulator"; * 이벤트가 귀하의 백엔드 서버로 제대로 전달되도록 하자. * * @param input 웹훅 이벤트 정보 - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossInternalController.webhook * @path POST /internal/webhook * @nestia Generated by Nestia - https://github.com/samchon/nestia */ @@ -103,9 +103,9 @@ export namespace webhook { * @param paymentKey 대상 가상 계좌 결제 정보의 {@link ITossPayment.paymentKey} * @returns 입금 완료된 가상 꼐좌 결제 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossInternalController.deposit * @path GET /internal/:paymentKey/deposit * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/functional/v1/billing/authorizations/card/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/billing/authorizations/card/index.ts similarity index 97% rename from src/api/functional/v1/billing/authorizations/card/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/billing/authorizations/card/index.ts index 7ad9356..725bb13 100644 --- a/src/api/functional/v1/billing/authorizations/card/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/v1/billing/authorizations/card/index.ts @@ -26,9 +26,9 @@ import { NestiaSimulator } from "../../../../../utils/NestiaSimulator"; * @param input 간편 결제 카드 등록 정보 * @returns 간편 결제 카드 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossBillingController.store * @path POST /v1/billing/authorizations/card * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/functional/v1/billing/authorizations/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/billing/authorizations/index.ts similarity index 97% rename from src/api/functional/v1/billing/authorizations/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/billing/authorizations/index.ts index b70b4d2..014bc53 100644 --- a/src/api/functional/v1/billing/authorizations/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/v1/billing/authorizations/index.ts @@ -27,9 +27,9 @@ export * as card from "./card"; * @param input 고객 식별자 키 * @returns 간편 결제 수단 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossBillingController.at * @path POST /v1/billing/authorizations/:billingKey * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/functional/v1/billing/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/billing/index.ts similarity index 98% rename from src/api/functional/v1/billing/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/billing/index.ts index f0c22d1..56ace01 100644 --- a/src/api/functional/v1/billing/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/v1/billing/index.ts @@ -35,9 +35,9 @@ export * as authorizations from "./authorizations"; * @param input 주문 정보 * @returns 결제 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossBillingController.pay * @path POST /v1/billing/:billingKey * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/functional/v1/cash_receipts/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/cash_receipts/index.ts similarity index 96% rename from src/api/functional/v1/cash_receipts/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/cash_receipts/index.ts index fbda66f..a059f8d 100644 --- a/src/api/functional/v1/cash_receipts/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/v1/cash_receipts/index.ts @@ -17,9 +17,9 @@ import { NestiaSimulator } from "../../../utils/NestiaSimulator"; * @param input 입력 정보 * @returns 현금 영수증 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossCashReceiptsController.store * @path POST /v1/cash-receipts * @nestia Generated by Nestia - https://github.com/samchon/nestia */ @@ -97,9 +97,9 @@ export namespace store { * @param input 취소 입력 정보 * @returns 취소된 현금 영수증 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossCashReceiptsController.cancel * @path POST /v1/cash-receipts/:receiptKey/cancel * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/functional/v1/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/index.ts similarity index 100% rename from src/api/functional/v1/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/index.ts diff --git a/src/api/functional/v1/payments/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/payments/index.ts similarity index 97% rename from src/api/functional/v1/payments/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/payments/index.ts index 368e1d7..dd70952 100644 --- a/src/api/functional/v1/payments/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/v1/payments/index.ts @@ -29,9 +29,9 @@ import { NestiaSimulator } from "../../../utils/NestiaSimulator"; * * @param paymentKey 결제 정보의 {@link ITossPayment.paymentKey} * @returns 결제 정보 - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossPaymentsController.at * @path GET /v1/payments/:paymentKey * @nestia Generated by Nestia - https://github.com/samchon/nestia */ @@ -115,9 +115,9 @@ export namespace at { * @param input 카드 결제 입력 정보 * @returns 카드 결제 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossPaymentsController.key_in * @path POST /v1/payments/key-in * @nestia Generated by Nestia - https://github.com/samchon/nestia */ @@ -205,9 +205,9 @@ export namespace key_in { * @param input 주문 정보 확인 * @returns 승인된 결제 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossPaymentsController.approve * @path POST /v1/payments/:paymentKey * @nestia Generated by Nestia - https://github.com/samchon/nestia */ @@ -294,9 +294,9 @@ export namespace approve { * @param input 취소 입력 정보 * @returns 취소된 결제 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossPaymentsController.cancel * @path POST /v1/payments/:paymentKey/cancel * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/functional/v1/virtual_accounts/index.ts b/packages/fake-toss-payments-server/src/api/functional/v1/virtual_accounts/index.ts similarity index 98% rename from src/api/functional/v1/virtual_accounts/index.ts rename to packages/fake-toss-payments-server/src/api/functional/v1/virtual_accounts/index.ts index 4d717c1..9fa209d 100644 --- a/src/api/functional/v1/virtual_accounts/index.ts +++ b/packages/fake-toss-payments-server/src/api/functional/v1/virtual_accounts/index.ts @@ -33,9 +33,9 @@ import { NestiaSimulator } from "../../../utils/NestiaSimulator"; * @param input 가상 결제 신청 정보. * @returns 가상 계좌 결제 정보 * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon * - * @controller [object Object] + * @controller FakeTossVirtualAccountsController.store * @path POST /v1/virtual-accounts * @nestia Generated by Nestia - https://github.com/samchon/nestia */ diff --git a/src/api/index.ts b/packages/fake-toss-payments-server/src/api/index.ts similarity index 73% rename from src/api/index.ts rename to packages/fake-toss-payments-server/src/api/index.ts index 00dcae5..bb6b7e0 100644 --- a/src/api/index.ts +++ b/packages/fake-toss-payments-server/src/api/index.ts @@ -1,4 +1,5 @@ +import * as toss from "./module"; + export * from "./module"; -import * as toss from "./module"; -export default toss; \ No newline at end of file +export default toss; diff --git a/src/api/module.ts b/packages/fake-toss-payments-server/src/api/module.ts similarity index 69% rename from src/api/module.ts rename to packages/fake-toss-payments-server/src/api/module.ts index 91174a8..dbb6e9a 100644 --- a/src/api/module.ts +++ b/packages/fake-toss-payments-server/src/api/module.ts @@ -2,4 +2,4 @@ export type * from "./IConnection"; export type * from "./Primitive"; export * from "./HttpError"; -export * as functional from "./functional"; \ No newline at end of file +export * as functional from "./functional"; diff --git a/src/api/structures/ITossBilling.ts b/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts similarity index 85% rename from src/api/structures/ITossBilling.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts index 3b9d99f..fa41106 100644 --- a/src/api/structures/ITossBilling.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossBilling.ts @@ -2,19 +2,17 @@ import { tags } from "typia"; /** * 간편 결제 등록 수단 정보. - * + * * `ITossBilling` 은 간편 결제 등록 수단을 형상화한 자료구조 인터페이스로써, 고객이 자신의 * 신용 카드를 서버에 등록해두고, 매번 결제가 필요할 때마다 카드 정보를 반복 입려하는 일 * 없이 간편하게 결제를 진행하고자 할 때 사용한다. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ -export interface ITossBilling - extends ITossBilling.ICustomerKey -{ +export interface ITossBilling extends ITossBilling.ICustomerKey { /** * 가맹점 ID. - * + * * 현재 tosspayments 가 쓰임. */ mId: string; @@ -23,7 +21,7 @@ export interface ITossBilling * {@link ITossBilling} 의 식별자 키. */ billingKey: string; - + /** * 결제 수단. */ @@ -44,14 +42,11 @@ export interface ITossBilling */ authenticatedAt: string & tags.Format<"date-time">; } -export namespace ITossBilling -{ +export namespace ITossBilling { /** * 간편 결제 카드 등록 정보. */ - export interface IStore - extends ICustomerKey - { + export interface IStore extends ICustomerKey { /** * 카드 번호. */ @@ -65,7 +60,7 @@ export namespace ITossBilling /** * 카드 만료 월 (2 자리). */ - cardExpirationMonth: string & tags.Pattern<"^(0[1-9]|1[012])$">; + cardExpirationMonth: string & tags.Pattern<"^(0[1-9]|1[012])$">; /** * 카드 비밀번호. @@ -74,10 +69,11 @@ export namespace ITossBilling /** * 고객의 생년월일. - * + * * 표기 형식 YYMMDD. */ - customerBirthday: string & tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; + customerBirthday: string & + tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; /** * 고객의 이름. @@ -90,8 +86,8 @@ export namespace ITossBilling customerEmail?: string & tags.Format<"email">; /** - * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. - * + * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. + * * 3DS 인증 결과를 전송해야 하는 경우에만 필수. */ vbv?: { @@ -109,15 +105,13 @@ export namespace ITossBilling * 3DS 인증 결과에 대한 코드 값. */ eci: string; - } + }; } /** * 간편 결제를 이용한 결제 신청 정보. */ - export interface IPaymentStore - extends ICustomerKey - { + export interface IPaymentStore extends ICustomerKey { /** * 결제 수단이 간편 결제임을 의미함. */ @@ -130,11 +124,11 @@ export namespace ITossBilling /** * 주문 식별자 키. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ orderId: string; - + /** * 결제 총액. */ @@ -143,21 +137,19 @@ export namespace ITossBilling /** * {@link ITossBilling} 의 식별자 정보. - * + * * `ITossBilling.IAccessor` 는 프론트 어플리케이션이 토스 페이먼츠에서 제공해주는 * 간편 결제 카드 등록 창을 이용했을 때, 해당 창에서 모든 과정이 완료된 후 보내주는 * 정보를 형상화한 자료구조 인터페이스이다. - * + * * 프론트 어플리케이션이 이 식별자 정보를 백엔드 서버로 보내면, 백엔드 서버는 토스 * 페이먼츠 서버의 {@link functional.billing.at} 함수를 호출함으로써, 해당 간편 결제 * 수단 정보를 취득할 수 있다. */ - export interface IAccessor - extends ICustomerKey - { + export interface IAccessor extends ICustomerKey { /** * 토스 페이먼츠에서 redirect URL 로 보내준 값. - * + * * 실상 Billing 의 식별자 {@link ITossBilling.billingKey} 그 자체라고 보면 됨. */ authKey: string; @@ -166,13 +158,12 @@ export namespace ITossBilling /** * 고객 식별자 정보. */ - export interface ICustomerKey - { + export interface ICustomerKey { /** * 고객 식별자 키. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ customerKey: string; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossCardPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts similarity index 82% rename from src/api/structures/ITossCardPayment.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts index 512042a..7442834 100644 --- a/src/api/structures/ITossCardPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossCardPayment.ts @@ -1,14 +1,14 @@ import { tags } from "typia"; + import { ITossPayment } from "./ITossPayment"; /** * 카드 결제 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ export interface ITossCardPayment - extends ITossPayment.IBase<"카드", "NORMAL"|"BILLING"> -{ + extends ITossPayment.IBase<"카드", "NORMAL" | "BILLING"> { /** * 카드 정보. */ @@ -17,20 +17,18 @@ export interface ITossCardPayment /** * 카드사의 즉시 할인 프로모션 정보. */ - discount: ITossCardPayment.IDiscount | null; + discount: null | ITossCardPayment.IDiscount; /** * 간편결제로 결제한 경우 간편결제 타입 정보. */ - easyPay: "토스결제" | "페이코" | "삼성페이" | null; + easyPay: null | "토스결제" | "페이코" | "삼성페이"; } -export namespace ITossCardPayment -{ +export namespace ITossCardPayment { /** * 카드 정보. */ - export interface ICard - { + export interface ICard { /** * 카드사 이름. */ @@ -73,14 +71,19 @@ export namespace ITossCardPayment /** * 카드 결제의 매입 상태. - * + * * - READY: 매입 대기 * - REQUESTED: 매입 요청됨 * - COMPLETED: 매입 완료 * - CANCEL_REQUESTED: 매입 취소 요청됨 * - CANCELD: 매입 취소됨 */ - acquireStatus: "READY" | "REQUESTED" | "COMPLETED" | "CANCEL_REQUESTED" | "CANCELED"; + acquireStatus: + | "READY" + | "REQUESTED" + | "COMPLETED" + | "CANCEL_REQUESTED" + | "CANCELED"; /** * 영수증 URL. @@ -91,24 +94,22 @@ export namespace ITossCardPayment /** * 카드사의 즉시 할인 프로모션 정보. */ - export interface IDiscount - { + export interface IDiscount { /** * 카드사의 즉시 할인 프로모션을 적용한 금액. */ amount: number; } - + /** * 신용 카드를 이용한 결제 신청 정보. */ - export interface IStore - { + export interface IStore { /** * 결제 수단이 신용 카드임을 의미. */ method: "card"; - + /** * 카드 번호. */ @@ -143,27 +144,28 @@ export namespace ITossCardPayment * 면세금 총액. */ 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])$">; + customerBirthday?: string & + tags.Pattern<"^([0-9]{2})(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])$">; /** * 고객의 이메일. @@ -171,8 +173,8 @@ export namespace ITossCardPayment customerEmail?: string & tags.Format<"email">; /** - * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. - * + * 해외카드로 결제하는 경우 3DS 인증 적용을 위해 사용. + * * 3DS 인증 결과를 전송해야 하는 경우에만 필수. */ vbv?: { @@ -190,25 +192,25 @@ export namespace ITossCardPayment * 3DS 인증 결과에 대한 코드 값. */ eci: string; - } + }; /** * 결제 승인 여부. - * - * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, - * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 - * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 + * + * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, + * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 + * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 * 상황을 시뮬레이션할 수 있다. - * + * * 본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고, - * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, - * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 + * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, + * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 * 정식 결제로 인청치 아니한다. - * + * * 반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는 * 이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가 * 필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것. */ __approved?: boolean; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossCashReceipt.ts b/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts similarity index 89% rename from src/api/structures/ITossCashReceipt.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts index d85e8ef..3d375d9 100644 --- a/src/api/structures/ITossCashReceipt.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossCashReceipt.ts @@ -2,11 +2,10 @@ import { tags } from "typia"; /** * 현금 영수증 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ -export interface ITossCashReceipt -{ +export interface ITossCashReceipt { /** * 현금 영수증의 식별자 키. */ @@ -26,7 +25,7 @@ export interface ITossCashReceipt * 주문 이름. */ orderName: string; - + /** * 현금 영수증 승인 번호. */ @@ -52,8 +51,7 @@ export interface ITossCashReceipt */ __paymentKey: string; } -export namespace ITossCashReceipt -{ +export namespace ITossCashReceipt { /** * 현금 영수증의 종류. */ @@ -62,8 +60,7 @@ export namespace ITossCashReceipt /** * 현금 영수증 요약 정보. */ - export interface ISummary - { + export interface ISummary { /** * 현금 영수증의 종류. */ @@ -93,8 +90,7 @@ export namespace ITossCashReceipt /** * 현금 영수증 입력 정보. */ - export interface IStore - { + export interface IStore { /** * 현금 영수증의 종류. */ @@ -117,8 +113,8 @@ export namespace ITossCashReceipt /** * 현금 영수증 발급을 위한 개인 식별 번호. - * - * 현금 영수증의 종류에 따라 휴대폰 번호나 주민등록번호 또는 사업자등록번호 및 + * + * 현금 영수증의 종류에 따라 휴대폰 번호나 주민등록번호 또는 사업자등록번호 및 * 카드 번호를 입력할 수 있다. */ registrationNumber: string; @@ -142,13 +138,12 @@ export namespace ITossCashReceipt /** * 현금 영수증 취소 입력 정보. */ - export interface ICancel - { + export interface ICancel { /** * 취소 금액. - * + * * 미 입력시 현금 영수증에 기재된 {@link ITossCashReceipt.amount 총액}이 취소됨. */ amount?: number; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossGiftCertificatePayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts similarity index 69% rename from src/api/structures/ITossGiftCertificatePayment.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts index 45dfa74..101f0ad 100644 --- a/src/api/structures/ITossGiftCertificatePayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossGiftCertificatePayment.ts @@ -2,24 +2,21 @@ import { ITossPayment } from "./ITossPayment"; /** * 상품권 결제 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ export interface ITossGiftCertificatePayment - extends ITossPayment.IBase<"상품권", "NORMAL"> -{ + extends ITossPayment.IBase<"상품권", "NORMAL"> { /** * 상품권 정보. */ giftCertificate: ITossGiftCertificatePayment.IGiftCertificate; } -export namespace ITossGiftCertificatePayment -{ +export namespace ITossGiftCertificatePayment { /** * 상품권 정보. */ - export interface IGiftCertificate - { + export interface IGiftCertificate { /** * 승인 번호. */ @@ -30,4 +27,4 @@ export namespace ITossGiftCertificatePayment */ settlementStatus: "COMPLETE" | "INCOMPLETE"; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossMobilePhonePayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts similarity index 66% rename from src/api/structures/ITossMobilePhonePayment.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts index 1bba26b..3ad8764 100644 --- a/src/api/structures/ITossMobilePhonePayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossMobilePhonePayment.ts @@ -2,24 +2,21 @@ import { ITossPayment } from "./ITossPayment"; /** * 휴대폰 결제 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ export interface ITossMobilePhonePayment - extends ITossPayment.IBase<"휴대폰", "NORMAL"> -{ + extends ITossPayment.IBase<"휴대폰", "NORMAL"> { /** * 휴대폰 정보. */ - mobilePhone: ITossMobilePhonePayment.IMobilePhone + mobilePhone: ITossMobilePhonePayment.IMobilePhone; } -export namespace ITossMobilePhonePayment -{ +export namespace ITossMobilePhonePayment { /** * 휴대폰 정보. */ - export interface IMobilePhone - { + export interface IMobilePhone { /** * 통신사. */ @@ -35,4 +32,4 @@ export namespace ITossMobilePhonePayment */ settlementStatus: "INCOMPLETED" | "COMPLETED"; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts similarity index 84% rename from src/api/structures/ITossPayment.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts index 65be2bc..79ad1d7 100644 --- a/src/api/structures/ITossPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossPayment.ts @@ -1,4 +1,5 @@ import { tags } from "typia"; + import { ITossBilling } from "./ITossBilling"; import { ITossCardPayment } from "./ITossCardPayment"; import { ITossCashReceipt } from "./ITossCashReceipt"; @@ -10,27 +11,26 @@ import { ITossVirtualAccountPayment } from "./ITossVirtualAccountPayment"; /** * 결제 정보. - * - * `ITossPayment` 는 토스 페이먼츠의 결제 정보를 형상화한 자료구조이자 유니언 타입의 + * + * `ITossPayment` 는 토스 페이먼츠의 결제 정보를 형상화한 자료구조이자 유니언 타입의 * 인터페이스로써, if condition 을 통하여 method 값을 특정하면, 파생 타입이 자동으로 * 지정된다. - * + * * ```typescript * if (payment.method === "카드") * payment.card; // payment be ITossCardPayment - * ``` - * - * @author Jeongho Nam - https://github.com/samchon + * ``` + * + * @author Samchon */ -export type ITossPayment - = ITossCardPayment +export type ITossPayment = + | ITossCardPayment | ITossGiftCertificatePayment | ITossMobilePhonePayment | ITossTransferPayment | ITossVirtualAccountPayment; -export namespace ITossPayment -{ +export namespace ITossPayment { /* ---------------------------------------------------------------- RESPONSE ---------------------------------------------------------------- */ @@ -38,18 +38,18 @@ export namespace ITossPayment * 결제의 기본 정보. */ 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 extends string, + Type extends string, + Status extends string = + | "READY" + | "IN_PROGRESS" + | "WAITING_FOR_DEPOSIT" + | "DONE" + | "CANCELED" + | "PARTIAL_CANCELED" + | "ABORTED" + | "EXPIRED", + > { /** * 결제 수단. */ @@ -57,7 +57,7 @@ export namespace ITossPayment /** * 결제 타입. - * + * * - NORMAL: 일반 결제 * - BILLING: 미리 등록한 카드에 의한 간편 결제. */ @@ -65,7 +65,7 @@ export namespace ITossPayment /** * 결제 상태. - * + * * - READY * - IN_PROGRESS * - WAITING_FOR_DEPOSIT @@ -79,7 +79,7 @@ export namespace ITossPayment /** * 가맹점 ID. - * + * * 현재 tosspayments 가 쓰임. */ mId: string; @@ -96,28 +96,28 @@ export namespace ITossPayment /** * 주문 식별자 키. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ orderId: string; /** * 거래 건에 대한 고유한 키 값. - * + * * {@link paymentKey} 와 달리, 이를 사용할 일은 없더라. */ transactionKey: string; /** * 주문 이름. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. */ orderName: string; /** * 화폐 단위. - * + * * 현재 토스 페이먼츠는 KRW 만 사용 가능. */ currency: string; @@ -154,7 +154,7 @@ export namespace ITossPayment /** * 문화비 지출 여부. - * + * * 도석입, 공연 티켓, 박물관/미술관 입장권 등. */ cultureExpense: boolean; @@ -172,12 +172,12 @@ export namespace ITossPayment /** * 결제 취소 내역. */ - cancels: ITossPaymentCancel[] | null; + cancels: null | ITossPaymentCancel[]; /** * 현금 영수증 정보. */ - cashReceipt: ITossCashReceipt.ISummary | null; + cashReceipt: null | ITossCashReceipt.ISummary; } /* ---------------------------------------------------------------- @@ -186,11 +186,10 @@ export namespace ITossPayment /** * 결제 승인 정보. */ - export interface IApproval - { + export interface IApproval { /** * 주문 식별자 키. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ orderId: string; @@ -203,18 +202,18 @@ export namespace ITossPayment /** * 결제 신청 정보. - * + * * `ITossPayment.IStore` 는 결제 신청 정보를 형상화한 자료구조이자 유니언 타입의 * 인터페이스로써, if condition 을 이용하여 대상 method 를 특정하면, 파생 타입이 * 자동으로 지정된다. - * + * * ```typescript * if (input.method === "card") * input.cardNumber; // input is ITossCardPayment.IStore * ``` */ - export type IStore - = ITossCardPayment.IStore + export type IStore = + | ITossCardPayment.IStore | ITossBilling.IPaymentStore | ITossVirtualAccountPayment.IStore; @@ -224,4 +223,4 @@ export namespace ITossPayment // orderId: string; // amount: number; // } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossPaymentCancel.ts b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts similarity index 87% rename from src/api/structures/ITossPaymentCancel.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts index f3c9140..f657ad6 100644 --- a/src/api/structures/ITossPaymentCancel.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentCancel.ts @@ -2,11 +2,10 @@ import { tags } from "typia"; /** * 결제 취소 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ -export interface ITossPaymentCancel -{ +export interface ITossPaymentCancel { /** * 취소 총액. */ @@ -37,13 +36,11 @@ export interface ITossPaymentCancel */ canceledAt: string & tags.Format<"date-time">; } -export namespace ITossPaymentCancel -{ +export namespace ITossPaymentCancel { /** * 결제 취소 신청 정보. */ - export interface IStore - { + export interface IStore { /** * {@link ITossPayment} 의 식별자 키. */ @@ -61,11 +58,10 @@ export namespace ITossPaymentCancel /** * 환불 계좌 정보. - * + * * 결제를 가상 계좌로 하였을 때에만 해당함. */ - refundReceiveAccount?: - { + refundReceiveAccount?: { /** * 은행 정보. */ @@ -97,4 +93,4 @@ export namespace ITossPaymentCancel */ refundableAmount?: number; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossPaymentWebhook.ts b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts similarity index 73% rename from src/api/structures/ITossPaymentWebhook.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts index c274cd6..77e6a09 100644 --- a/src/api/structures/ITossPaymentWebhook.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossPaymentWebhook.ts @@ -1,27 +1,24 @@ /** * 웹훅 이벤트 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ -export interface ITossPaymentWebhook -{ +export interface ITossPaymentWebhook { /** * 이벤트 타입. */ eventType: "PAYMENT_STATUS_CHANGED"; - + /** * 이벤트 데이터. */ data: ITossPaymentWebhook.IData; } -export namespace ITossPaymentWebhook -{ +export namespace ITossPaymentWebhook { /** * 웹훅 이벤트 데이터. */ - export interface IData - { + export interface IData { /** * {@link ITossPayment} 의 식별자 키. */ @@ -29,19 +26,23 @@ export namespace ITossPaymentWebhook /** * 주문 식별자 키. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ orderId: string; /** * 결제 상태. - * + * * - DONE: 결제 완료 * - CANCELED: 결제가 취소됨 * - PARTIAL_CANCELED: 결제가 부분 취소됨 * - WAITING_FOR_DEPOSIT: 입금 대기 중 */ - status: "DONE" | "CANCELED" | "PARTIAL_CANCELED" | "WAITING_FOR_DEPOSIT"; + status: + | "DONE" + | "CANCELED" + | "PARTIAL_CANCELED" + | "WAITING_FOR_DEPOSIT"; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossTransferPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts similarity index 69% rename from src/api/structures/ITossTransferPayment.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts index b6db18f..ca22544 100644 --- a/src/api/structures/ITossTransferPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossTransferPayment.ts @@ -2,24 +2,21 @@ import { ITossPayment } from "./ITossPayment"; /** * 계좌 이체 결제 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ export interface ITossTransferPayment - extends ITossPayment.IBase<"계좌이체", "NORMAL"> -{ + extends ITossPayment.IBase<"계좌이체", "NORMAL"> { /** * 계좌 이체 정보. */ transfer: ITossTransferPayment.ITransfer; } -export namespace ITossTransferPayment -{ +export namespace ITossTransferPayment { /** * 계좌 이체 정보. */ - export interface ITransfer - { + export interface ITransfer { /** * 은행명. */ @@ -30,4 +27,4 @@ export namespace ITossTransferPayment */ settlementStatus: "INCOMPLETED" | "COMPLETED"; } -} \ No newline at end of file +} diff --git a/src/api/structures/ITossVirtualAccountPayment.ts b/packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts similarity index 79% rename from src/api/structures/ITossVirtualAccountPayment.ts rename to packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts index c48c1db..7279d98 100644 --- a/src/api/structures/ITossVirtualAccountPayment.ts +++ b/packages/fake-toss-payments-server/src/api/structures/ITossVirtualAccountPayment.ts @@ -1,31 +1,29 @@ import { tags } from "typia"; + import { ITossPayment } from "./ITossPayment"; /** * 가상 계좌 결제 정보. - * - * @author Jeongho Nam - https://github.com/samchon + * + * @author Samchon */ export interface ITossVirtualAccountPayment - extends ITossPayment.IBase<"가상계좌", "NORMAL"> -{ + extends ITossPayment.IBase<"가상계좌", "NORMAL"> { /** * 가상 계좌로 결제할 때 전달되는 입금 콜백을 검증하기 위한 값. */ secret: string; - + /** * 가상 계좌 정보. */ virtualAccount: ITossVirtualAccountPayment.IVirtualAccount; } -export namespace ITossVirtualAccountPayment -{ +export namespace ITossVirtualAccountPayment { /** * 가상 계좌를 이용한 결제 신청 정보. */ - export interface IStore - { + export interface IStore { /** * 결제 수단이 가상 계좌임을 의미. */ @@ -33,14 +31,14 @@ export namespace ITossVirtualAccountPayment /** * 주문 식별자 번호. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 자체적으로 관리하는 식별자 키. */ orderId: string; /** * 주문 이름. - * + * * 토스 페이먼츠가 아닌, 이를 이용하는 서비스에서 발급한 주문명. */ orderName: string; @@ -62,17 +60,17 @@ export namespace ITossVirtualAccountPayment /** * 결제 승인 여부. - * - * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, - * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 - * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 + * + * 오직 가짜 페이먼츠 서버 `fake-toss-payments-server` 에서만 사용되는 값으로써, + * 결제 승인을 고의로 지연시키거나 할 때 사용된다. 이 값을 `false` 로 하면, 프론트 + * 어플리케이션이 토스 페이먼츠가 제공해주는 결제 창을 사용하여 결제를 진행하는 * 상황을 시뮬레이션할 수 있다. - * + * * 본디 토스 페이먼츠 서버는 프론트 어플리케이션에서 백엔드 서버를 거치지 않고, - * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, - * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 + * 토스 페이먼츠가 제공해주는 결제 창을 이용하여 직접 결제를 요청하는 경우, + * 백엔드에서 이를 별도 {@link functional.payments.approve 승인} 처리해주기 전까지 * 정식 결제로 인청치 아니한다. - * + * * 반면 백엔드 서버에서 토스 페이먼츠 서버의 API 를 호출하는 경우, 토스 페이먼츠는 * 이를 그 즉시로 승인해주기, `fake-toss-payments-server` 에서 별도의 승인 처리가 * 필요한 상황을 시뮬레이션하기 위해서는 이러한 속성이 필요한 것. @@ -83,8 +81,7 @@ export namespace ITossVirtualAccountPayment /** * 가상 계좌 정보. */ - export interface IVirtualAccount - { + export interface IVirtualAccount { /** * 계좌 번호. */ @@ -94,41 +91,46 @@ export namespace ITossVirtualAccountPayment * 가상 계좌 타입. */ accountType: "일반" | "고정"; - + /** * 은행명. */ bank: string; - + /** * 고객 이름. */ customerName: string; - + /** * 입금 기한. */ dueDate: string & tags.Format<"date">; - + /** * 가상 계좌 만료 여부. */ expired: boolean; - + /** * 정산 상태. */ - settlementStatus: "INCOMPLETED" | "COMPLETED", - + settlementStatus: "INCOMPLETED" | "COMPLETED"; + /** * 환불 처리 상태. - * + * * - NONE: 해당 없음 * - FAILED: 환불 실패 * - PENDING: 환불 처리중 * - PARTIAL_FAILED: 부분 환불 실패 * - COMPLETED: 환불 완료 */ - refundStatus: "NONE" | "FAILED" | "PENDING" | "PARTIAL_FAILED" | "COMPLETED"; + refundStatus: + | "NONE" + | "FAILED" + | "PENDING" + | "PARTIAL_FAILED" + | "COMPLETED"; } -} \ No newline at end of file +} diff --git a/packages/fake-toss-payments-server/src/api/typings/Atomic.ts b/packages/fake-toss-payments-server/src/api/typings/Atomic.ts new file mode 100644 index 0000000..61a6dee --- /dev/null +++ b/packages/fake-toss-payments-server/src/api/typings/Atomic.ts @@ -0,0 +1,14 @@ +/** + * @packageDocumentation + * @module api.typings + */ +//================================================================ +/** + * 객체 정의로부터 원자 멤버들만의 타입을 추려냄. + * + * @template Instance 대상 객체의 타입 + * @author Samchon + */ +export type Atomic = { + [P in keyof Instance]: Instance[P] extends object ? never : Instance[P]; +}; diff --git a/packages/fake-toss-payments-server/src/api/typings/Writable.ts b/packages/fake-toss-payments-server/src/api/typings/Writable.ts new file mode 100644 index 0000000..0ed6231 --- /dev/null +++ b/packages/fake-toss-payments-server/src/api/typings/Writable.ts @@ -0,0 +1,12 @@ +/** + * @packageDocumentation + * @module api.typings + */ +//================================================================ +export type Writable = { + -readonly [P in keyof T]: T[P]; +}; + +export function Writable(elem: Readonly): Writable { + return elem; +} diff --git a/packages/fake-toss-payments-server/src/api/utils/NestiaSimulator.ts b/packages/fake-toss-payments-server/src/api/utils/NestiaSimulator.ts new file mode 100644 index 0000000..085ff9f --- /dev/null +++ b/packages/fake-toss-payments-server/src/api/utils/NestiaSimulator.ts @@ -0,0 +1,70 @@ +import { HttpError } from "@nestia/fetcher"; + +import typia from "typia"; + +export namespace NestiaSimulator { + export interface IProps { + host: string; + path: string; + method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + contentType: string; + } + + export const assert = (props: IProps) => { + return { + param: param(props), + query: query(props), + body: body(props), + }; + }; + const param = + (props: IProps) => + (name: string) => + (task: () => T): void => { + validate( + (exp) => `URL parameter "${name}" is not ${exp.expected} type.`, + )(props)(task); + }; + + const query = + (props: IProps) => + (task: () => T): void => + validate( + () => + "Request query parameters are not following the promised type.", + )(props)(task); + + const body = + (props: IProps) => + (task: () => T): void => + validate(() => "Request body is not following the promised type.")( + props, + )(task); + + const validate = + (message: (exp: typia.TypeGuardError) => string, path?: string) => + (props: IProps) => + (task: () => T): void => { + try { + task(); + } catch (exp) { + if (typia.is(exp)) + throw new HttpError( + props.method, + props.host + props.path, + 400, + { + "Content-Type": props.contentType, + }, + JSON.stringify({ + method: exp.method, + path: path ?? exp.path, + expected: exp.expected, + value: exp.value, + message: message(exp), + }), + ); + throw exp; + } + }; +} diff --git a/src/controllers/FakeTossBillingController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts similarity index 97% rename from src/controllers/FakeTossBillingController.ts rename to packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts index 19e34cd..75e16ad 100644 --- a/src/controllers/FakeTossBillingController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossBillingController.ts @@ -1,11 +1,10 @@ import core from "@nestia/core"; import * as nest from "@nestjs/common"; import * as fastify from "fastify"; -import { v4 } from "uuid"; - 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 { FakeTossPaymentProvider } from "../providers/FakeTossPaymentProvider"; import { FakeTossStorage } from "../providers/FakeTossStorage"; @@ -29,7 +28,7 @@ export class FakeTossBillingController { * @returns 간편 결제 카드 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post("authorizations/card") public store( @@ -66,7 +65,7 @@ export class FakeTossBillingController { * @returns 간편 결제 수단 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post("authorizations/:billingKey") public at( @@ -105,7 +104,7 @@ export class FakeTossBillingController { * @returns 결제 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post(":billingKey") public pay( diff --git a/src/controllers/FakeTossCashReceiptsController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts similarity index 96% rename from src/controllers/FakeTossCashReceiptsController.ts rename to packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts index fcc02c9..446c01b 100644 --- a/src/controllers/FakeTossCashReceiptsController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossCashReceiptsController.ts @@ -1,10 +1,9 @@ import core from "@nestia/core"; import * as nest from "@nestjs/common"; import * as fastify from "fastify"; -import { v4 } from "uuid"; - 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 { FakeTossStorage } from "../providers/FakeTossStorage"; import { FakeTossUserAuth } from "../providers/FakeTossUserAuth"; @@ -18,7 +17,7 @@ export class FakeTossCashReceiptsController { * @returns 현금 영수증 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post() public store( @@ -76,7 +75,7 @@ export class FakeTossCashReceiptsController { * @returns 취소된 현금 영수증 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post(":receiptKey/cancel") public cancel( diff --git a/src/controllers/FakeTossInternalController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts similarity index 96% rename from src/controllers/FakeTossInternalController.ts rename to packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts index 193119e..c2b588e 100644 --- a/src/controllers/FakeTossInternalController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossInternalController.ts @@ -1,7 +1,6 @@ import core from "@nestia/core"; import * as nest from "@nestjs/common"; import * as fastify from "fastify"; - import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossPaymentWebhook } from "toss-payments-server-api/lib/structures/ITossPaymentWebhook"; @@ -24,7 +23,7 @@ export class FakeTossInternalController { * 이벤트가 귀하의 백엔드 서버로 제대로 전달되도록 하자. * * @param input 웹훅 이벤트 정보 - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post("webhook") public webhook(@core.TypedBody() input: ITossPaymentWebhook): void { @@ -49,7 +48,7 @@ export class FakeTossInternalController { * @returns 입금 완료된 가상 꼐좌 결제 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Get(":paymentKey/deposit") public deposit( diff --git a/src/controllers/FakeTossPaymentsController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts similarity index 97% rename from src/controllers/FakeTossPaymentsController.ts rename to packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts index e6450de..f8d1b0a 100644 --- a/src/controllers/FakeTossPaymentsController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossPaymentsController.ts @@ -1,7 +1,6 @@ import core from "@nestia/core"; import * as nest from "@nestjs/common"; import * as fastify from "fastify"; - 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"; @@ -30,7 +29,7 @@ export class FakeTossPaymentsController { * @param paymentKey 결제 정보의 {@link ITossPayment.paymentKey} * @returns 결제 정보 * - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Get(":paymentKey") public at( @@ -66,7 +65,7 @@ export class FakeTossPaymentsController { * @returns 카드 결제 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post("key-in") public key_in( @@ -120,7 +119,7 @@ export class FakeTossPaymentsController { * @returns 승인된 결제 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post(":paymentKey") public approve( @@ -155,7 +154,7 @@ export class FakeTossPaymentsController { * @returns 취소된 결제 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post(":paymentKey/cancel") public cancel( diff --git a/src/controllers/FakeTossVirtualAccountsController.ts b/packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts similarity index 98% rename from src/controllers/FakeTossVirtualAccountsController.ts rename to packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts index e10cd43..0c3c804 100644 --- a/src/controllers/FakeTossVirtualAccountsController.ts +++ b/packages/fake-toss-payments-server/src/controllers/FakeTossVirtualAccountsController.ts @@ -1,9 +1,8 @@ import core from "@nestia/core"; import * as nest from "@nestjs/common"; import * as fastify from "fastify"; -import { v4 } from "uuid"; - import { ITossVirtualAccountPayment } from "toss-payments-server-api/lib/structures/ITossVirtualAccountPayment"; +import { v4 } from "uuid"; import { FakeTossPaymentProvider } from "../providers/FakeTossPaymentProvider"; import { FakeTossStorage } from "../providers/FakeTossStorage"; @@ -36,7 +35,7 @@ export class FakeTossVirtualAccountsController { * @returns 가상 계좌 결제 정보 * * @security basic - * @author Jeongho Nam - https://github.com/samchon + * @author Samchon */ @core.TypedRoute.Post() public store( diff --git a/src/executable/server.ts b/packages/fake-toss-payments-server/src/executable/server.ts similarity index 100% rename from src/executable/server.ts rename to packages/fake-toss-payments-server/src/executable/server.ts diff --git a/src/index.ts b/packages/fake-toss-payments-server/src/index.ts similarity index 100% rename from src/index.ts rename to packages/fake-toss-payments-server/src/index.ts diff --git a/src/module.ts b/packages/fake-toss-payments-server/src/module.ts similarity index 100% rename from src/module.ts rename to packages/fake-toss-payments-server/src/module.ts diff --git a/src/providers/FakeTossPaymentProvider.ts b/packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts similarity index 99% rename from src/providers/FakeTossPaymentProvider.ts rename to packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts index 2f62c18..83b43f2 100644 --- a/src/providers/FakeTossPaymentProvider.ts +++ b/packages/fake-toss-payments-server/src/providers/FakeTossPaymentProvider.ts @@ -1,6 +1,5 @@ -import { v4 } from "uuid"; - import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { v4 } from "uuid"; export namespace FakeTossPaymentProvider { export function get_common_props(input: ITossPayment.IStore) { diff --git a/src/providers/FakeTossStorage.ts b/packages/fake-toss-payments-server/src/providers/FakeTossStorage.ts similarity index 100% rename from src/providers/FakeTossStorage.ts rename to packages/fake-toss-payments-server/src/providers/FakeTossStorage.ts diff --git a/src/providers/FakeTossUserAuth.ts b/packages/fake-toss-payments-server/src/providers/FakeTossUserAuth.ts similarity index 100% rename from src/providers/FakeTossUserAuth.ts rename to packages/fake-toss-payments-server/src/providers/FakeTossUserAuth.ts diff --git a/src/providers/FakeTossWebhookProvider.ts b/packages/fake-toss-payments-server/src/providers/FakeTossWebhookProvider.ts similarity index 100% rename from src/providers/FakeTossWebhookProvider.ts rename to packages/fake-toss-payments-server/src/providers/FakeTossWebhookProvider.ts diff --git a/packages/fake-toss-payments-server/src/utils/DateUtil.ts b/packages/fake-toss-payments-server/src/utils/DateUtil.ts new file mode 100644 index 0000000..e9577e4 --- /dev/null +++ b/packages/fake-toss-payments-server/src/utils/DateUtil.ts @@ -0,0 +1,129 @@ +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; + } + + 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/src/utils/ErrorUtil.ts b/packages/fake-toss-payments-server/src/utils/ErrorUtil.ts similarity index 100% rename from src/utils/ErrorUtil.ts rename to packages/fake-toss-payments-server/src/utils/ErrorUtil.ts diff --git a/packages/fake-toss-payments-server/src/utils/Terminal.ts b/packages/fake-toss-payments-server/src/utils/Terminal.ts new file mode 100644 index 0000000..c7174aa --- /dev/null +++ b/packages/fake-toss-payments-server/src/utils/Terminal.ts @@ -0,0 +1,18 @@ +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)); + }, + ); + }); + } +} diff --git a/src/utils/VolatileMap.ts b/packages/fake-toss-payments-server/src/utils/VolatileMap.ts similarity index 100% rename from src/utils/VolatileMap.ts rename to packages/fake-toss-payments-server/src/utils/VolatileMap.ts diff --git a/test/features/internal/validate_fake_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts similarity index 99% rename from test/features/internal/validate_fake_payment_cancel.ts rename to packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts index 1a5a203..1a6dfaf 100644 --- a/test/features/internal/validate_fake_payment_cancel.ts +++ b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel.ts @@ -1,10 +1,9 @@ import { TestValidator } from "@nestia/e2e"; -import { sleep_for } from "tstl"; -import typia from "typia"; - import toss from "toss-payments-server-api"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossPaymentWebhook } from "toss-payments-server-api/lib/structures/ITossPaymentWebhook"; +import { sleep_for } from "tstl"; +import typia from "typia"; import { FakeTossStorage } from "../../../src/providers/FakeTossStorage"; import { TestConnection } from "../../internal/TestConnection"; diff --git a/test/features/internal/validate_fake_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts similarity index 99% rename from test/features/internal/validate_fake_payment_cancel_over.ts rename to packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts index ebfa5ef..369ad64 100644 --- a/test/features/internal/validate_fake_payment_cancel_over.ts +++ b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_over.ts @@ -1,8 +1,7 @@ import { TestValidator } from "@nestia/e2e"; -import typia from "typia"; - import toss from "toss-payments-server-api"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import typia from "typia"; import { TestConnection } from "../../internal/TestConnection"; diff --git a/test/features/internal/validate_fake_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts similarity index 99% rename from test/features/internal/validate_fake_payment_cancel_partial.ts rename to packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts index d3cef5d..30712de 100644 --- a/test/features/internal/validate_fake_payment_cancel_partial.ts +++ b/packages/fake-toss-payments-server/test/features/internal/validate_fake_payment_cancel_partial.ts @@ -1,9 +1,8 @@ import { ArrayUtil, TestValidator } from "@nestia/e2e"; -import { sleep_for } from "tstl"; -import typia from "typia"; - import toss from "toss-payments-server-api"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { sleep_for } from "tstl"; +import typia from "typia"; import { FakeTossStorage } from "../../../src/providers/FakeTossStorage"; import { TestConnection } from "../../internal/TestConnection"; diff --git a/test/features/test_fake_billing_payment.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts similarity index 99% rename from test/features/test_fake_billing_payment.ts rename to packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts index 40125b2..8b90b60 100644 --- a/test/features/test_fake_billing_payment.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment.ts @@ -1,11 +1,10 @@ import { TestValidator } from "@nestia/e2e"; -import typia from "typia"; -import { v4 } from "uuid"; - import toss from "toss-payments-server-api"; 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 typia from "typia"; +import { v4 } from "uuid"; import { TestConnection } from "../internal/TestConnection"; diff --git a/test/features/test_fake_billing_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel.ts similarity index 100% rename from test/features/test_fake_billing_payment_cancel.ts rename to packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel.ts diff --git a/test/features/test_fake_billing_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_over.ts similarity index 100% rename from test/features/test_fake_billing_payment_cancel_over.ts rename to packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_over.ts diff --git a/test/features/test_fake_billing_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_partial.ts similarity index 100% rename from test/features/test_fake_billing_payment_cancel_partial.ts rename to packages/fake-toss-payments-server/test/features/test_fake_billing_payment_cancel_partial.ts diff --git a/test/features/test_fake_card_payment.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts similarity index 99% rename from test/features/test_fake_card_payment.ts rename to packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts index f543c2f..a9e7e00 100644 --- a/test/features/test_fake_card_payment.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_card_payment.ts @@ -1,10 +1,9 @@ import { TestValidator } from "@nestia/e2e"; -import typia from "typia"; -import { v4 } from "uuid"; - import toss from "toss-payments-server-api"; import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossCardPayment"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import typia from "typia"; +import { v4 } from "uuid"; import { TestConnection } from "../internal/TestConnection"; diff --git a/test/features/test_fake_card_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel.ts similarity index 100% rename from test/features/test_fake_card_payment_cancel.ts rename to packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel.ts diff --git a/test/features/test_fake_card_payment_cancel_over.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_over.ts similarity index 100% rename from test/features/test_fake_card_payment_cancel_over.ts rename to packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_over.ts diff --git a/test/features/test_fake_card_payment_cancel_partial.ts b/packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_partial.ts similarity index 100% rename from test/features/test_fake_card_payment_cancel_partial.ts rename to packages/fake-toss-payments-server/test/features/test_fake_card_payment_cancel_partial.ts diff --git a/test/features/test_fake_cash_receipt.ts b/packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts similarity index 99% rename from test/features/test_fake_cash_receipt.ts rename to packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts index ef229ff..cd8511a 100644 --- a/test/features/test_fake_cash_receipt.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_cash_receipt.ts @@ -1,10 +1,9 @@ import { TestValidator } from "@nestia/e2e"; -import typia from "typia"; - import toss from "toss-payments-server-api"; import { ITossCashReceipt } from "toss-payments-server-api/lib/structures/ITossCashReceipt"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossVirtualAccountPayment } from "toss-payments-server-api/lib/structures/ITossVirtualAccountPayment"; +import typia from "typia"; import { TestConnection } from "../internal/TestConnection"; import { test_fake_virtual_account_payment } from "./test_fake_virtual_account_payment"; diff --git a/test/features/test_fake_storage_capacity.ts b/packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts similarity index 99% rename from test/features/test_fake_storage_capacity.ts rename to packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts index 31cc328..a73ccc4 100644 --- a/test/features/test_fake_storage_capacity.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_storage_capacity.ts @@ -1,13 +1,12 @@ import { TestValidator } from "@nestia/e2e"; +import toss from "toss-payments-server-api"; +import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { randint } from "tstl/algorithm/random"; import { IPointer } from "tstl/functional/IPointer"; import { assert } from "typia"; import { v4 } from "uuid"; -import toss from "toss-payments-server-api"; -import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; -import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; - import { TossFakeConfiguration } from "../../src/FakeTossConfiguration"; import { FakeTossStorage } from "../../src/providers/FakeTossStorage"; import { AdvancedRandomGenerator } from "../internal/AdvancedRandomGenerator"; diff --git a/test/features/test_fake_storage_expiration_time.ts b/packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts similarity index 99% rename from test/features/test_fake_storage_expiration_time.ts rename to packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts index db1b51e..734ceab 100644 --- a/test/features/test_fake_storage_expiration_time.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_storage_expiration_time.ts @@ -1,14 +1,13 @@ import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import toss from "toss-payments-server-api"; +import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossCardPayment"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { randint } from "tstl/algorithm/random"; import { IPointer } from "tstl/functional/IPointer"; import { sleep_for } from "tstl/thread/global"; import { assert } from "typia"; import { v4 } from "uuid"; -import toss from "toss-payments-server-api"; -import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossCardPayment"; -import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; - import { TossFakeConfiguration } from "../../src/FakeTossConfiguration"; import { FakeTossStorage } from "../../src/providers/FakeTossStorage"; import { AdvancedRandomGenerator } from "../internal/AdvancedRandomGenerator"; diff --git a/test/features/test_fake_virtual_account_payment.ts b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts similarity index 99% rename from test/features/test_fake_virtual_account_payment.ts rename to packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts index b0cfd9d..e4e2a8a 100644 --- a/test/features/test_fake_virtual_account_payment.ts +++ b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment.ts @@ -1,11 +1,10 @@ import { TestValidator } from "@nestia/e2e"; -import typia from "typia"; -import { v4 } from "uuid"; - import toss from "toss-payments-server-api"; import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; import { ITossPaymentWebhook } from "toss-payments-server-api/lib/structures/ITossPaymentWebhook"; import { ITossVirtualAccountPayment } from "toss-payments-server-api/lib/structures/ITossVirtualAccountPayment"; +import typia from "typia"; +import { v4 } from "uuid"; import { FakeTossStorage } from "../../src/providers/FakeTossStorage"; import { AdvancedRandomGenerator } from "../internal/AdvancedRandomGenerator"; diff --git a/test/features/test_fake_virtual_account_payment_cancel.ts b/packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel.ts similarity index 100% rename from test/features/test_fake_virtual_account_payment_cancel.ts rename to packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel.ts diff --git a/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 similarity index 100% rename from test/features/test_fake_virtual_account_payment_cancel_over.ts rename to packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_over.ts diff --git a/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 similarity index 100% rename from test/features/test_fake_virtual_account_payment_cancel_partial.ts rename to packages/fake-toss-payments-server/test/features/test_fake_virtual_account_payment_cancel_partial.ts diff --git a/test/index.ts b/packages/fake-toss-payments-server/test/index.ts similarity index 100% rename from test/index.ts rename to packages/fake-toss-payments-server/test/index.ts diff --git a/test/internal/AdvancedRandomGenerator.ts b/packages/fake-toss-payments-server/test/internal/AdvancedRandomGenerator.ts similarity index 100% rename from test/internal/AdvancedRandomGenerator.ts rename to packages/fake-toss-payments-server/test/internal/AdvancedRandomGenerator.ts diff --git a/test/internal/TestConnection.ts b/packages/fake-toss-payments-server/test/internal/TestConnection.ts similarity index 100% rename from test/internal/TestConnection.ts rename to packages/fake-toss-payments-server/test/internal/TestConnection.ts diff --git a/packages/fake-toss-payments-server/test/tsconfig.json b/packages/fake-toss-payments-server/test/tsconfig.json new file mode 100644 index 0000000..25ae87e --- /dev/null +++ b/packages/fake-toss-payments-server/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../bin", + }, + "include": [".", "../src"] +} \ No newline at end of file diff --git a/tsconfig.json b/packages/fake-toss-payments-server/tsconfig.json similarity index 100% rename from tsconfig.json rename to packages/fake-toss-payments-server/tsconfig.json diff --git a/packages/iamport-server-api/README.md b/packages/iamport-server-api/README.md new file mode 100644 index 0000000..1ac96c5 --- /dev/null +++ b/packages/iamport-server-api/README.md @@ -0,0 +1,38 @@ +# SDK Library +This is a SDK library generated by [`nestia`](https://nestia.io). + +With this SDK library, you can easily and safely interact with backend server. + +Just import and call some API functions like gif image below: + +![nestia-sdk-demo](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif) + +> Left is server code, and right is client code utilizing the SDK + + + + +# What [`Nestia`](https://nestia.io) is: +![Nestia Logo](https://nestia.io/logo.png) + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) +[![Downloads](https://img.shields.io/npm/dm/nestia.svg)](https://www.npmjs.com/package/nestia) +[![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) + +[Nestia](https://nestia.io) is a set of helper libraries for NestJS, supporting below features: + + - `@nestia/core`: super-fast decorators + - `@nestia/sdk` + - SDK generator for clients + - Swagger generator evolved than ever + - Automatic E2E test functions generator + - `nestia`: just CLI (command line interface) tool + +> **Note** +> +> - **Only one line** required, with pure TypeScript type +> - Runtime validator is **20,000x faster** than `class-validator` +> - JSON serialization is **200x faster** than `class-transformer` +> - SDK is similar with [tRPC](https://trpc.io), but much advanced \ No newline at end of file diff --git a/packages/iamport-server-api/package.json b/packages/iamport-server-api/package.json new file mode 100644 index 0000000..814fc82 --- /dev/null +++ b/packages/iamport-server-api/package.json @@ -0,0 +1,39 @@ +{ + "name": "iamport-server-api", + "version": "4.0.0-dev.20230920", + "description": "SDK library generated by Nestia", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "build": "npm run build:sdk && npm run compile", + "build:sdk": "rimraf ../fake-iamport-server/src/api/functional && cd ../fake-iamport-server && npx nestia sdk && cd ../iamport-server-api", + "compile": "rimraf lib && tsc", + "deploy": "npm run build && npm publish", + "postinstall": "ts-patch install" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/nestia" + }, + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/nestia/issues" + }, + "homepage": "https://nestia.io", + "files": [ + "lib", + "package.json", + "README.md" + ], + "devDependencies": { + "rimraf": "^5.0.1", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", + "typescript": "^5.2.2" + }, + "dependencies": { + "@nestia/fetcher": "^2.0.4", + "typia": "^5.0.4" + } +} \ No newline at end of file diff --git a/packages/iamport-server-api/tsconfig.json b/packages/iamport-server-api/tsconfig.json new file mode 100644 index 0000000..0d70bb0 --- /dev/null +++ b/packages/iamport-server-api/tsconfig.json @@ -0,0 +1,97 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "DOM", + "ES2015" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */// "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */// "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */// "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */// "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + "newLine": "lf", /* Set the newline character for emitting files. */// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. *//* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "plugins": [ + { + "transform": "typia/lib/transform" + } + ], + "strictNullChecks": true + }, + "include": [ + "../fake-iamport-server/src/api" + ] +} \ No newline at end of file diff --git a/packages/payment-api/README.md b/packages/payment-api/README.md new file mode 100644 index 0000000..1ac96c5 --- /dev/null +++ b/packages/payment-api/README.md @@ -0,0 +1,38 @@ +# SDK Library +This is a SDK library generated by [`nestia`](https://nestia.io). + +With this SDK library, you can easily and safely interact with backend server. + +Just import and call some API functions like gif image below: + +![nestia-sdk-demo](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif) + +> Left is server code, and right is client code utilizing the SDK + + + + +# What [`Nestia`](https://nestia.io) is: +![Nestia Logo](https://nestia.io/logo.png) + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) +[![Downloads](https://img.shields.io/npm/dm/nestia.svg)](https://www.npmjs.com/package/nestia) +[![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) + +[Nestia](https://nestia.io) is a set of helper libraries for NestJS, supporting below features: + + - `@nestia/core`: super-fast decorators + - `@nestia/sdk` + - SDK generator for clients + - Swagger generator evolved than ever + - Automatic E2E test functions generator + - `nestia`: just CLI (command line interface) tool + +> **Note** +> +> - **Only one line** required, with pure TypeScript type +> - Runtime validator is **20,000x faster** than `class-validator` +> - JSON serialization is **200x faster** than `class-transformer` +> - SDK is similar with [tRPC](https://trpc.io), but much advanced \ No newline at end of file diff --git a/packages/payment-api/package.json b/packages/payment-api/package.json new file mode 100644 index 0000000..ba5ec3e --- /dev/null +++ b/packages/payment-api/package.json @@ -0,0 +1,39 @@ +{ + "name": "payment-api", + "version": "4.0.0-dev.20230920", + "description": "SDK library generated by Nestia", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "build": "npm run build:sdk && npm run compile", + "build:sdk": "rimraf ../payment-backend/src/api/functional && cd ../payment-backend && npx nestia sdk && cd ../payment-api", + "compile": "rimraf lib && tsc", + "deploy": "npm run build && npm publish", + "postinstall": "ts-patch install" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/nestia" + }, + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/nestia/issues" + }, + "homepage": "https://nestia.io", + "files": [ + "lib", + "package.json", + "README.md" + ], + "devDependencies": { + "rimraf": "^5.0.1", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", + "typescript": "^5.2.2" + }, + "dependencies": { + "@nestia/fetcher": "^2.0.4", + "typia": "^5.0.4" + } +} \ No newline at end of file diff --git a/packages/payment-api/tsconfig.json b/packages/payment-api/tsconfig.json new file mode 100644 index 0000000..31525c6 --- /dev/null +++ b/packages/payment-api/tsconfig.json @@ -0,0 +1,97 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "DOM", + "ES2015" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */// "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */// "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */// "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */// "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + "newLine": "lf", /* Set the newline character for emitting files. */// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. *//* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "plugins": [ + { + "transform": "typia/lib/transform" + } + ], + "strictNullChecks": true + }, + "include": [ + "../payment-backend/src/api" + ] +} \ No newline at end of file diff --git a/packages/payment-backend/.env b/packages/payment-backend/.env new file mode 100644 index 0000000..acdf5e1 --- /dev/null +++ b/packages/payment-backend/.env @@ -0,0 +1,26 @@ +MODE=local +API_PORT=37821 +UPDATOR_PORT=37820 +SYSTEM_PASSWORD=samchon + +POSTGRES_HOST=127.0.0.1 +POSTGRES_PORT=5432 +POSTGRES_DATABASE=payments +POSTGRES_SCHEMA=payments +POSTGRES_USERNAME=samchon +POSTGRES_USERNAME_READONLY=samchon_r +POSTGRES_PASSWORD=samchon +POSTGRES_URL=postgresql://${POSTGRES_USERNAME}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DATABASE}?schema=${POSTGRES_SCHEMA} + +API_ENCRYPTION_KEY=MCKOxv9B23r7EatArCFcBP03nfaS03T8 +API_ENCRYPTION_IV=9haeYD1tIf4v8xs7 +DB_HISTORY_ENCRYPTION_KEY=MTlsOESZ6geZTjeaubOdR71tobe0KqE7 +DB_HISTORY_ENCRYPTION_IV=7BqAGp1HPY9Ox66w +DB_RESERVATION_ENCRYPTION_KEY=0OrB5XVgkYpoybWeGIUkattMb4lTLQgz +DB_RESERVATION_ENCRYPTION_IV=ESgYCYV10Iy6qd1y +DB_CANCEL_HISTORY_ENCRYPTION_KEY=aU3y0PTXraB2SEYj1ACwtTYKxiYBvH4g +DB_CANCEL_HISTORY_ENCRYPTION_IV=fro2JXImhmbDtDNx + +IAMPORT_KEY=aaaaaa +IAMPORT_SECRET=bbbbb +TOSS_PAYMENTS_SECRET=test_ak_ZORzdMaqN3wQd5k6ygr5AkYXQGwy \ No newline at end of file diff --git a/packages/payment-backend/INFRASTRUCTURE.md b/packages/payment-backend/INFRASTRUCTURE.md new file mode 100644 index 0000000..f655120 --- /dev/null +++ b/packages/payment-backend/INFRASTRUCTURE.md @@ -0,0 +1,164 @@ +# INTRASTRUCTURE +## 1. DBMS +### 1.1. RDB Instance +[`samchon/payments`](https://github.com/samchon/payments) is using the `MariaDB@10.5` as its DBMS. + +Also, the accounts of the DBMS are separated to the `readonly` and `writable`. In the policy, `writable` account only can be used in automated program like the backend server. The developer or someone else need to connect to the DBMS directly, they're allowed to use only the `readonly` account. + +```sql +-- CREATE SCHEMA WITH STRICT MODE +CREATE SCHEMA test_db_schema DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +SET GLOBAL sql_mode = 'ANSI,TRADITIONAL'; + +-- WRITABLE ACCOUNT +CREATE USER writable_account; +SET password FOR writable_account = PASSWORD('Some Password'); +GRANT SELECT, + INSERT, + UPDATE, + DELETE, + CREATE, + DROP, + INDEX, + ALTER, + CREATE TEMPORARY TABLES, + CREATE VIEW, + EVENT, + TRIGGER, + SHOW VIEW, + CREATE ROUTINE, + ALTER ROUTINE, + EXECUTE +ON test_db_schema.* TO writable_account; + +-- READONLY ACCOUNT +CREATE USER readonly_account; +SET password FOR readonly_account = PASSWORD('Some Password'); +GRANT SELECT, EXECUTE ON test_db_schema.* TO readonly_account; + +-- FINALIZATION +FLUSH PRIVILEGES; +``` + +### 1.2. EC2 Instance +If you're planning to install the MariaDB on the EC2 instance, instead of the RDB instance, to reduce costs, you can install the MariaDB by inserting below commands. Of course, you should allow the MariaDB port number, `3306`. + +```bash +sudo apt-get install -y apt-transport-https +curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash + +sudo apt-get install software-properties-common +sudo add-apt-repository 'deb [arch=amd64,arm64,ppc64el] http://sfo1.mirrors.digitalocean.com/mariadb/repo/10.5/ubuntu bionic main' +sudo apt update + +sudo apt-get install -y mariadb-server +sudo mysql_secure_installation # Allow remote connection +sudo vi /etc/mysql/mariadb.conf.d/50-server.cnf # Disable bind-address +sudo service mysql restart +``` + +After the install, open the MariaDB terminal and create the new schema `test_db_schema`. Also, you must separate accounts of the MariaDB to `readonly` and `writable`. In the policy, `writable` account only can be used in automated program like the backend server. The developer or someone else need to connect to the DBMS directly, they're allowed to use only the `readonly` account. + +```sql +-- CREATE SCHEMA WITH STRICT MODE +CREATE SCHEMA test_db_schema + DEFAULT CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; +SET GLOBAL sql_mode = 'ANSI,TRADITIONAL'; + +-- WRITABLE ACCOUNT +CREATE USER writable_account; +SET password FOR writable_account = PASSWORD('Some Password'); +GRANT SELECT, + INSERT, + UPDATE, + DELETE, + CREATE, + DROP, + INDEX, + ALTER, + CREATE TEMPORARY TABLES, + CREATE VIEW, + EVENT, + TRIGGER, + SHOW VIEW, + CREATE ROUTINE, + ALTER ROUTINE, + EXECUTE +ON test_db_schema.* TO writable_account; + +-- READONLY ACCOUNT +CREATE USER readonly_account; +SET password FOR readonly_account = PASSWORD('Some Password'); +GRANT SELECT, EXECUTE ON bbs.* TO readonly_account; + +-- FINALIZATION +FLUSH PRIVILEGES; +``` + + + + +## 2. Backend Server +To open a backend server, you need to permit one or two ports. + +The first is the `37000` port, that is used by an non-distriptive distribution updator server. The updator server is opened only in the master instance, therefore you don't need to open the `37000` port, if the newly created EC2 instance is not the master server. + +The second is the `37001` port, that is used by the backend server. Therefore, you've open the `37001` port whether the newly created EC2 instance is master or not. + + - port numbers + - `37000` + - for non-distruptive distribution update system + - configure it only for the master instance + - `37001` + - for the backend server + - it must be configured in the every instances + +After opening the matched port(s), install the backend server and mount the server up. + +```bash +################################ +# PREPARE ASSETS +################################ +# CHANGE TIMEZONE +sudo timedatectl set-timezone Asia/Seoul + +# INSTALL NODE +sudo apt-get update +sudo apt-get install curl +curl -sL https://deb.nodesource.com/setup_14.x | sudo bash - + +# INSTALL COMPILERS +sudo apt-get -y install git +sudo apt-get -y install nodejs +sudo apt-get -y install npm + +# CONFIGURATION +sudo sysctl net.core.somaxconn=2048 + +################################ +# BUILD PROJECT +################################ +# CLONE REPOSITORY +git config --global credential.helper store +git clone https://github.com/samchon/payments +cd ${PROJECT} + +# INSTALL PROJECT +npm install +npm run build + +################################ +# MOUNT SERVER +################################ +# ONLY WHEN MASTER INSTANCE +npm run start:updator:master + +# ONLY WHEN SLAVE INSTANCE +npm run start:updator:slave real + +# START SERVER - USE ONE OF BELOW +npm run start dev +npm run start real +npm run start real master +``` \ No newline at end of file diff --git a/packages/payment-backend/LICENSE b/packages/payment-backend/LICENSE new file mode 100644 index 0000000..a686d45 --- /dev/null +++ b/packages/payment-backend/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Samchon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/payment-backend/README.md b/packages/payment-backend/README.md new file mode 100644 index 0000000..27c70b4 --- /dev/null +++ b/packages/payment-backend/README.md @@ -0,0 +1,888 @@ +# Payments Server +## 1. Outline +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/payments/blob/master/LICENSE) +[![npm version](https://badge.fury.io/js/payment-api.svg)](https://www.npmjs.com/package/payment-api) +[![Downloads](https://img.shields.io/npm/dm/payment-api.svg)](https://www.npmjs.com/package/payment-api) +[![Build Status](https://github.com/samchon/payments/workflows/build/badge.svg)](https://github.com/samchon/payments/actions?query=workflow%3Abuild) + +`payment-backend` 는 통합 결제 서버를 구현한 프로젝트이다. + +여기서 말하는 통합 결제란, [아임포트](https://github.com/samchon/fake-iamport-server)나 [토스 페이먼츠](https://github.com/samchon/fake-toss-payments-server) 등, 여러 PG 사들을 일괄 관리할 수 있다는 뜻이다. 더하여 `payment-backend` 는 MSA (Micro Service Architecture) 를 고려하여 설계된 프로젝트로써, 귀하의 서비스 중 결제 부문만을 따로이 분리하여 관리할 수 있다. + +또한 `payment-backend` 가 연동하게 되는 결제 PG 사들은 본디 프론트 어플리케이션과 연동한 수기 테스트가 필요하다. 이 때문에 이들 결제 PG 사들과 연동해야 하는 결제 서버들은, 테스트 자동화 프로그램을 작성할 수 없기에, 필연적으로 테스트 커버리지가 낮아 매우 불안정해진다. 하지만 `payment-backend` 는 결제 PG 사들의 API 를 흉내낸 가짜 PG 서버들을 구현, 이들을 통하여 테스트 자동화 프로그램을 구성함으로써 안정성을 담보한다. + + - [samchon/fake-iamport-server](https://github.com/samchon/fake-iamport-server) + - [samchon/fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) + +더불어 `payment-backend` 는 `payments-api` 라 하여, 통합 결제 서버와 연동할 수 있는 SDK 라이브러리를 제공한다. 귀하는 이 `payments-api` 를 통하여, 통합 결제 서버와 매우 손쉽게 연동할 수 있고, 이를 통하여 결제 부문에 관련된 MSA (Micro Service Architecture) 를 매우 안전하게 구성할 수 있다. + +그리고 만일 귀하가 `payment-backend` 와의 연동을, 제공되는 SDK 를 활용하는 것이 아닌 API 스펙을 보고 직접 구현하고자 한다면, 반드시 알아두어야 할 것이 하나 있다. 그것은 바로 `payment-backend` 가 모든 request 및 response body 에 적재하는 JSON 데이터를, 보안 강화를 위하여, AES 알고리즘으로 암호화한다는 것이다. + + - 서버 접속 정보 + - Host 주소 + - 로컬 서버: http://localhost:37821 + - Dev 서버: https://YOUR-DEV-SERVER + - Real 서버: https://YOUR-REAL-SERVER + - 프로토콜: HTTP/S + - Request/Response Body: Encrypted JSON + - AES-128/256 + - key: `8zXaXlMEP23fGcDZigh3524gxTr1IAuD` + - iv: `9pMoEdAjHusokpUg` + - CBC mode + - PKCS #5 Padding + - Base64 Encoding + - 매뉴얼 + - 자료구조 매뉴얼: [src/api/structures/IPaymentHistory.ts](https://github.com/samchon/payments/blob/HEAD/src/api/structures/IPaymentHistory.ts) + - API 함수 매뉴얼: [src/api/functional/histories/index.ts](https://github.com/samchon/payments/blob/HEAD/src/api/functional/histories/index.ts) + - 예제 코드 + - 아임포트 + - 결제 기록하기: [test_fake_iamport_payment_history.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_history.ts) + - 간편 결제 등록하기: [test_fake_iamport_payment_reservation.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_reservation.ts) + - 웹훅 이벤트 리스닝: [test_fake_iamport_payment_webhook.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_webhook.ts) + - 토스 페이먼츠 + - 결제 기록하기: [test_fake_toss_payment_history.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_history.ts) + - 간편 결제 등록하기: [test_fake_toss_payment_reservation.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_reservation.ts) + - 웹훅 이벤트 리스닝: [test_fake_toss_payment_webhook.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_webhook.ts) + +```typescript +import { v4 } from "uuid"; + +import imp from "iamport-server-api"; +import payments from "payment-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IPaymentHistory } from "payment-api/lib/structures/IPaymentHistory"; + +import { IamportAsset } from "../../../../services/iamport/IamportAsset"; + +export async function test_fake_iamport_payment_history + (connection: payments.IConnection): Promise +{ + // 주문 정보 + const yourOrderId: string = v4(); // 귀하의 백엔드 서버가 발행한 주문 ID + const yourOrderPrice: number = 12_000; // 주문 금액 + + // 아임포트 카드 결제 시뮬레이션 + 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 주문" + } + ); + + // 결제 이력 등록하기 + const history: IPaymentHistory = await payments.functional.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: "https://github.com/samchon", + price: yourOrderPrice, + password: "some-password", + } + ); + + // 결제 내역 조회하기 + const read: IPaymentHistory = await payments.functional.histories.at + ( + connection, + history.id, + { + password: "some-password" + } + ); + + // if condition 을 이용한 자동 다운 캐스팅 + if (read.vendor_code === "iamport") + read.data.imp_uid; + return read.data; +} +``` + + + + +## 2. Installation +### 2.1. NodeJS +본 서버 프로그램은 TypeScript 로 만들어졌으며, NodeJS 에서 구동된다. + +고로 제일 먼저 할 일은, NodeJS 를 설치하는 것이다. 아래 링크를 열어, NodeJS 프로그램을 다운로드 받은 후 즉각 설치하기 바란다. 참고로 NodeJS 버전은 어지간히 낮은 옛 시대의 버전만 아니면 되니, 구태여 latest 버전을 설치할 필요는 없으며, stable 버전만으로도 충분하다. + + - https://nodejs.org/en/ + +### 2.2. PostgreSQL +본 서버는 PostgreSQL 을 사용하고 있다. + +따라서 로컬에서 DB 서버를 개발하고 구동하려거든, PostgreSQL 이 반드시 설치되어있어야 한다. 아래 링크를 참조하여, PostgreSQL 14 버전을 설치할 것. 단, PostgreSQL 을 설치하면서, StackBuilder 와 PostGIS 도 함께 설치해줘야 한다. + + - https://www.enterprisedb.com/downloads/postgres-postgresql-downloads + - https://postgis.net/workshops/postgis-intro/installation.html + +![PostgreSQL installer](assets/images/postgresql-installer.png) + +![StackBuilder installer](assets/images/stackbuilder-installer.png) + +그리고 만일 개발 환경이 윈도우라면, 환경변수 PATH 에 PostgreSQL 이 설치된 경로의 `bin` 폴더를 추가해준다. 아마도 그 경로는 `C:\Program Files\PostgreSQL\14\bin` 일 것이다. 맥북의 경우에는 `/Applications/Postgres.app/Contents/MacOS/bin` 이다. + +이후 PostgreSQL 터미널로 접속, `samchon_test` 와 `payments` 스키마를 각각 생성해준다. 그리고 `samchon_w` 와 `samchon_r` 계정을 생성하여, 각각 쓰기 및 읽기 권한을 부여한다. + +만일 로컬 PostgreSQL 의 계정을 `postgres`, 그리고 비밀번호를 `root` 로 설정하였다면, `npm run schema` 명령어로 아래 SQL 스크립트를 대체할 수 있다. + +```sql +-- CREATE USER +CREATE ROLE samchon_w WITH ENCRYPTED PASSWORD 'https://github.com/samchon'; +GRANT samchon_w TO postgres; + +-- CREATE DB & SCHEMA +CREATE DATABASE samchon_test OWNER samchon_w; +\connect samchon_test; +CREATE SCHEMA payments AUTHORIZATION samchon_w; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA payments TO samchon_w; + +-- READABLE ACCOUNT +CREATE USER samchon_r WITH ENCRYPTED PASSWORD 'https://github.com/samchon'; +GRANT CONNECT ON DATABASE samchon_test TO samchon_r; +GRANT USAGE ON SCHEMA payments TO samchon_r; +GRANT SELECT ON ALL TABLES IN SCHEMA payments TO samchon_r; +``` + +### 2.3. Server +NodeJS 및 PostgreSQL 의 설치가 끝났다면, 바로 `payment-backend` 구동을 시작할 수 있다. + +제일 먼저 `git clone` 을 통하여, 결제 서버 프로젝트를 로컬 저장소에 복사하도록 한다. 그리고 해당 폴더로 이동하여 `npm install` 명령어를 실행함으로써, 통합 결제 서버를 구동하는 데 필요한 라이브러리들을 다운로드 한다. 그리고 `npm run build` 명령어를 입력하여, 결제 서버의 소스 코드를 컴파일한다. 마지막으로 `npm run start` 명령어를 실행해주면, 결제 서버가 구동된다. + +다만 `payment-backend` 를 구동하기 전, 각각 [PaymentConfiguration](https://github.com/samchon/payments/blob/HEAD/src/PaymentConfiguration.ts) 과 [PaymentGlobal](https://github.com/samchon/payments/blob/HEAD/src/PaymentGlobal.ts) 클래스에 어떠한 속성들이 있는지 꼼꼼히 읽어보고, 귀하의 서비스에 알맞는 설정을 해 주도록 한다. + +```bash +# CLONE REPOSITORY +git clone https://github.com/samchon/payments +cd payments + +# INSTALLATION & COMPILATION +npm install +npm run build + +# START SERVER & STOP SERVER +npm run start +npm run stop +``` + +[![npm version](https://badge.fury.io/js/payment-backend.svg)](https://www.npmjs.com/package/payment-backend) +[![Downloads](https://img.shields.io/npm/dm/payment-backend.svg)](https://www.npmjs.com/package/payment-backend) + +더하여 `payment-backend` 는 npm 모듈로 설치하여 `import` 할 수 있다. + +이러한 방식은 `payment-backend` 와 연동하는 백엔드 서버를 개발할 때 특히 유용하다. 해당 백엔드 서버의 안정성을 상시 보증하기 위하여 테스트 자동화 프로그램을 개발할 때, 테스트 자동화 프로그램에서 `paments-server` 의 설정과 개설 및 폐쇄를 완전히 통제할 수 있기 때문이다. + +따라서 귀하의 백엔드 서버가 TypeScript 내지 JavaScript 를 사용한다면, 테스트 자동화 프로그램을 구성함에 있어 github 저장소를 clone 하고 `payment-backend` 를 별도 구동하기보다, 귀하의 백엔드 서버 테스트 프로그램에서 `payment-backend` 모듈을 `import` 후 그것의 개설과 폐쇄를 직접 통제하는 것을 권장한다. + +그리고 이렇게 테스트 자동화 프로그램으로 `payment-backend` 를 `import` 하여 사용할 때 역시, 각각 [PaymentConfiguration](https://github.com/samchon/payments/blob/HEAD/src/PaymentConfiguration.ts) 과 [PaymentGlobal](https://github.com/samchon/payments/blob/HEAD/src/PaymentGlobal.ts) 클래스에 어떠한 속성들이 있는지 꼼꼼히 읽어보고, 귀하의 서비스에 알맞는 설정을 해 주도록 한다. + +```typescript +// npm install --save-dev payment-backend +import payments from "payment-backend"; + +async function main(): Promise +{ + // UPDATOR SERVER OPENING + const updator = await payments.PaymentUpdator.master(); + + // BACKEND OPENING + const backend: payments.PaymentBackend = new payments.PaymentBackend(); + await backend.open(); + + // CLOSING + await backend.close(); + await updator.close(); +} +``` + +### 2.4. SDK +[![npm version](https://badge.fury.io/js/payment-api.svg)](https://www.npmjs.com/package/payment-api) +[![Downloads](https://img.shields.io/npm/dm/payment-api.svg)](https://www.npmjs.com/package/payment-api) + +본 프로젝트 `payment-backend` 는 연동을 위한 SDK 라이브러리를 제공한다. + +귀하는 이 `payment-api` 를 통하여, 통합 결제 서버와 매우 손쉽게 연동할 수 있고, 이를 통하여 결제 부문에 관련된 MSA (Micro Service Architecture) 를 매우 안전하게 구성할 수 있다. + +`npm install --save payment-api` 명령어를 통하여 통합 결제와의 연동을 위한 SDK 라이브러리를 설치한 후, 아래 매뉴얼 및 예제 코드를 참고하여 귀하의 백엔드 서비스가 필요로 하는 결제 기능을 개발하도록 한다. + + - 서버 접속 정보 + - Host 주소 + - 로컬 서버: http://localhost:37821 + - Dev 서버: https://YOUR-DEV-SERVER + - Real 서버: https://YOUR-REAL-SERVER + - 프로토콜: HTTP/S + - Request/Response Body: Encrypted JSON + - AES-128/256 + - CBC mode + - PKCS #5 Padding + - Base64 Encoding + - 매뉴얼 + - 자료구조 매뉴얼: [src/api/structures/IPaymentHistory.ts](https://github.com/samchon/payments/blob/HEAD/src/api/structures/IPaymentHistory.ts) + - API 함수 매뉴얼: [src/api/functional/histories/index.ts](https://github.com/samchon/payments/blob/HEAD/src/api/functional/histories/index.ts) + - 예제 코드 + - 아임포트 + - 결제 기록하기: [test_fake_iamport_payment_history.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_history.ts) + - 간편 결제 등록하기: [test_fake_iamport_payment_reservation.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_reservation.ts) + - 웹훅 이벤트 리스닝: [test_fake_iamport_payment_webhook.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_webhook.ts) + - 토스 페이먼츠 + - 결제 기록하기: [test_fake_toss_payment_history.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_history.ts) + - 간편 결제 등록하기: [test_fake_toss_payment_reservation.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_reservation.ts) + - 웹훅 이벤트 리스닝: [test_fake_toss_payment_webhook.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_webhook.ts) + +```typescript +import { v4 } from "uuid"; + +import payments from "payment-api"; +import toss from "toss-payments-server-api"; +import { IPaymentReservation } from "payment-api/lib/structures/IPaymentReservation"; +import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; + +import { TossAsset } from "../../../../services/toss/TossAsset"; + +export async function test_fake_toss_payment_reservation + (connection: payments.IConnection): Promise +{ + const yourSourceId: string = v4(); // 귀하의 백엔드 서버가 발행한 식별자 ID. + + // 토스 페이먼츠 간편 결제 카드 등록 시뮬레이션 + const billing: ITossBilling = + await toss.functional.v1.billing.authorizations.card.store + ( + TossAsset.connection("test-iamport-store-id"), + { + customerKey: yourSourceId, + cardNumber: "1111222233334444", + cardExpirationYear: "28", + cardExpirationMonth: "03", + cardPassword: "99", + customerBirthday: "880311", + consumerName: "남정호" + } + ); + + // 간편 결제 수단 등록하기 + const reservation: IPaymentReservation = + await payments.functional.reservations.store + ( + connection, + { + vendor: { + code: "toss.payments", + store_id: "test-iamport-store-id", + uid: billing.billingKey, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourSourceId, + }, + title: "some-title", + password: "some-password" + } + ); + + // 간편 결제 등록 수단 조회하기 + const read: IPaymentReservation = await payments.functional.reservations.at + ( + connection, + reservation.id, + { + password: "some-password" + } + ); + + // if condition 을 이용한 자동 다운 캐스팅 + if (read.vendor_code === "toss.payments") + read.data.billingKey; + return read.data; +} +``` + + + + +## 3. Development +### 3.1. Definition +백엔드 서버에 새 API 를 추가하고 기능을 변경하는 일 따위는 물론, API 컨트롤러, 즉 [src/controllers](src/controllers) 의 코드를 수정함으로써 이루어진다. 하지만 `payment-backend` 는 신규 API 가 필요하거나 혹은 기존 API 의 변경 필요할 때, 대뜸 [Main Program](#33-main-program) 의 코드부터 작성하고 보는 것을 매우 지양한다. 그 대신 `payment-backend` 는 API 의 인터페이스만을 먼저 정의하고, [Main Program](#33-main-program) 의 구현은 나중으로 미루는 것을 지향한다. + +따라서 `payment-backend` 에 새 API 를 추가하려거든, [src/controllers](src/controllers) 에 새 API 의 인터페이스만을 먼저 정의해준다. 곧이어 `npm run build:api` 명령어를 통하여, API Library 를 빌드한다. 경우에 따라서는 서비스 서버와의 동시 개발을 위하여, 새로이 빌드된 SDK 를 그대로 `npm run package:api` 해 버려도 좋다. + +이후 로컬에서 새로이 생성된 SDK 와 해당 API 를 이용, 유즈케이스 시나리오를 테스트 자동화 프로그램으로 작성한다. 이후 Main Program 을 제작하며, 앞서 작성해 둔 테스트 자동화 프로그램으로 상시 검증한다. 마지막으로 Main Program 까지 완성되면 이를 배포하면 된다. + +이하 `payment-backend` 의 개략적인 개발 순서를 요약하면 아래와 같다. + + - API Interface Definition + - API Library (SDK) 빌드 + - Test Automation Program 제작 + - Main Program 제작 및 테스트 자동화 프로그램을 이용한 상시 검증 + - DEV 및 REAL 서버에 배포 + +### 3.2. Test Automation Program +[![Build Status](https://github.com/samchon/payments/workflows/build/badge.svg)](https://github.com/samchon/payments/actions?query=workflow%3Abuild) + +```bash +npm run test +``` + +새로이 개발할 [API 인터페이스 정의](#31-api-interface-definition)를 마쳤다면, 그 다음에 할 일은 바로 해당 API 에 대한 유즈케이스 시나리오를 세우고 이를 테스트 자동화 프로그램을 만들어, 향후 [Main Program](#33-main-program) 제작시 이를 상시 검증할 수 있는 수단을 구비해두는 것이다 - TDD (Test Driven Development). + +그리고 본 프로젝트는 `npm run test` 라는 명령어를 통하여, 서버 프로그램의 일체 기능 및 정책 등에 대하여 검증할 수 있는, 테스트 자동화 프로그램을 구동해 볼 수 있다. 더불어 테스트 자동화 프로그램은 순수하게 `payment-backend` 의 메인 서버 프로그램 뿐 아니라, 통합 결제 서버와 연동하는 다양한 외부 PG 사 시스템들도, 가상으로 구동하게 된다. + + - [fake-iamport-payments-server](https://github.com/samchon/fake-iamport-payments-server) + - [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) + +그리고 만약 새 테스트 로직을 추가하고 싶다면, [src/test/features](src/test/features) 내 적당한 위치에 새 `ts` 파일을 하나 만들고, `test_` 로 시작하는 함수를 하나 만들어 그 안에 테스트 로직을 작성한 후, 이를 `export` 심벌을 이용하여 배출해주면 된다. 이에 대한 자세한 내용은 [src/test/features](src/test/features) 폴더에 들어있는 모든 `ts` 파일 하나 하나가 다 좋은 예제 격이니, 이를 참고하도록 한다. + +```typescript +import { v4 } from "uuid"; +import { sleep_for } from "tstl/thread/global"; + +import imp from "iamport-server-api"; +import payments from "../../../../api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IPaymentHistory } from "../../../../api/structures/IPaymentHistory"; +import { IPaymentWebhook } from "../../../../api/structures/IPaymentWebhook"; + +import { FakePaymentStorage } from "../../../../providers/FakePaymentStorage"; +import { IamportAsset } from "../../../../services/iamport/IamportAsset"; +import { PaymentConfiguration } from "../../../../PaymentConfiguration"; + +export async function test_fake_iamport_payment_webhook + (connection: payments.IConnection): Promise +{ + const yourOrderId: string = v4(); // 귀하의 서비스가 발행한 주문 ID. + const yourOrderPrice: number = 19_900; // 주문 금액 + + // 아임포트 가상 계좌 결제 시뮬레이션 + 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: "남정호" + } + ); + + // 웹훅 URL 설정하기. + const webhook_url: string = "http://127.0.0.1:" + + PaymentConfiguration.API_PORT + + payments.functional.internal.webhook.PATH; + + // 결제 이력 등록하기 + const history: IPaymentHistory = await payments.functional.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", + } + ); + + // 가상 계좌 입금 시뮬레이션 + await imp.functional.internal.deposit + ( + await IamportAsset.connection("test-iamport-store-id"), + payment.response.imp_uid + ); + + // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. + await sleep_for(100); + + // 웹훅 이벤트 리스닝 시뮬레이션 + const webhook: IPaymentWebhook = FakePaymentStorage.webhooks.back(); + if (webhook.current.id !== history.id) + throw new Error("Bug on PaymentWebhooksController.iamport(): failed to deliver the webhook event."); + else if (webhook.previous.paid_at !== null) + throw new Error("Bug on PaymentWebhookProvider.process(): failed to delivery the exact previous data."); + else if (webhook.current.paid_at === null) + throw new Error("Bug on PaymentWebhookProvider.process(): failed to delivery the exact current data."); + + // 웹훅 데이터 삭제 + FakePaymentStorage.webhooks.pop_back(); +} +``` + +### 3.3. Main Program +[API 인터페이스를 정의](#31-api-interface-definition)하고 그에 관련된 [테스트 자동화 프로그램](#32-test-automation-program)을 제작하였다면, 마지막으로 남은 일은 바로 서버의 메인 프로그램을 작성, 해당 API 를 완성하는 것이다. 앞서 정의한 [API 인터페이스](#31-api-interface-definition) 메서드 내에, 상세 구현 코드를 작성하고, 이를 [테스트 자동화 프로그램](#32-test-automation-program)을 통하여 상시 검증하도록 하자. + +단, 모든 소스 코드를 전부 API 컨트롤러의 메서드에 작성하는 우는 범하지 않기를 바란다. API 컨트롤러는 단지 매개체 + a 의 역할만을 해야 할 뿐이며, 주 소스 코드는 [src](src) 폴더 내 각 폴더의 분류에 따라 알맞게 나뉘어 작성되어야 한다. 특히, DB 를 통한 데이터 입출력에 관해서는 가급 [src/providers](src/providers) 를 경유하도록 할 것. + +더하여 통합 결제 서버의 설정 정보는 모두 [src/PaymentConfiguration.ts](src/PaymentConfiguration.ts) 에 몰아두었으니, 이 설정 정보들을 귀하의 서비스에 알맞게 수정하는 것 또한 잊지 말기 바란다. + +### 3.4. Encryption +모든 데이터는 암호화되어 전송되거나 저장된다. + + - 암호화 방식 + - AES-128/256 + - CBC mode + - PKCS #5 Padding + - Base64 Encoding + +본 통합 결제 서버 `payment-backend` 는 보안을 강화하기 위하여, http 프로토콜로 전송되는 모든 `body` 데이터를 암호화한다 이는 `request body` 와 `response body` 양쪽 모두 해당되는 이야기이며, 설사 http 대신 https 프로토콜을 사용한다 하더라도 예외는 없다. + +더하여 `payment-backend` 는 결제를 비롯한 모든 민감 데이터들을 암호화하여 저장하고 있다. 또한, 각 암호화 항목마다 각기 다른 secret key 및 initialization vector 를 사용함으로써, 보안을 한층 더 강화하고 있다. 그리고 이러한 민감 데이터들은 일괄 조회가 불가능하며, 오직 개별 단위의 조회만 가능하다. 이 개별 단위의 조회조차, 해당 레코드의 비밀번호를 모르면 일절 조회할 수 없다. + +`payment-backend` 에는 이처럼 보안 강화를 위한 강력한 암호화 정책들이 존재한다. 혹여 귀하가 본 `payment-backend` 를 확장하여 몇 가지 기능을 더 개발한다 하더라도, 이러한 암호화 원칙들은 부디 지켜주었으면 한다. + + + + +## 4. Deploy +### 4.1. Non-distruptive Update System +만일 귀하가 통합 결제 서버 `payment-backend` 의 코드를 수정하고 이를 커밋하였다면, 귀하는 이를 기존의 서버 인스턴스를 종료하는 일 없이, 무중단 업데이트를 수행할 수 있다. `npm run update` 명령어를 입력함으로써, 이러한 무중단 업데이트는 실행된다. + + - Pull new commit + - Build the new soure code + - Restart the backend server without distruption + +이러한 무중단 업데이트를 달성하기 위해서는, 서버 인스턴스는 메인 백엔드 서버 프로그램을 시작하기 전, 업데이트 프로그램을 실행해 줄 필요가 있다. 만일 귀하의 서버 인스턴스가 ELB (Elastic Loader Balancer) 등을 통하여 여러 대로 구성되어있고, 현재의 인스턴스가 슬레이브라면, `npm run start:updator:slave` 명령어를 실행해주면 된다. + +반면 현재가 마스터 인스턴스라면, `npm run start:updator:master` 명령어를 실행하도록 한다. + +```bash +#---- +# RUN UPDATOR PROGRAM +#---- +# THE INSTANCE IS MASTER +npm run start:updator:master + +# THE INSTANCE IS SLAVE +npm run start:updator:slave + +#---- +# MOUNT THE BACKEND SERVER UP +#---- +npm run start real +``` + +### 4.2. Local Server +간혹 로컬에, [테스트 자동화 프로그램](#33-test-automation-program)이 아닌, `payment-backend` 그 자체를 구동해야 할 때가 있다. 이럴 때는 아래와 같이 `npm run start local` 명령어를 입력해주면, 로컬에 `payment-backend` 서버를 개설할 수 있다. 그리고 실행된 서버를 종료하려거든, `npm run stop` 명령어를 입력해주면 된다. + +```bash +npm run start local +npm run stop +``` + +또한, 로컬 개발 환경에서의 무중단 업데이트가 얼마나 의미가 있겠냐만은, 어쨋든 `payment-backend` 는 로컬 환경에서도 무중단 업데이트라는 것을 할 수 있다. 아래와 같이 로컬 서버를 구동하기 전 `npm run start updator:master` 명령어를 통하여 업데이트 관리자 프로그램을 구동하고, 향후 무중단 업데이트가 필요할 때마다 `npm run update local` 명령어를 입력해주면 된다. + +```bash +# START THE LOCAL BACKEND SERVER WITH UPDATOR PROGRAM +npm run start updator:master +npm run start local + +# UPDATE THE LOCAL SERVER WITHOUT DISTRUPTION +npm run update local +``` + +### 4.3. Dev Server +Dev 서버를 업데이트하는 것은 매우 간단하다. 그저 소스 코드를 `dev` 브랜치에 커밋한 후, 로컬 개발환경에서 `npm run update dev` 명령어를 입력해주면 끝이다. 이로써 Dev 서버의 소스 코드는 가장 최신의 것으로 바뀌며, 동시에 무중단 업데이트가 실행되어 이것이 서버 API 에 적용될 뿐이다. + +```bash +npm run update dev +``` + +다만 dev 서버의 경우, PostgreSQL 이 별도의 RDS 로 구성된 게 아닌, `payment-backend` 가 설치되고 가동되는 EC2 인스턴스에 함께 설치되기도 한다. 그리고 dev 서버는 로컬 서버와 마찬가지로 테스트 용도를 위하여 개설된 목적인 바, 경우에 따라 DB 를 초기화하고 재 구성해야 하는 경우 또한 생기기 마련이다. + +이 경우, 아래와 같이 `npm run ssh:dev` 명령어를 입력하여 dev 서버로 접속한 후, `npm run reset:dev` 명령어를 입력해주면 된다. 이 명령어는 dev 서버의 소스코드를 가장 최신의 것으로 변경한 후, `payment-backend` 의 백엔드 및 업데이트 서버를 종료하고, 테스트 프로그램을 가동함으로써 DB 를 초기화하고 필수 및 샘플 데이터를 재 구성한 후, 종료된 `payment-backend` 의 백엔드와 업데이트 서버를 재 시작해주는 역할을 한다. + +```bash +# 다음 두 명령어로 리셋 가능 +npm run ssh:dev +npm run reset:dev + +# 참고사항 - npm run reset:dev 를 구성하는 명령어 셋 +git pull +npm install +npm run build +pm2 stop all +npm run test -- --mode=dev +npm run start:updator:master +npm run start dev +``` + +더하여 `payment-backend` 를 개발하다보면, 문득 현재 가동 중인 `payment-backend` 서버의 정보가 이리저리 궁금해질 수 있다. 가령 현재 가동 중인 dev 서버가 사용 중인 소스 코드가 무엇인지 알고 싶어, 해당 서버가 사용 중인 소스 코드의 commit 에 대한 hash code 를 알고싶을 수도 있는 법이다. + +이 때는 망설이지 말고 바로 아래와 같이, `npm run monitor dev` 명령을 수행해주면, 바로 현재의 dev 서버에 대한 각종 정보를 취득할 수 있다. 취득할 수 있는 정보는 아래와 같이 대분류 주제로는 두 가지, 그리고 소분류로는 다섯 가지가 있다. + + - 퍼포먼스 정보: [IPerformance](src/api/structures/monitors/IPerformance.ts) + - CPU 사용량 + - 메모리 사용량 + - 리소스 사용량 + - 시스템 정보: [ISystem](src/api/structures/monitors/ISystem.ts) + - 커밋 정보: 현 서버가 사용 중인 소스 코드의 커밋에 관한 정보 + - 패키지 정보: `package.json` + - 기타 서버 개설 일시 정보 등 + +``` +npm run monitor dev +``` + +### 4.4. Real Server +Real 서버를 업데이트하는 일 또한 [dev](#43-dev-server) 서버 때와 마찬가지로 매우 간단하다. 그저 편집한 소스 코드를 `master` 브랜치에 커밋하고, 로컬 개발 환경에서 `npm run update real` 명령어를 실행함으로써, 마스터 서버가 스스로 무중단 업데이트를 수행하도록 할 수 있다. + +```bash +npm run update real +``` + +또한 master 서버에 대하여도, 아래 명령어를 통하여, 각종 정보를 취득할 수 있다. + +```bash +npm run monitor master +``` + + + + +## 5. Appendix +### 5.1. API Documents +본 통합 결제 서버 `payment-backend` 는 TypeScript 로 제작되었으며, 이를 이용하는 클라이언트 서버 또한 TypeScript 내지 JavaScript 로 개발하는 것을 가정하였기에, SDK 연동 라이브러리 `payment-api` 를 제공하는 것으로 연동 가이드를 마치고 있다. + +하지만 `payment-backend` 를 사용하는 클라이언트 서버가 반드시 TypeScript 내지 JavaScript 만으로 개발한다는 보장은 없는 법, 이러한 경우를 위해 별도의 대책을 마련해두었다. 그것은 바로 연동 라이브러리 `payment-api` 의 명세를 직접 읽어보는 것이다. + +`payment-api` 의 제작에 쓰인 [nestia](https://github.com/samchon/nestia) 는 SDK 연동 라이브러리를 빌드하면서, 해당 SDK 라이브러리 코드가 상당 수준의 API 문서 역할을 할 수 있도록, API 명세 및 상세 설명 내역을 깔끔하게 정리하여 보여준다. 아래 예제 코드는 그러한 예시 중 하나이다. + +이외에 `payment-backend` 는 앞서 [3.4. Encryption](#34-encryption) 단원에서 설명했듯, http(s) 프로토콜을 사용하되 요청 및 응답 `body` 를 `AES-PKCS-5` 알고리즘으로 한 번 더 암호화한다. 아래는 암호화 알고리즘의 상세 정보이니, 이 또한 참고하기 바란다. + + - 서버 접속 정보 + - Host 주소 + - 로컬 서버: http://localhost:37821 + - Dev 서버: https://YOUR-DEV-SERVER + - Real 서버: https://YOUR-REAL-SERVER + - 프로토콜: HTTP/S + - Request/Response Body: Encrypted JSON + - AES-128/256 + - key: `GwRKWmITTfWQVzyOJNXUzXflhOa4EWaS` + - iv: `2gbpEmFga729nqo2` + - CBC mode + - PKCS #5 Padding + - Base64 Encoding + - 매뉴얼 + - 자료구조 매뉴얼: [src/api/structures/IPaymentHistory.ts](https://github.com/samchon/payments/blob/HEAD/src/api/structures/IPaymentHistory.ts) + - API 함수 매뉴얼: [src/api/functional/histories/index.ts](https://github.com/samchon/payments/blob/HEAD/src/api/functional/histories/index.ts) + - 예제 코드 + - 아임포트 + - 결제 기록하기: [test_fake_iamport_payment_history.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_history.ts) + - 간편 결제 등록하기: [test_fake_iamport_payment_reservation.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_reservation.ts) + - 웹훅 이벤트 리스닝: [test_fake_iamport_payment_webhook.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_iamport_payment_webhook.ts) + - 토스 페이먼츠 + - 결제 기록하기: [test_fake_toss_payment_history.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_history.ts) + - 간편 결제 등록하기: [test_fake_toss_payment_reservation.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_reservation.ts) + - 웹훅 이벤트 리스닝: [test_fake_toss_payment_webhook.ts](https://github.com/samchon/payments/blob/HEAD/src/test/features/fake/examples/test_fake_toss_payment_webhook.ts) + +```typescript +/** + * 결제 내역 상세 조회하기. + * + * @param connection connection Information of the remote HTTP(s) server with headers (+encryption password) + * @param id Primary Key + * @param input 결제 내역의 비밀번호 + * @returns 결제 내역 + * + * @nestia Generated by Nestia - https://github.com/samchon/nestia + * @controller PaymentHistoriesController.at() + * @path PATCH /histories/:id + */ +export function at + ( + connection: IConnection, + id: string, + input: Primitive + ): Promise +{ + return Fetcher.fetch(connection, at.CONFIG, at.METHOD, at.path(id), input); +} +export namespace at +{ + export type Input = Primitive; + export type Output = Primitive; + + export const METHOD = "PATCH"; + export const PATH = "/histories/:id"; + export const CONFIG = { + input_encrypted: true, + output_encrypted: true, + }; + + export function path(id: string): string + { + return `/histories/${id}`; + } +} +``` + +### 5.2. NPM Run Commands +현재 package.json 에 정의된 run command 의 역할은 다음과 같다. + + - `build`: 통합 결제 서버 소스 컴파일 + - `build:api`: SDK 연동 라이브러리 컴파일 + - `dev`: 소스 incremental 컴파일 + - `monitor`: 서버의 정보 취득 (`npm run monitor dev`, `npm run monitor real`) + - `package:api`: SDK 연동 라이브러리 배포 + - `reset:local`: 로컬 DB 리셋 + - `reset:dev`: Dev 서버 종료 후 DB 리셋하여 재시작 + - `ssh:dev`: Dev 서버 터미널 접속 + - `ssh:real`: Real 서버 터미널 접속 + - `start`: 백엔드 서버 가동 (`npm run start dev`, `npm run start real`) + - `start:updator:master`: 무중단 업데이트 시스템 master 버전 실행 + - `start:updator:slave`: 무중단 업데이트 시스템 slave 버전 실행 + - `start:reload`: 백엔드 서버 재시작, 주로 무중단 업데이트 프로그램에서 사용됨 + - `stop`: 백엔드 서버 중단 + - `stop:updator:master`: 무중단 업데이트 시스템 master 버전 중단 + - `stop:updator:slave`: 무중단 업데이트 시스템 slave 버전 중단 + - `update`: 무중단 업데이트 실행 (`npm run update dev`, `npm run update real`) + - `test`: 테스트 자동화 프로그램 가동 + - `test:update`: 무중단 업데이트 시스템이 잘 구현되었는 지 검증해 봄 + +### 5.3. Dependencies +#### 5.3.1. Typia +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/typia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/typia.svg)](https://www.npmjs.com/package/typia) +[![Downloads](https://img.shields.io/npm/dm/typia.svg)](https://www.npmjs.com/package/typia) +[![Build Status](https://github.com/samchon/typia/workflows/build/badge.svg)](https://github.com/samchon/typia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/wiki-documentation-forestgreen)](https://github.com/samchon/typia/wiki) + +```typescript +// RUNTIME VALIDATORS +export function is(input: unknown | T): input is T; // returns boolean +export function assert(input: unknown | T): T; // throws TypeGuardError +export function validate(input: unknown | T): IValidation; // detailed + +// STRICT VALIDATORS +export function equals(input: unknown | T): input is T; +export function assertEquals(input: unknown | T): T; +export function validateEquals(input: unknown | T): IValidation; + +// JSON +export function application(): IJsonApplication; // JSON schema +export function assertParse(input: string): T; // type safe parser +export function assertStringify(input: T): string; // safe and faster + // +) isParse, validateParse + // +) stringify, isStringify, validateStringify +``` + +[typia](https://github.com/samchon/typia) is a transformer library of TypeScript, supporting below features: + + - Super-fast Runtime Validators + - Safe JSON parse and fast stringify functions + - JSON schema generator + +All functions in `typia` require **only one line**. You don't need any extra dedication like JSON schema definitions or decorator function calls. Just call `typia` function with only one line like `typia.assert(input)`. + +Also, as `typia` performs AOT (Ahead of Time) compilation skill, its performance is much faster than other competitive libaries. For an example, when comparing validate function `is()` with other competitive libraries, `typia` is maximum **15,000x times faster** than `class-validator`. + +[typia](https://github.com/samchon/typia) 는 AOT 컴파일을 이용, (NestJS 가 사용하는 `class-validator` 대비) 최대 15,000 배 빠른 runtime validation 을 행할 수 있는 라이브러리이다. +아래 [@nestia/core](https://github.com/samchon/nestia) 와 함께 결합하여 사용하면, 귀하의 NestJS 백엔드 서버의 퍼포먼스, 특히 최대 동시 접속 가능자 수를 크게 향상시킬 수 있다. + +그리고 [typia](https://github.com/samchon/typia) 는 종래의 NestJS 및 `class-validator` 처럼 TypeScript 타입과 별도로 JSON 스키마 정의해야 한다거나, 별도의 DTO 클래스를 만들며 decorator 함수들을 호출해야 하는 등의 부가 작업이 일절 필요없다. 때문에 퍼포먼스 향상 외에도, 작업 효율 또한 크게 진전을 이룰 수 있다. + +![Is Function Benchmark](https://github.com/samchon/typia/raw/master/benchmark/results/11th%20Gen%20Intel(R)%20Core(TM)%20i5-1135G7%20%40%202.40GHz/images/is.svg) + +> Measured on [Intel i5-1135g7, Surface Pro 8](https://github.com/samchon/typia/tree/master/benchmark/results/11th%20Gen%20Intel(R)%20Core(TM)%20i5-1135G7%20%40%202.40GHz#is) + +#### 5.3.2. Nestia +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) +[![Downloads](https://img.shields.io/npm/dm/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) +[![Build Status](https://github.com/samchon/typia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/wiki-documentation-forestgreen)](https://github.com/samchon/nestia/wiki) + +[Nestia](https://github.com/samchon/nestia) is a helper library set for `NestJS`, supporting below features: + + - [`@nestia/core`](https://github.com/samchon/nestia#nestiacore): **15,000x times faster** validation decorator using `typia` + - [`@nestia/sdk`](https://github.com/samchon/nestia#nestiasdk): evolved **SDK** and **Swagger** generator for `@nestia/core` + - `nestia`: just CLI (command line interface) tool + +```typescript +import { Controller } from "@nestjs/common"; +import { TypedBody, TypedRoute } from "@nestia/core"; + +import { IBbsArticle } from "@bbs-api/structures/IBbsArticle"; + +@Controller("bbs/articles") +export class BbsArticlesController { + /** + * Store a new content. + * + * @param inupt Content to store + * @returns Newly archived article + */ + @TypedRoute.Post() // 10x faster and safer JSON.stringify() + public async store( + // super-fast validator + @TypedBody() input: IBbsArticle.IStore + ): Promise; + // do not need DTO class definition, + // just fine with interface +} +``` + +`@nestia/core` 는 [typia](https://github.com/samchon/typia) 를 이용, NestJS 의 validation 의 속도를 최대 15,000 배 가속해주는 라이브러리이다. + +그리고 `@nestia/sdk` 는 NestJS 로 만든 백엔드 서버 프로그램을 컴파일러 수준에서 분석, 클라이언트가 사용할 수 있는 SDK 라이브러리를 빌드해주는 프로그램이다. 본 저장소에서 사용하는 [fake-iamport-server](https://github.com/samchon/fake-iamport-server) 나 [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) 가 결제 PG 서버의 API 를 흉내내어 만든 가짜 서버인데, 뜬금 클라이언트가 이들 PG 서버와의 연동에 실제로 사용할 수 있는 SDK 라이브러리가 함께 제공되는 이유도 바로 이 덕분이다. + +때문에 만일 귀하가 결제를 사용하는 모종의 백엔드 서버를 개발 중이라면, `payment-backend` 뿐 아니라 [Nestia](https://github.com/samchon/nestia) 도 함께 사용해보는 것이 어떠한가? 첫째로 validation 속도를 가속하여 동시 접속자 수를 크게 늘릴 수 있다. 그리고 귀하의 백엔드 서버 또한 `payment-backend` 처럼 클라이언트 개발자가 사용할 수 있는 SDK 라이브러리를 자동으로 빌드하여 배포할 수 있으니, 백엔드 개발자와 프론트 개발자가 보다 편하게 연동 작업을 행할 수 있다. + +물론 `@nestia/sdk` 는 Swagger 또한 빌드할 수 있는데, 본 저장소가 사용하는 [fake-iamport-server](https://github.com/samchon/fake-iamport-server) 및 [fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) 로부터 빌드된 Swagger 문서들이 각각 아임포트와 토스 페이먼츠의 공식 개발자 가이드 문서보다 일목요연하고 체계도가 높은것도, 바로 이러한 이유 때문이다. Swagger 내지 가이드 문서를 사람이 손으로 작성하는게 아니라, `@nestia/sdk` 가 컴파일러 수준에서 백엔드 소스 코드와 DTO 를 분석하여 자동으로 생성해주었기 때문인 것. + +#### 5.3.3. Safe-TypeORM +Enhance TypeORM in the compilation level. + + - https://github.com/samchon/safe-typeorm + +`safe-typeorm` 은 `typeorm` 을 컴파일 수준에서 강화해주는 헬퍼 라이브러리이다. + +이를 사용하면 컴파일 및 단계에서 잘못된 SQL 쿼리문을 바로잡거나, 앱조인을 통하여 퍼포먼스 튜닝을 자동으로 할 수 있고 JSON 변환을 제로 코스트로 할 수 있는 등, 아래와 같은 이점이 있다. + + - When writing [**SQL query**](https://github.com/samchon/safe-typeorm#safe-query-builder), + - Errors would be detected in the **compilation** level + - **Auto Completion** would be provided + - **Type Hint** would be supported + - You can implement [**App-join**](https://github.com/samchon/safe-typeorm#app-join-builder) very conveniently + - When [**SELECT**ing for **JSON** conversion](https://github.com/samchon/safe-typeorm#json-select-builder) + - [**App-Join**](https://github.com/samchon/safe-typeorm#app-join-builder) with the related entities would be automatically done + - Exact JSON **type** would be automatically **deduced** + - The **performance** would be **automatically tuned** + - When [**INSERT**](https://github.com/samchon/safe-typeorm#insert-collection)ing records + - Sequence of tables would be automatically sorted by analyzing dependencies + - The **performance** would be **automatically tuned** + +![Safe-TypeORM Demo](https://raw.githubusercontent.com/samchon/safe-typeorm/master/assets/demonstrations/safe-query-builder.gif) + +#### 5.3.3. Fake Payment Servers +본 통합 결제 서버 `payment-backend` 가 연동하게 되는 결제 PG 사들은 본디 프론트 어플리케이션과 연동한 수기 테스트가 필요하다. 이 때문에 이들 결제 PG 사들과 연동해야 하는 결제 서버들은, 테스트 자동화 프로그램을 작성할 수 없기에, 필연적으로 테스트 커버리지가 낮아 매우 불안정해진다. + +이에 `payment-backend` 는 결제 PG 사들의 API 를 모방한 가짜 PG 서버들을 구현, 이들을 통하여 테스트 자동화 프로그램을 구현함으로써, 테스트 커버리지를 높이고 안정성을 담보하였다. 그리고 이들 가짜 결제 PG 사 서버들을 별도 프로젝트로 분리하여 오픈소스로 공개하니, `payment-backend` 와 연동하는 귀사의 서비스 백엔드 서버를 개발할 때 (특히 테스트 자동화 프로그램을 개발할 때), 이를 적극 활용하기 바란다. + + - [samchon/fake-iamport-server](https://github.com/samchon/fake-iamport-server) + - [samchon/fake-toss-payments-server](https://github.com/samchon/fake-toss-payments-server) + +```typescript +import { sleep_for } from "tstl/thread/global"; +import { v4 } from "uuid"; + +import toss from "toss-payments-server-api"; +import payments from "payment-api"; +import { IPaymentHistory } from "payment-api/lib/structures/IPaymentHistory"; +import { IPaymentWebhook } from "payment-api/lib/structures/IPaymentWebhook"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; + +import { FakePaymentStorage } from "../../../../providers/FakePaymentStorage"; +import { PaymentConfiguration } from "../../../../PaymentConfiguration"; +import { TossAsset } from "../../../../services/toss/TossAsset"; + +export async function test_fake_toss_payment_webhook + (connection: payments.IConnection): Promise +{ + const yourOrderId: string = v4(); // 귀하의 서비스가 발행한 주문 ID + const yourOrderPrice: number = 25_000; // 주문 금액 + + //---- + // 결제 내역 등록 + //---- + // 토스 페이먼츠 가상 결제 시뮬레이션 + const payment: ITossPayment = await toss.functional.v1.virtual_accounts.store + ( + TossAsset.connection("test-iamport-store-id"), + { + // 가싱 계좌 정보 + method: "virtual-account", + bank: "신한", + customerName: "남정호", + + // 주문 정보 + orderId: yourOrderId, + orderName: "some-order-name", + amount: 25_000, + + // 고의 미승인 처리 + __approved: false + } + ); + + // 웹훅 URL 설정하기. + const webhook_url: string = "http://127.0.0.1:" + + PaymentConfiguration.API_PORT + + payments.functional.internal.webhook.PATH; + + // 결제 이력 등록하기 + const history: IPaymentHistory = await payments.functional.histories.store + ( + connection, + { + vendor: { + code: "toss.payments", + store_id: "test-iamport-store-id", + uid: payment.paymentKey, + }, + source: { + schema: "some-schema", + table: "some-table", + id: yourOrderId + }, + webhook_url, // 테스트용 웹훅 URL + price: yourOrderPrice, + password: "some-password", + } + ); + + //---- + // 웹훅 이벤트 리스닝 + //---- + // 입금 시뮬레이션 + await toss.functional.internal.deposit + ( + TossAsset.connection("test-iamport-store-id"), + payment.paymentKey + ); + + // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. + await sleep_for(100); + + // 웹흑 이벤트 리스닝 시뮬레이션. + const webhook: IPaymentWebhook = FakePaymentStorage.webhooks.back(); + if (webhook.current.id !== history.id) + throw new Error("Bug on PaymentWebhooksController.toss(): failed to deliver the webhook event."); + else if (webhook.previous.paid_at !== null) + throw new Error("Bug on PaymentWebhookProvider.process(): failed to delivery the exact previous data."); + else if (webhook.current.paid_at === null) + throw new Error("Bug on PaymentWebhookProvider.process(): failed to delivery the exact current data."); + + // 웹훅 데이터 삭제 + FakePaymentStorage.webhooks.pop_back(); +} +``` \ No newline at end of file diff --git a/packages/payment-backend/docs/ERD.md b/packages/payment-backend/docs/ERD.md new file mode 100644 index 0000000..5248f0c --- /dev/null +++ b/packages/payment-backend/docs/ERD.md @@ -0,0 +1,225 @@ +# Payments System +> Generated by [`prisma-markdown`](https://github.com/samchon/prisma-markdown) + +- [Payments](#Payments) + +## Payments +```mermaid +erDiagram +payment_reservations { + String id PK + String source_schema + String source_table + String source_id + String vendor_code + String vendor_uid + String vendor_store_id + String title + String data + String password + DateTime created_at +} +payment_histories { + String id PK + String source_schema + String source_table + String source_id + String vendor_code + String vendor_uid + String vendor_store_id + String currency + Float price + Float refund "nullable" + String webhook_url + String data + String password + DateTime created_at + DateTime paid_at "nullable" + DateTime cancelled_at "nullable" +} +payment_history_cancels { + String id PK + String payment_history_id FK + Float amount + String reason + String data + DateTime created_at +} +payment_history_webhooks { + String id PK + String payment_history_id FK + String previous + String current + String data + DateTime created_at +} +payment_history_webhook_responses { + String id PK + String payment_history_webhook_id FK + Int status "nullable" + 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_reservations` +간편 결제 수단 정보. + +`payment_reservations` 는 고객이 신용카드 등을 정기 결제 수단으로 등록, +매 결제시마다 반복된 결제 수단 인증 작업을 안 해도 되게끔 하는, +이른바 "간편 결제" 에 대한 레코드 정보를 담은 테이블이다. + +참고로 간편 결제에 대한 레코드를 저장할 때, 서비스 시스템은 해당 레코드에 대하여 +비밀번호를 지정하여야 한다. 비밀번호를 유실한 경우, 해당 레코드는 그 누구도 조회할 +수 없으니 서비스 시스템은 이를 잘 관리하여야 한다. 참고로 이 비밀번호는 유저의 +비밀번호와 일절 무관하여야 하며, 오직 서비스 시스템이 스스로 임의 발급한 +비밀번호여야만 한다. + +이외에 PG사의 간편 결제 정보는 [payment_reservations.data](#payment_reservations) 에 저장된다. + +**Properties** + - `id`: + - `source_schema`: 결제를 발생시킨 원천 레코드의 스키마 + - `source_table`: 결제를 발생시킨 원천 레코드의 테이블 + - `source_id`: 결제를 발생시킨 원천 레코드의 ID + - `vendor_code` + > PG 벤더사의 코드. + > + > ex) "iamport" + - `vendor_uid`: PG 벤더사에서 발급해준 간편 결제 식별자 키 + - `vendor_store_id` + > PG 벤더사에서의 가맹점 ID. + > + > PG 벤더사 내에서의 회원사 ID 와 진배 없다. + - `title`: 제목. + - `data`: PG 벤더사로부터의 간편 결제에 대한 상세 데이터. + - `password` + > 레코드 비밀번호. + > + > 서비스 시스템이 임의로 발급한 비밀번호로써, 유저와는 하등 무관해야 함. + - `created_at`: 레코드 생성 일시. + +### `payment_histories` +결제 (신청/지불) 정보. + +`payment_histories` 는 PG 시스템을 통한 결제 신청 혹은 지불 정보를 기록한 엔티티이다. +서비스 시스템으로부 결제 사건의 귀속 정보 (vendor_X` + `source_Y`) 를 전달받아, +본 Payment 시스템이 PG 사에 추가 정보를 취득하여 레코드가 완성된다. + +그리고 만일 현 결제 건이 가상계좌와 같이 레코드 생성 시점에 지불이 이루어지지 않은 +경우라면, 사후 [웹훅 이벤트](#payment_history_webhooks)를 통하여 지불 완료 +시각을 뜻하는 [payment_histories.paid_at](#payment_histories) 이 설정될 수 있다. 반대로 이미 +결제가 완료된 경우라도, 환불 등의 이유로 [payment_histories.cancelled_at](#payment_histories) 이 +사후 기재될 수 있다. + +**Properties** + - `id`: + - `source_schema`: 결제를 발생시킨 원천 레코드의 스키마 + - `source_table`: 결제를 발생시킨 원천 레코드의 테이블 + - `source_id`: 결제를 발생시킨 원천 레코드의 ID + - `vendor_code` + > PG 벤더사의 코드. + > + > ex) "iamport" + - `vendor_uid`: PG 벤더사에서 발급해준 간편 결제 식별자 키 + - `vendor_store_id` + > PG 벤더사에서의 가맹점 ID. + > + > PG 벤더사 내에서의 회원사 ID 와 진배 없다. + - `currency` + > 기준 통화. + > + > KRW, USB, JPY 등. + - `price`: 결제 가격. + - `refund`: 결제 취소 시의 환불 금액. + - `webhook_url` + > 웹훅 URL. + > + > 웹훅 데이터를 받아갈 서비스 시스템의 URL. + - `data` + > PG 벤더사로부터의 결제 이력에 대한 상세 데이터. + > + > JSON-string 이 암호화되어 저장되어있다. + - `password` + > 레코드 비밀번호. + > + > 서비스 시스템이 임의로 발급한 비밀번호로써, 유저와는 하등 무관해야 함. + - `created_at`: 레코드 생성 일시 + - `paid_at` + > 결제 완료 일시. + > + > 가상 계좌와 같이, 레코드 생성 일시와 결제 완료 일시가 다를 수 있다. + - `cancelled_at` + > 결제 취소 시간. + > + > 단, 결제 취소가 여러번에 걸쳐 일어난 경우 (부분 환불), + > 가장 첫 번째의 취소 시간을 기록한다. + +### `payment_history_cancels` +결제 취소 이력. + +결제 취소 이력을 기록한 엔티티. + +단, 본 시스템을 통하지 않고 각각의 PG사로 직접 접속하여 결제 취소한 경우, +그 내역이 정확하지 않을 수 있다. 다만 PG사로부터 웹훅 이벤트 발생시, 매번 데이터를 +갱신하고 있기에, 그 부정확함이 영구적이지는 않다. + +**Properties** + - `id`: + - `payment_history_id`: 귀속 결제 이력의 [payment_histories.id](#payment_histories) + - `amount`: 환불 금액. + - `reason`: 취소 사유. + - `data` + > PG 벤더사로부터의 결제 취소 이력에 대한 상세 데이터. + > + > JSON-string 이 암호화되어 저장되어있다. + - `created_at`: 결제 취소 시간. + +### `payment_history_webhooks` +웹훅 이벤트 레코드. + +`payment_history_webhooks` 는 PG 벤더사로부터의 이벤트를 기록하는 엔티티이다. + +웹훅 이벤트는 고객이 가상계좌를 선택하고 사후에 지불을 완료했다던가, 이미 결제한 +금액을 환불하여 결제가 취소되었다던가 하는 등의 이유로 발생한다. 그리고 웹훅 이벤트 +레코드의 발생은 곧, 원천 결제 레코드에 해당하는 [payment_histories.data](#payment_histories) 의 +수정을 불러온다. + +때문에 [payment_history_webhooks.previous](#payment_history_webhooks) 라 하여, 웹훅 이벤트가 발생하기 +전의 [payment_histories.data](#payment_histories) 를 기록하는 속성이 존재한다. 만일 웹훅 +이벤트가 발생하여 변동된 데이터를 보고 싶다면, 현 웹훅 이벤트가 가장 최신인지 +여부를 따져, [payment_histories.data](#payment_histories) 를 조회하던가 아니면 그 다음 웹훅 +레코드의 [payment_history_webhooks.previous](#payment_history_webhooks) 를 조회하던가 하면 된다. + +**Properties** + - `id`: + - `payment_history_id`: 귀속 결제 이력의 [payment_histories.id](#payment_histories) + - `previous`: 이전 결제 이력의 [payment_histories.data](#payment_histories) + - `current`: 웹훅 이후의 결제 이력에 대한 [payment_histories.data](#payment_histories) + - `data` + > PG 벤더사로부터 받은 웹훅 데이터. + > + > [payment_histories.data](#payment_histories) 와는 다르다 + - `created_at`: 웹훅 레코드 생성 일시. + +### `payment_history_webhook_responses` +서비스 시스템의 웹훅에 대한 응답 이력 레코드. + +`payment_history_webhook_responses` 는 서비스 시스템이 본 Payment 시스템으로부터 +전송받은 웹훅 이벤트에 대한 응답을 기록하는 엔티티이다. 만일 서비스 시스템이 +먹통이라 데이터 수신 자체를 못하는 상황이라면, `status` 와 `body` 데이터 모두 +`NULL` 값이 부여된다. + +그리고 본 Payment 시스템이 서비스 시스템으로 전송하는 웹훅 이벤트 데이터 또한, +본 시스템의 여타 API 와 같이 `body` 부문이 암호화 처리되어 전송된다. 자세한 내용은 +본 저장소의 README 문서를 참고할 것. + +**Properties** + - `id`: + - `payment_history_webhook_id`: 귀속 웹훅의 [payment_history_webhooks.id](#payment_history_webhooks) + - `status`: HTTP 응답 상태 코드. + - `body`: HTTP 응답 본문. + - `created_at`: 레코드 생성 일시. \ No newline at end of file diff --git a/packages/payment-backend/nestia.config.ts b/packages/payment-backend/nestia.config.ts new file mode 100644 index 0000000..7e77527 --- /dev/null +++ b/packages/payment-backend/nestia.config.ts @@ -0,0 +1,12 @@ +import type nestia from "@nestia/sdk"; + +const NESTIA_CONFIG: nestia.INestiaConfig = { + simulate: true, + input: "src/controllers", + 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 new file mode 100644 index 0000000..a046085 --- /dev/null +++ b/packages/payment-backend/package.json @@ -0,0 +1,105 @@ +{ + "name": "payment-backend", + "version": "4.0.0-dev.20230920", + "description": "Payment Backend Server", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "----------------------------------------------": "", + "build": "npm run build:sdk && npm run build:main && npm run build:test", + "build:api": "rimraf packages/api/lib && nestia sdk && tsc -p packages/api/tsconfig.json", + "build:main": "rimraf lib && tsc", + "build:prisma": "prisma generate --schema=src/schema.prisma", + "build:sdk": "rimraf src/api/functional && nestia sdk", + "build:swagger": "nestia swagger", + "build:test": "rimraf bin && tsc -p test/tsconfig.json", + "dev": "npm run build:test -- --watch", + "eslint": "eslint src && eslint test", + "eslint:fix": "eslint src --fix && eslint test --fix", + "package:api": "npm run build:api && cd packages/api && npm publish", + "package:models": "npm run build:models && cd packags/models && npm publish", + "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", + "test:update": "node bin/test/manual/update", + "generate:password": "npx ts-node src/test/manual/password", + "generate:uuid": "ts-node test/manual/uuid", + "reset:dev": "git pull && npm install && npm run build && npm run stop && npm run test -- --mode dev && npm run start:updator:master && npm run start dev", + "schema": "node lib/executable/schema", + "------------------------------------------------": "", + "revert": "node lib/executable/revert", + "start": "pm2 start lib/executable/server.js -i 0 --name payments-server-backend-server --wait-ready --listen-timeout 120000 --kill-timeout 15000 --", + "start:local": "pm2 start lib/executable/server.js -i 2 --name payments-server-backend-server --wait-ready --listen-timeout 120000 --kill-timeout 15000 -- local", + "start:updator:master": "pm2 start --name payments-server-backend-updator-master lib/updator/master.js", + "start:updator:slave": "pm2 start --name payments-server-backend-updator-slave lib/updator/slave.js", + "start:reload": "pm2 reload payments-server-backend-server", + "stop": "pm2 delete payments-server-backend-server", + "stop:updator:master": "pm2 delete payments-server-backend-updator-master", + "stop:updator:slave": "pm2 delete payments-server-backend-updator-slave", + "update": "node lib/executable/update", + "-------------------------------------------------": "" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/payments" + }, + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/payments/issues" + }, + "homepage": "https://github.com/samchon/payments", + "devDependencies": { + "@nestia/sdk": "^2.0.4", + "@trivago/prettier-plugin-sort-imports": "^4.0.0", + "@types/atob": "^2.1.2", + "@types/bcryptjs": "^2.4.4", + "@types/btoa": "^1.2.3", + "@types/cli": "^0.11.19", + "@types/express": "^4.17.12", + "@types/inquirer": "^8.2.5", + "@types/node": "^15.6.1", + "@types/pg": "^8.6.5", + "@types/uuid": "^8.3.0", + "@typescript-eslint/eslint-plugin": "^5.26.0", + "@typescript-eslint/parser": "^5.26.0", + "cli": "^1.0.1", + "copyfiles": "^2.4.1", + "iamport-server-api": "D:\\github\\samchon\\payments\\packages\\iamport-server-api\\iamport-server-api-4.0.0-dev.20230920.tgz", + "nestia": "^4.5.0", + "pm2": "^4.5.6", + "prettier": "^2.6.2", + "prisma-markdown": "^1.0.0", + "rimraf": "^3.0.2", + "sloc": "^0.2.1", + "toss-payments-server-api": "D:\\github\\samchon\\payments\\packages\\toss-payments-server-api\\toss-payments-server-api-4.0.0-dev.20230920.tgz", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", + "typescript": "^5.2.2", + "typescript-transform-paths": "^3.4.6" + }, + "dependencies": { + "@nestia/core": "^2.0.4", + "@prisma/client": "^5.3.1", + "bcryptjs": "^2.4.3", + "dotenv": "^16.3.1", + "dotenv-expand": "^10.0.0", + "fake-iamport-server": "D:\\github\\samchon\\payments\\packages\\fake-iamport-server\\fake-iamport-server-4.0.0-dev.20230920.tgz", + "fake-toss-payments-server": "D:\\github\\samchon\\payments\\packages\\fake-toss-payments-server\\fake-toss-payments-server-4.0.0-dev.20230920.tgz", + "git-last-commit": "^1.0.0", + "inquirer": "^8.2.5", + "mutex-server": "^0.3.1", + "prisma": "^5.3.1", + "source-map-support": "^0.5.19", + "tstl": "^2.5.13", + "typia": "^5.0.4" + }, + "files": [ + "lib", + "src", + "LICENSE", + "README.md" + ] +} \ No newline at end of file diff --git a/packages/payment-backend/src/PaymentAsset.ts b/packages/payment-backend/src/PaymentAsset.ts new file mode 100644 index 0000000..823f584 --- /dev/null +++ b/packages/payment-backend/src/PaymentAsset.ts @@ -0,0 +1,19 @@ +import iamport from "iamport-server-api"; +import toss from "toss-payments-server-api"; + +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 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 new file mode 100644 index 0000000..13a166a --- /dev/null +++ b/packages/payment-backend/src/PaymentBackend.ts @@ -0,0 +1,120 @@ +import nest from "@modules/nestjs"; +import core from "@nestia/core"; +import { NestFactory } from "@nestjs/core"; +import express from "express"; +import FakeIamport from "fake-iamport-server"; +import FakeToss from "fake-toss-payments-server"; + +import { PaymentConfiguration } from "./PaymentConfiguration"; +import { PaymentGlobal } from "./PaymentGlobal"; +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 core.EncryptedModule.dynamic( + __dirname + "/controllers", + PaymentConfiguration.ENCRYPTION_PASSWORD(), + ), + { 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.TossFakeConfiguration.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); + }); + } + + /** + * 개설한 서버 닫기. + */ + 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_ = []; + } + + 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; +} +type FunctionLike = (...args: any) => any; diff --git a/packages/payment-backend/src/PaymentConfiguration.ts b/packages/payment-backend/src/PaymentConfiguration.ts new file mode 100644 index 0000000..aabadba --- /dev/null +++ b/packages/payment-backend/src/PaymentConfiguration.ts @@ -0,0 +1,104 @@ +import nest from "@modules/nestjs"; +import { ExceptionManager } from "@nestia/core"; +import { IEncryptionPassword } from "@nestia/fetcher"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { IIamportUser } from "iamport-server-api/lib/structures/IIamportUser"; + +import { PaymentGlobal } from "./PaymentGlobal"; + +const EXTENSION = __filename.substr(-2); +if (EXTENSION === "js") require("source-map-support").install(); + +/** + * 통합 결제 서버 설정 정보. + * + * @author Samchon + */ +export namespace PaymentConfiguration { + /* ----------------------------------------------------------- + CONNECTIONS + ----------------------------------------------------------- */ + export const API_PORT = () => Number(PaymentGlobal.env.API_PORT); + export const UPDATOR_PORT = () => Number(PaymentGlobal.env.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.API_ENCRYPTION_KEY ?? "", + iv: PaymentGlobal.env.API_ENCRYPTION_IV ?? "", + }); + export const SYSTEM_PASSWORD = () => PaymentGlobal.env.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.IAMPORT_KEY, + imp_secret: PaymentGlobal.env.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.TOSS_PAYMENTS_SECRET; + }; + + /** + * @internal + */ + export const ROOT = __dirname.split("\\").join("/") + "/.."; + + /** + * @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); + } +}); diff --git a/packages/payment-backend/src/PaymentGlobal.ts b/packages/payment-backend/src/PaymentGlobal.ts new file mode 100644 index 0000000..e12df0c --- /dev/null +++ b/packages/payment-backend/src/PaymentGlobal.ts @@ -0,0 +1,128 @@ +import { PrismaClient } from "@prisma/client"; +import dotenv from "dotenv"; +import dotenvExpand from "dotenv-expand"; +import { MutexConnector } from "mutex-server"; +import { MutableSingleton, Singleton } from "tstl"; +import typia from "typia"; + +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().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 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; + }); +} +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; +} + +interface IMode { + value?: "local" | "dev" | "real"; +} +const modeWrapper: IMode = {}; + +interface IEnvironments { + // DEFAULT CONFIGURATIONS + MODE: "local" | "dev" | "real"; + API_PORT: `${number}`; + UPDATOR_PORT: `${number}`; + SYSTEM_PASSWORD: string; + + // POSTGRES CONNECTION INFO + POSTGRES_URL: string; + POSTGRES_HOST: string; + POSTGRES_PORT: `${number}`; + POSTGRES_DATABASE: string; + POSTGRES_SCHEMA: string; + POSTGRES_USERNAME: string; + POSTGRES_USERNAME_READONLY: string; + POSTGRES_PASSWORD: string; + + // ENCRYPTION KEYS + API_ENCRYPTION_KEY: string; + API_ENCRYPTION_IV: string; + DB_HISTORY_ENCRYPTION_KEY: string; + DB_HISTORY_ENCRYPTION_IV: string; + DB_RESERVATION_ENCRYPTION_KEY: string; + DB_RESERVATION_ENCRYPTION_IV: string; + DB_CANCEL_HISTORY_ENCRYPTION_KEY: string; + DB_CANCEL_HISTORY_ENCRYPTION_IV: string; + + // VENDOR'S SECRETS + IAMPORT_KEY: string; + IAMPORT_SECRET: string; + TOSS_PAYMENTS_SECRET: string; +} + +const environments: Singleton = new Singleton(() => { + const env = dotenv.config(); + dotenvExpand.expand(env); + return typia.assert(process.env); +}); + +const prismaClient = new Singleton( + () => + new PrismaClient({ + datasources: { + db: { + url: PaymentGlobal.env.POSTGRES_URL, + }, + }, + }), +); diff --git a/packages/payment-backend/src/PaymentSetupWizard.ts b/packages/payment-backend/src/PaymentSetupWizard.ts new file mode 100644 index 0000000..42e4c79 --- /dev/null +++ b/packages/payment-backend/src/PaymentSetupWizard.ts @@ -0,0 +1,26 @@ +import { PrismaClient } from "@prisma/client"; +import cp from "child_process"; + +import { PaymentGlobal } from "./PaymentGlobal"; + +export namespace PaymentSetupWizard { + export async function schema(client: PrismaClient): 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"); + + await client.$executeRawUnsafe( + `GRANT SELECT ON ALL TABLES IN SCHEMA ${PaymentGlobal.env.POSTGRES_SCHEMA} TO ${PaymentGlobal.env.POSTGRES_USERNAME_READONLY}`, + ); + } + + export async function seed(): Promise {} +} diff --git a/packages/payment-backend/src/PaymentUpdator.ts b/packages/payment-backend/src/PaymentUpdator.ts new file mode 100644 index 0000000..b58c05d --- /dev/null +++ b/packages/payment-backend/src/PaymentUpdator.ts @@ -0,0 +1,105 @@ +import { MutexAcceptor, MutexConnector, MutexServer } from "mutex-server"; +import { HashSet } from "tstl/container/HashSet"; + +import { PaymentConfiguration } from "./PaymentConfiguration"; +import { Terminal } from "./utils/Terminal"; + +/** + * 업데이트 리스너. + * + * @author Samchon + */ +export namespace PaymentUpdator { + 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); + }, + }; + + // 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; + } + + /** + * 슬레이브 인스턴스에서의 업데이트 리스터 실행. + * + * @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"); + + // RELOAD PM2 + await Terminal.execute("npm run start:reload"); + } + } +} diff --git a/packages/payment-backend/src/api/HttpError.ts b/packages/payment-backend/src/api/HttpError.ts new file mode 100644 index 0000000..5df328a --- /dev/null +++ b/packages/payment-backend/src/api/HttpError.ts @@ -0,0 +1 @@ +export { HttpError } from "@nestia/fetcher"; diff --git a/packages/payment-backend/src/api/IConnection.ts b/packages/payment-backend/src/api/IConnection.ts new file mode 100644 index 0000000..107bdb8 --- /dev/null +++ b/packages/payment-backend/src/api/IConnection.ts @@ -0,0 +1 @@ +export type { IConnection } from "@nestia/fetcher"; diff --git a/packages/payment-backend/src/api/Primitive.ts b/packages/payment-backend/src/api/Primitive.ts new file mode 100644 index 0000000..60d3944 --- /dev/null +++ b/packages/payment-backend/src/api/Primitive.ts @@ -0,0 +1 @@ +export type { Primitive } from "@nestia/fetcher"; diff --git a/packages/payment-backend/src/api/functional/index.ts b/packages/payment-backend/src/api/functional/index.ts new file mode 100644 index 0000000..8e6c9bf --- /dev/null +++ b/packages/payment-backend/src/api/functional/index.ts @@ -0,0 +1,8 @@ +/** + * @packageDocumentation + * @module api.functional + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as monitors from "./monitors"; +export * as payments from "./payments"; \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/monitors/health/index.ts b/packages/payment-backend/src/api/functional/monitors/health/index.ts new file mode 100644 index 0000000..d5510b4 --- /dev/null +++ b/packages/payment-backend/src/api/functional/monitors/health/index.ts @@ -0,0 +1,50 @@ +/** + * @packageDocumentation + * @module api.functional.monitors.health + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; + +/** + * @controller HealthController.get + * @path GET /monitors/health + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, +): Promise { + return !!connection.simulate + ? get.simulate( + connection, + ) + : PlainFetcher.fetch( + connection, + { + ...get.METADATA, + path: get.path(), + } as const, + ); +} +export namespace get { + + export const METADATA = { + method: "GET", + path: "/monitors/health", + request: null, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/monitors/health`; + } + export const simulate = async ( + _connection: IConnection, + ): Promise => { + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/monitors/index.ts b/packages/payment-backend/src/api/functional/monitors/index.ts new file mode 100644 index 0000000..094d418 --- /dev/null +++ b/packages/payment-backend/src/api/functional/monitors/index.ts @@ -0,0 +1,9 @@ +/** + * @packageDocumentation + * @module api.functional.monitors + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as health from "./health"; +export * as performance from "./performance"; +export * as system from "./system"; \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/monitors/performance/index.ts b/packages/payment-backend/src/api/functional/monitors/performance/index.ts new file mode 100644 index 0000000..6efe697 --- /dev/null +++ b/packages/payment-backend/src/api/functional/monitors/performance/index.ts @@ -0,0 +1,62 @@ +/** + * @packageDocumentation + * @module api.functional.monitors.performance + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { EncryptedFetcher } from "@nestia/fetcher/lib/EncryptedFetcher"; +import typia from "typia"; + +import type { IPerformance } from "../../../structures/monitors/IPerformance"; + +/** + * @controller PerformanceController.get + * @path GET /monitors/performance + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, +): Promise { + return !!connection.simulate + ? get.simulate( + connection, + ) + : EncryptedFetcher.fetch( + connection, + { + ...get.METADATA, + path: get.path(), + } as const, + ); +} +export namespace get { + export type Output = Primitive; + + export const METADATA = { + method: "GET", + path: "/monitors/performance", + request: null, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/monitors/performance`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + ): Promise => { + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/monitors/system/index.ts b/packages/payment-backend/src/api/functional/monitors/system/index.ts new file mode 100644 index 0000000..dbf5b9c --- /dev/null +++ b/packages/payment-backend/src/api/functional/monitors/system/index.ts @@ -0,0 +1,62 @@ +/** + * @packageDocumentation + * @module api.functional.monitors.system + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { EncryptedFetcher } from "@nestia/fetcher/lib/EncryptedFetcher"; +import typia from "typia"; + +import type { ISystem } from "../../../structures/monitors/ISystem"; + +/** + * @controller SystemController.get + * @path GET /monitors/system + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, +): Promise { + return !!connection.simulate + ? get.simulate( + connection, + ) + : EncryptedFetcher.fetch( + connection, + { + ...get.METADATA, + path: get.path(), + } as const, + ); +} +export namespace get { + export type Output = Primitive; + + export const METADATA = { + method: "GET", + path: "/monitors/system", + request: null, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/monitors/system`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + ): Promise => { + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/payments/histories/index.ts b/packages/payment-backend/src/api/functional/payments/histories/index.ts new file mode 100644 index 0000000..169fbd3 --- /dev/null +++ b/packages/payment-backend/src/api/functional/payments/histories/index.ts @@ -0,0 +1,331 @@ +/** + * @packageDocumentation + * @module api.functional.payments.histories + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { EncryptedFetcher } from "@nestia/fetcher/lib/EncryptedFetcher"; +import typia from "typia"; +import type { Format } from "typia/lib/tags/Format"; + +import type { IPaymentCancelHistory } from "../../../structures/payments/IPaymentCancelHistory"; +import type { IPaymentHistory } from "../../../structures/payments/IPaymentHistory"; +import type { IPaymentSource } from "../../../structures/payments/IPaymentSource"; +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * 결제 내역 상세 조회하기. + * + * @param input 결제 내역의 원천 정보 + 비밀번호 + * @returns 결제 내역 + * @author Samchon + * + * @controller PaymentHistoriesController.get + * @path PATCH /payments/histories/get + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, + input: get.Input, +): Promise { + return !!connection.simulate + ? get.simulate( + connection, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...get.METADATA, + path: get.path(), + } as const, + input, + ); +} +export namespace get { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "PATCH", + path: "/payments/histories/get", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/histories/get`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + input: get.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "text/plain", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 결제 내역 상세 조회하기. + * + * @param id Primary Key + * @param input 결제 내역의 비밀번호 + * @returns 결제 내역 + * @author Samchon + * + * @controller PaymentHistoriesController.at + * @path PATCH /payments/histories/:id + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function at( + connection: IConnection, + id: string & Format<"uuid">, + input: at.Input, +): Promise { + return !!connection.simulate + ? at.simulate( + connection, + id, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...at.METADATA, + path: at.path(id), + } as const, + input, + ); +} +export namespace at { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "PATCH", + path: "/payments/histories/:id", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (id: string & Format<"uuid">): string => { + return `/payments/histories/${encodeURIComponent(id ?? "null")}`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + id: string & Format<"uuid">, + input: at.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(id), + contentType: "text/plain", + }); + assert.param("id")(() => typia.assert(id)); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 결제 내역 발행하기. + * + * @param input 결제 내역 입력 정보 + * @returns 결제 내역 + * @author Samchon + * + * @controller PaymentHistoriesController.store + * @path POST /payments/histories + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function store( + connection: IConnection, + input: store.Input, +): Promise { + return !!connection.simulate + ? store.simulate( + connection, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...store.METADATA, + path: store.path(), + } as const, + input, + ); +} +export namespace store { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "POST", + path: "/payments/histories", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/histories`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + input: store.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "text/plain", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 결제 취소하기. + * + * @param input 결제 취소 내역 입력 정보 + * @author Samchon + * + * @controller PaymentHistoriesController.cancel + * @path PUT /payments/histories/cancel + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function cancel( + connection: IConnection, + input: cancel.Input, +): Promise { + return !!connection.simulate + ? cancel.simulate( + connection, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...cancel.METADATA, + path: cancel.path(), + } as const, + input, + ); +} +export namespace cancel { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "PUT", + path: "/payments/histories/cancel", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/histories/cancel`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + input: cancel.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "text/plain", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/payments/index.ts b/packages/payment-backend/src/api/functional/payments/index.ts new file mode 100644 index 0000000..1f10e4e --- /dev/null +++ b/packages/payment-backend/src/api/functional/payments/index.ts @@ -0,0 +1,10 @@ +/** + * @packageDocumentation + * @module api.functional.payments + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +export * as histories from "./histories"; +export * as internal from "./internal"; +export * as reservations from "./reservations"; +export * as webhooks from "./webhooks"; \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/payments/internal/index.ts b/packages/payment-backend/src/api/functional/payments/internal/index.ts new file mode 100644 index 0000000..2011806 --- /dev/null +++ b/packages/payment-backend/src/api/functional/payments/internal/index.ts @@ -0,0 +1,78 @@ +/** + * @packageDocumentation + * @module api.functional.payments.internal + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { EncryptedFetcher } from "@nestia/fetcher/lib/EncryptedFetcher"; +import typia from "typia"; + +import type { IPaymentWebhookHistory } from "../../../structures/payments/IPaymentWebhookHistory"; +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * + * @internal + * + * @controller PaymentInternalController.webhook + * @path POST /payments/internal/webhook + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function webhook( + connection: IConnection, + input: webhook.Input, +): Promise { + return !!connection.simulate + ? webhook.simulate( + connection, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...webhook.METADATA, + path: webhook.path(), + } as const, + input, + ); +} +export namespace webhook { + export type Input = Primitive; + + export const METADATA = { + method: "POST", + path: "/payments/internal/webhook", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/internal/webhook`; + } + export const simulate = async ( + connection: IConnection, + input: webhook.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "text/plain", + }); + assert.body(() => typia.assert(input)); + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/payments/reservations/index.ts b/packages/payment-backend/src/api/functional/payments/reservations/index.ts new file mode 100644 index 0000000..3201288 --- /dev/null +++ b/packages/payment-backend/src/api/functional/payments/reservations/index.ts @@ -0,0 +1,250 @@ +/** + * @packageDocumentation + * @module api.functional.payments.reservations + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { EncryptedFetcher } from "@nestia/fetcher/lib/EncryptedFetcher"; +import typia from "typia"; +import type { Format } from "typia/lib/tags/Format"; + +import type { IPaymentReservation } from "../../../structures/payments/IPaymentReservation"; +import type { IPaymentSource } from "../../../structures/payments/IPaymentSource"; +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * 간편 결제 수단 조회하기. + * + * @param input 간편 결제 수단의 원천 정보 + 비밀번호 + * @returns 결제 내역 + * + * @controller PaymentReservationsController.get + * @path PATCH /payments/reservations/get + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function get( + connection: IConnection, + input: get.Input, +): Promise { + return !!connection.simulate + ? get.simulate( + connection, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...get.METADATA, + path: get.path(), + } as const, + input, + ); +} +export namespace get { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "PATCH", + path: "/payments/reservations/get", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/reservations/get`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + input: get.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "text/plain", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 간편 결제 수단 조회하기. + * + * @param id Primary Key + * @param input 비밀번호 + * @returns 간편 결제 수단 정보 + * + * @controller PaymentReservationsController.at + * @path PATCH /payments/reservations/:id + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function at( + connection: IConnection, + id: string & Format<"uuid">, + input: at.Input, +): Promise { + return !!connection.simulate + ? at.simulate( + connection, + id, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...at.METADATA, + path: at.path(id), + } as const, + input, + ); +} +export namespace at { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "PATCH", + path: "/payments/reservations/:id", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (id: string & Format<"uuid">): string => { + return `/payments/reservations/${encodeURIComponent(id ?? "null")}`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + id: string & Format<"uuid">, + input: at.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(id), + contentType: "text/plain", + }); + assert.param("id")(() => typia.assert(id)); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} + +/** + * 간편 결제 수단 등록하기. + * + * @param input 간편 결제 수단 입력 정보 + * @returns 간편 결제 수단 정보 + * + * @controller PaymentReservationsController.store + * @path POST /payments/reservations + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function store( + connection: IConnection, + input: store.Input, +): Promise { + return !!connection.simulate + ? store.simulate( + connection, + input, + ) + : EncryptedFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "text/plain", + }, + }, + { + ...store.METADATA, + path: store.path(), + } as const, + input, + ); +} +export namespace store { + export type Input = Primitive; + export type Output = Primitive; + + export const METADATA = { + method: "POST", + path: "/payments/reservations", + request: { + type: "text/plain", + encrypted: true + }, + response: { + type: "text/plain", + encrypted: true, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/reservations`; + } + export const random = (g?: Partial): Primitive => + typia.random>(g); + export const simulate = async ( + connection: IConnection, + input: store.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "text/plain", + }); + assert.body(() => typia.assert(input)); + return random( + typeof connection.simulate === 'object' && + connection.simulate !== null + ? connection.simulate + : undefined + ); + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/functional/payments/webhooks/index.ts b/packages/payment-backend/src/api/functional/payments/webhooks/index.ts new file mode 100644 index 0000000..01ea638 --- /dev/null +++ b/packages/payment-backend/src/api/functional/payments/webhooks/index.ts @@ -0,0 +1,145 @@ +/** + * @packageDocumentation + * @module api.functional.payments.webhooks + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +//================================================================ +import type { IConnection, Primitive } from "@nestia/fetcher"; +import { PlainFetcher } from "@nestia/fetcher/lib/PlainFetcher"; +import type { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import type { ITossPaymentWebhook } from "toss-payments-server-api/lib/structures/ITossPaymentWebhook"; +import typia from "typia"; + +import { NestiaSimulator } from "../../../utils/NestiaSimulator"; + +/** + * + * @danger + * + * @controller PaymentWebhooksController.iamport + * @path POST /payments/webhooks/iamport + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function iamport( + connection: IConnection, + input: iamport.Input, +): Promise { + return !!connection.simulate + ? iamport.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...iamport.METADATA, + path: iamport.path(), + } as const, + input, + ); +} +export namespace iamport { + export type Input = Primitive; + + export const METADATA = { + method: "POST", + path: "/payments/webhooks/iamport", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/webhooks/iamport`; + } + export const simulate = async ( + connection: IConnection, + input: iamport.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + } +} + +/** + * + * @internal + * + * @controller PaymentWebhooksController.toss + * @path POST /payments/webhooks/toss + * @nestia Generated by Nestia - https://github.com/samchon/nestia + */ +export async function toss( + connection: IConnection, + input: toss.Input, +): Promise { + return !!connection.simulate + ? toss.simulate( + connection, + input, + ) + : PlainFetcher.fetch( + { + ...connection, + headers: { + ...(connection.headers ?? {}), + "Content-Type": "application/json", + }, + }, + { + ...toss.METADATA, + path: toss.path(), + } as const, + input, + ); +} +export namespace toss { + export type Input = Primitive; + + export const METADATA = { + method: "POST", + path: "/payments/webhooks/toss", + request: { + type: "application/json", + encrypted: false + }, + response: { + type: "application/json", + encrypted: false, + }, + status: null, + } as const; + + export const path = (): string => { + return `/payments/webhooks/toss`; + } + export const simulate = async ( + connection: IConnection, + input: toss.Input, + ): Promise => { + const assert = NestiaSimulator.assert({ + method: METADATA.method, + host: connection.host, + path: path(), + contentType: "application/json", + }); + assert.body(() => typia.assert(input)); + } +} \ No newline at end of file diff --git a/packages/payment-backend/src/api/index.ts b/packages/payment-backend/src/api/index.ts new file mode 100644 index 0000000..c44c69d --- /dev/null +++ b/packages/payment-backend/src/api/index.ts @@ -0,0 +1,5 @@ +import * as PaymentAPI from "./module"; + +export * from "./module"; + +export default PaymentAPI; diff --git a/packages/payment-backend/src/api/module.ts b/packages/payment-backend/src/api/module.ts new file mode 100644 index 0000000..dbb6e9a --- /dev/null +++ b/packages/payment-backend/src/api/module.ts @@ -0,0 +1,5 @@ +export type * from "./IConnection"; +export type * from "./Primitive"; +export * from "./HttpError"; + +export * as functional from "./functional"; diff --git a/packages/payment-backend/src/api/structures/common/IEntity.ts b/packages/payment-backend/src/api/structures/common/IEntity.ts new file mode 100644 index 0000000..07fb86d --- /dev/null +++ b/packages/payment-backend/src/api/structures/common/IEntity.ts @@ -0,0 +1,15 @@ +/** + * 공통 엔티티. + * + * 통상적으로 UUID 타입의 PK 값을 가지는 엔티티 레코드들에 대한 추상 정의. + * + * @author Samchon + */ +export interface IEntity { + /** + * 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 new file mode 100644 index 0000000..6e4df06 --- /dev/null +++ b/packages/payment-backend/src/api/structures/monitors/IPerformance.ts @@ -0,0 +1,5 @@ +export interface IPerformance { + 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 new file mode 100644 index 0000000..500fcd3 --- /dev/null +++ b/packages/payment-backend/src/api/structures/monitors/ISystem.ts @@ -0,0 +1,75 @@ +export interface ISystem { + /** + * Random Unique ID. + */ + uid: string; + + /** + * `process.argv` + */ + arguments: string[]; + + /** + * Git commit information. + */ + commit: ISystem.ICommit; + + /** + * `package.json` */ + package: ISystem.IPackage; + + /** + * 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 user information. + */ + 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[]; + } +} diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts b/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts new file mode 100644 index 0000000..0fbc08c --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentCancelHistory.ts @@ -0,0 +1,101 @@ +import { tags } from "typia"; + +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 { + /** + * 결제의 근간이 된 원천 레코드 정보. + */ + source: IPaymentSource; + + /** + * 결제 이력에 대한 비밀번호 입력. + */ + password: string; + + /** + * 환불 금액. + */ + price: number & tags.Minimum<0>; + + /** + * 결제 취소 사유. + */ + reason: string; + + /** + * 환불 계좌 정보. + * + * 가상 계좌로 입금한 경우, 결제 취소시, 이를 환불받을 계좌가 필요함. + * + * 단, 이 정보는 본 결제 시스템에 저장하지 아니함. + */ + account: null | IBankAccount; + } + + /** + * 은행 계좌 정보. + * + * 가상 계좌로 입금한 경우, 결제 취소시, 이를 환불받을 계좌가 필요함. + * + * 단, 이 정보는 본 결제 시스템에 저장하지 아니함. + */ + export interface IBankAccount { + /** + * 은행 이름. + */ + bank: string; + + /** + * 계좌번호. + */ + account: string; + + /** + * 예금주. + */ + holder: 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 new file mode 100644 index 0000000..079e977 --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentHistory.ts @@ -0,0 +1,171 @@ +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { tags } from "typia"; + +import { IPaymentCancelHistory } from "./IPaymentCancelHistory"; +import { IPaymentSource } from "./IPaymentSource"; +import { IPaymentVendor } from "./IPaymentVendor"; + +/** + * 결제 내역. + * + * `IPaymentHistory` 는 결제 내역을 형상화한 자료구조이자 유니언 타입의 인터페이이다. + * 서비스 시스템으로부 결제 사건의 귀속 정보 ({@link IPaymentHistory.IStore.vendor} + + * {@link IPaymentHistory.IStore.source}) 를 전달받아, 본 Payment 시스템이 PG 사에 + * 추가 정보를 취득하여 레코드가 완성된다. + * + * 그리고 만일 현 결제 건이 가상계좌와 같이 레코드 생성 시점에 지불이 이루어지지 않은 + * 경우라면, 사후 {@link IPaymentWebhook 웹훅 이벤트}를 통하여 지불 완료 시각을 뜻하는 + * {@link paid_at} 가 설정될 수 있다. 그리고 반대로 이미 결제가 완료된 경우라도, + * 환불 등의 이유로 인하여 {@link cancelled_at} 이 사후 기재될 수 있다. + * + * 참고로 `if condition` 을 통하여 {@link IPaymentHistory.vendor_code} 값을 특정하면, + * 파생 타입이 자동으로 다운 캐스팅 된다. + * + * ```typescript + * if (history.vendor_code === "toss.payments") + * history.data.paymentKey; // history.data be ITossPayment + * ``` + * + * @author Samchon + */ +export type IPaymentHistory = + | 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. + */ + id: string & tags.Format<"uuid">; + + /** + * 벤더사 식별자 코드. + * + * {@link IPaymentVendor.code}와 완전히 동일한 값이되, 단지 union type + * specialization 을 위해 중복 표기하였을 뿐이다. `if condition` 을 통하여 + * {@link IPaymentHistory.data}의 타입을 특정할 수 있다. + */ + vendor_code: VendorCode; + + /** + * 벤더 정보. + */ + vendor: IPaymentVendor; + + /** + * 원천 래코드 정보. + */ + source: IPaymentSource; + + /** + * 결제 상세 데이터, 벤더별로 데이터 양식이 다르니 주의할 것. + */ + data: Data; + + /** + * 통화 단위 + * + * KRW, USB, JPY 등. + */ + currency: string; + + /** + * 결제 가격. + */ + price: number & tags.Minimum<0>; + + /** + * 결제 취소시의 환불 금액. + */ + refund: null | (number & tags.Minimum<0>); + + /** + * 결제 정보가 갱신되었을 때, 이를 수신할 URL + */ + webhook_url: null | (string & tags.Format<"url">); + + /** + * 결제 레코드 생성 일시. + */ + created_at: string & tags.Format<"date-time">; + + /** + * 결제 완료 일시. + */ + paid_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">; + + /** + * 결제의 근간이 된 원천 레코드 정보. + */ + 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; + } +} diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts b/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts new file mode 100644 index 0000000..d3fa144 --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentReservation.ts @@ -0,0 +1,116 @@ +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; +import { tags } from "typia"; + +import { IPaymentSource } from "./IPaymentSource"; +import { IPaymentVendor } from "./IPaymentVendor"; + +/** + * 간편 결제 수단 정보. + * + * `IPaymentReservation` 은 간편 결제 수단을 형상화한 자료구조이자 유니언 타입의 + * 인터페이스로써, if condition 을 통하여 {@link IPaymentReservation.vendor_code} 값을 + * 특정하면, 파생 타입이 자동으로 다운 캐스팅 된다. + * + * ```typescript + * if (history.vendor_code === "toss.payments") + * history.data.billingKey; // history.data be ITossBilling + * ``` + * + * @author Samchon + */ +export type IPaymentReservation = + | 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. + */ + id: string & tags.Format<"uuid">; + + /** + * 벤더사 코드. + * + * {@link IPaymentVendor.code}와 완전히 동일한 값이되, 단지 union type + * specialization 을 위해 중복 표기하였을 뿐이다. If else condition 을 통하여 + * {@link IPaymentReservation.data}의 타입을 특정할 수 있다. + */ + vendor_code: VendorCode; + + /** + * 벤더사. + */ + vendor: IPaymentVendor; + + /** + * 대상 액터의 참조 정보. + */ + source: IPaymentSource; + + /** + * 제목. + */ + title: string; + + /** + * 벤더사 데이터. + */ + data: Data; + + /** + * 레코드 생성 일시. + */ + created_at: string & tags.Format<"date-time">; + } + + /** + * 간편 결제 수단 등록 정보. + * + * 결제사의 팝업 창로부터 전달받은 식별자 정보를 취합하여 전달한다. + * + * 참고로 아임포트의 경우 간편 결제로 등록한 카드에 자체 식별자 번호를 부여하지 않는다. + * 따라서 귀하의 서비스가 발행한 식별자 ID 가 곧, 해당 간편 결제 수단의 유일무이한 + * 식별자ㅏ 되니, 이를 {@link IPaymentVendor.uid} 와 {@link IPaymentSource.id} 에 + * 모두 동일하게 할당해주면 된다. + */ + export interface IStore { + /** + * 벤더사 정보. + */ + vendor: IPaymentVendor; + + /** + * 원천 레코드 정보. + */ + source: IPaymentSource; + + /** + * 제목 + */ + title: 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 new file mode 100644 index 0000000..b998534 --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentSource.ts @@ -0,0 +1,53 @@ +import { tags } from "typia"; + +/** + * 원천 레코드 참조 정보. + * + * `IPaymentSource` 는 {@link IPaymentHistory 결제 내역} 및 + * {@link IPaymentResrvation 간편 결제 수단}의 원천이 되는 레코드의 참조 정보를 형상화한 + * 자료구조 인터페이이다. 만일 대상이 {@link IPaymentHistory 결제 내역}이라면 결제의 + * 근원이 되는 주문에 대한 참조 정보를, 대상이 {@link IPaymentResrvation 간편 결제 수단} + * 이라면 이를 기록한 귀사 서비스의 참조 정보를 기입하면 된다. + * + * @author Samchon + */ +export interface IPaymentSource { + /** + * DB 스키마 이름 + */ + schema: string; + + /** + * DB 테이블 명 + */ + table: string; + + /** + * 참조 레코드의 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 {} + + /** + * 비밀번호 입력 정보. + */ + export interface IPassword { + /** + * 레코드 조회를 위한 비밀번호 입력. + */ + password: string; + } +} diff --git a/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts b/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts new file mode 100644 index 0000000..0ddd377 --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentVendor.ts @@ -0,0 +1,51 @@ +/** + * 벤더사 정보. + * + * `IPaymentVendor` 결제 PG 벤더사에 관련된 정보를 형상화한 자료구조 인터페이스이다. + * + * @author Samchon + */ +export interface IPaymentVendor { + /** + * 벤더사 식별자 코드. + * + * 아임포트의 경우에는 `iamport` 를, 토스의 경우에는 `toss.payments` 를 적어주면 된다. + */ + code: Code; + + /** + * 벤더사에 등록한 스토어 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; +} +export namespace IPaymentVendor { + /** + * 벤더사 식별자 코드 타입. + */ + 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 new file mode 100644 index 0000000..6347697 --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentWebhook.ts @@ -0,0 +1,44 @@ +import { tags } from "typia"; + +import { IPaymentHistory } from "./IPaymentHistory"; +import { IPaymentSource } from "./IPaymentSource"; + +/** + * 웹훅 이벤터 데이터. + * + * @author Samchon + */ +export interface IPaymentWebhook { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * 원천 레코드 정보. + */ + source: IPaymentSource; + + /** + * 웹훅 이벤트 수신 전 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하기 전, 즉 이전 상태의 결제 내역 정보. + */ + previous: IPaymentWebhook.IHistory; + + /** + * 웹훅 이벤트 수신 후 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하여, 새로이 바뀌게 된 결제 내역 정보. + */ + current: IPaymentWebhook.IHistory; +} +export namespace IPaymentWebhook { + /** + * 결제 내역 정보. + * + * 본래의 결제 내역 {@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 new file mode 100644 index 0000000..83e43d4 --- /dev/null +++ b/packages/payment-backend/src/api/structures/payments/IPaymentWebhookHistory.ts @@ -0,0 +1,57 @@ +import { tags } from "typia"; + +import { IPaymentHistory } from "./IPaymentHistory"; +import { IPaymentSource } from "./IPaymentSource"; + +/** + * 웹훅 이벤트 레코드. + * + * `IPaymentHistoryWebhook` 는 PG 벤더사로부터의 이벤트를 기록하는 엔티티이다. + * + * 웹훅 이벤트는 고객이 가상계좌를 선택하고 사후에 지불을 완료했다던가, 이미 결제한 + * 금액을 환불하여 결제가 취소되었다던가 하는 등의 이유로 발생한다. 그리고 웹훅 이벤트 + * 레코드의 발생은 곧, 원천 결제 레코드에 해당하는 {@link IPaymentHistory.data} 의 + * 수정을 불러온다. + * + * 때문에 `IPaymentHistoryWebhook` 에는 {@link previous} 라 하여, 웹훅 이벤트가 + * 발생하기 전의 {@link PaymentHistory.data} 를 기록하는 속성이 존재한다. 만일 웹훅 + * 이벤트가 발생하여 변동된 데이터를 보고 싶다면, 현 웹훅 이벤트가 가장 최신인지 여부를 + * 따져, {@link IPaymentHistory.data} 를 조회하던가 아니면 그 다음 웹훅 레코드의 + * {@link previous} 를 조회하던가 하면 된다. + * + * @author Samchon + */ +export interface IPaymentWebhookHistory { + /** + * Primary Key. + */ + id: string & tags.Format<"uuid">; + + /** + * 원천 레코드 정보. + */ + source: IPaymentSource; + + /** + * 웹훅 이벤트 수신 전 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하기 전, 즉 이전 상태의 결제 내역 정보. + */ + previous: IPaymentWebhookHistory.IHistory; + + /** + * 웹훅 이벤트 수신 후 결제 내역 정보. + * + * PG 사로부터 웹훅 이벤트 데이터를 수신하여, 새로이 바뀌게 된 결제 내역 정보. + */ + current: IPaymentWebhookHistory.IHistory; +} +export namespace IPaymentWebhookHistory { + /** + * 결제 내역 정보. + * + * 본래의 결제 내역 {@link IPaymentHistory} 에서 중복되는 원천 레코드 정보 + * {@link IPaymentSource} 를 뺌. + */ + export type IHistory = Omit; +} diff --git a/packages/payment-backend/src/api/typings/Atomic.ts b/packages/payment-backend/src/api/typings/Atomic.ts new file mode 100644 index 0000000..61a6dee --- /dev/null +++ b/packages/payment-backend/src/api/typings/Atomic.ts @@ -0,0 +1,14 @@ +/** + * @packageDocumentation + * @module api.typings + */ +//================================================================ +/** + * 객체 정의로부터 원자 멤버들만의 타입을 추려냄. + * + * @template Instance 대상 객체의 타입 + * @author Samchon + */ +export type Atomic = { + [P in keyof Instance]: Instance[P] extends object ? never : Instance[P]; +}; diff --git a/packages/payment-backend/src/api/typings/Writable.ts b/packages/payment-backend/src/api/typings/Writable.ts new file mode 100644 index 0000000..0ed6231 --- /dev/null +++ b/packages/payment-backend/src/api/typings/Writable.ts @@ -0,0 +1,12 @@ +/** + * @packageDocumentation + * @module api.typings + */ +//================================================================ +export type Writable = { + -readonly [P in keyof T]: T[P]; +}; + +export function Writable(elem: Readonly): Writable { + return elem; +} diff --git a/packages/payment-backend/src/api/utils/NestiaSimulator.ts b/packages/payment-backend/src/api/utils/NestiaSimulator.ts new file mode 100644 index 0000000..085ff9f --- /dev/null +++ b/packages/payment-backend/src/api/utils/NestiaSimulator.ts @@ -0,0 +1,70 @@ +import { HttpError } from "@nestia/fetcher"; + +import typia from "typia"; + +export namespace NestiaSimulator { + export interface IProps { + host: string; + path: string; + method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + contentType: string; + } + + export const assert = (props: IProps) => { + return { + param: param(props), + query: query(props), + body: body(props), + }; + }; + const param = + (props: IProps) => + (name: string) => + (task: () => T): void => { + validate( + (exp) => `URL parameter "${name}" is not ${exp.expected} type.`, + )(props)(task); + }; + + const query = + (props: IProps) => + (task: () => T): void => + validate( + () => + "Request query parameters are not following the promised type.", + )(props)(task); + + const body = + (props: IProps) => + (task: () => T): void => + validate(() => "Request body is not following the promised type.")( + props, + )(task); + + const validate = + (message: (exp: typia.TypeGuardError) => string, path?: string) => + (props: IProps) => + (task: () => T): void => { + try { + task(); + } catch (exp) { + if (typia.is(exp)) + throw new HttpError( + props.method, + props.host + props.path, + 400, + { + "Content-Type": props.contentType, + }, + JSON.stringify({ + method: exp.method, + path: path ?? exp.path, + expected: exp.expected, + value: exp.value, + message: message(exp), + }), + ); + throw exp; + } + }; +} diff --git a/packages/payment-backend/src/controllers/monitors/HealthController.ts b/packages/payment-backend/src/controllers/monitors/HealthController.ts new file mode 100644 index 0000000..b1d65a8 --- /dev/null +++ b/packages/payment-backend/src/controllers/monitors/HealthController.ts @@ -0,0 +1,7 @@ +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/PerformanceController.ts b/packages/payment-backend/src/controllers/monitors/PerformanceController.ts new file mode 100644 index 0000000..0147690 --- /dev/null +++ b/packages/payment-backend/src/controllers/monitors/PerformanceController.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..47809d9 --- /dev/null +++ b/packages/payment-backend/src/controllers/monitors/SystemController.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..2b91585 --- /dev/null +++ b/packages/payment-backend/src/controllers/payments/PaymentHistoriesController.ts @@ -0,0 +1,77 @@ +import nest from "@modules/nestjs"; +import core from "@nestia/core"; +import { IPaymentCancelHistory } from "payment-api/lib/structures/payments/IPaymentCancelHistory"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentSource } from "payment-api/lib/structures/payments/IPaymentSource"; +import { tags } from "typia"; + +import { PaymentCancelHistoryProvider } from "../../providers/payments/PaymentCancelHistoryProvider"; +import { PaymentHistoryProvider } from "../../providers/payments/PaymentHistoryProvider"; + +@nest.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 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 결제 취소 내역 입력 정보 + * + * @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 new file mode 100644 index 0000000..dfbe5ab --- /dev/null +++ b/packages/payment-backend/src/controllers/payments/PaymentInternalController.ts @@ -0,0 +1,16 @@ +import nest from "@modules/nestjs"; +import core from "@nestia/core"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; + +import { FakePaymentStorage } from "../../providers/payments/FakePaymentStorage"; + +@nest.Controller("payments/internal") +export class PaymentInternalController { + /** + * @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 new file mode 100644 index 0000000..7b35e2b --- /dev/null +++ b/packages/payment-backend/src/controllers/payments/PaymentReservationsController.ts @@ -0,0 +1,55 @@ +import nest from "@modules/nestjs"; +import core from "@nestia/core"; +import { IPaymentReservation } from "payment-api/lib/structures/payments/IPaymentReservation"; +import { IPaymentSource } from "payment-api/lib/structures/payments/IPaymentSource"; +import { tags } from "typia"; + +import { PaymentReservationProvider } from "../../providers/payments/PaymentReservationProvider"; + +@nest.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 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); + } +} diff --git a/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts b/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts new file mode 100644 index 0000000..248bc1e --- /dev/null +++ b/packages/payment-backend/src/controllers/payments/PaymentWebhooksController.ts @@ -0,0 +1,60 @@ +import nest from "@modules/nestjs"; +import core from "@nestia/core"; +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"; + +import { PaymentWebhookProvider } from "../../providers/payments/PaymentWebhookProvider"; +import { IamportPaymentService } from "../../services/iamport/IamportPaymentService"; +import { TossPaymentService } from "../../services/toss/TossPaymentService"; + +@nest.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); + } + + /** + * @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 new file mode 100644 index 0000000..17a0bab --- /dev/null +++ b/packages/payment-backend/src/executable/master.ts @@ -0,0 +1,9 @@ +import { PaymentUpdator } from "../PaymentUpdator"; + +async function main(): Promise { + await PaymentUpdator.master(); +} +main().catch((exp) => { + console.error(exp); + process.exit(-1); +}); diff --git a/packages/payment-backend/src/executable/monitor.ts b/packages/payment-backend/src/executable/monitor.ts new file mode 100644 index 0000000..da93cc4 --- /dev/null +++ b/packages/payment-backend/src/executable/monitor.ts @@ -0,0 +1,30 @@ +import { PaymentConfiguration } from "../PaymentConfiguration"; +import { PaymentGlobal } from "../PaymentGlobal"; +import PaymentAPI from "../api"; +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"); + + // 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 }); +} +main().catch((exp) => { + console.log(exp); + process.exit(-1); +}); diff --git a/packages/payment-backend/src/executable/schema.ts b/packages/payment-backend/src/executable/schema.ts new file mode 100644 index 0000000..2c5ff9b --- /dev/null +++ b/packages/payment-backend/src/executable/schema.ts @@ -0,0 +1,84 @@ +import { PrismaClient } from "@prisma/client"; + +import { PaymentGlobal } from "../PaymentGlobal"; + +async function execute( + database: string, + username: string, + password: string, + script: string, +): Promise { + try { + const prisma = new PrismaClient({ + datasources: { + db: { + url: `postgresql://${username}:${password}@${PaymentGlobal.env.POSTGRES_HOST}:${PaymentGlobal.env.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(); + } + await prisma.$disconnect(); + } catch (err) { + console.log(err); + } +} + +async function main(): Promise { + const config = { + database: PaymentGlobal.env.POSTGRES_DATABASE, + schema: PaymentGlobal.env.POSTGRES_SCHEMA, + username: PaymentGlobal.env.POSTGRES_USERNAME, + readonlyUsername: PaymentGlobal.env.POSTGRES_USERNAME_READONLY, + password: PaymentGlobal.env.POSTGRES_PASSWORD, + }; + const root = { + account: process.argv[2] ?? "postgres", + password: process.argv[3] ?? "root", + }; + + 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, + ` + CREATE SCHEMA ${config.schema} AUTHORIZATION ${config.username}; + `, + ); + + 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}; + `, + ); +} +main().catch((exp) => { + console.log(exp); + process.exit(-1); +}); diff --git a/packages/payment-backend/src/executable/server.ts b/packages/payment-backend/src/executable/server.ts new file mode 100644 index 0000000..5e757bc --- /dev/null +++ b/packages/payment-backend/src/executable/server.ts @@ -0,0 +1,68 @@ +import fs from "fs"; +import { randint } from "tstl/algorithm/random"; +import { Singleton } from "tstl/thread/Singleton"; + +import { PaymentBackend } from "../PaymentBackend"; +import { PaymentGlobal } from "../PaymentGlobal"; +import { Scheduler } from "../schedulers/Scheduler"; +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`); +}); + +function cipher(val: number): string { + if (val < 10) return "0" + val; + else return String(val); +} + +async function mkdir(path: string): Promise { + 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 {} +} + +async function main(): Promise { + // 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); + + // SCHEDULER ONLY WHEN MASTER + if (PaymentGlobal.mode !== "real" || process.argv[3] === "master") + await Scheduler.repeat(); +} +main().catch((exp) => { + console.log(exp); + process.exit(-1); +}); diff --git a/packages/payment-backend/src/executable/update.ts b/packages/payment-backend/src/executable/update.ts new file mode 100644 index 0000000..81613ab --- /dev/null +++ b/packages/payment-backend/src/executable/update.ts @@ -0,0 +1,57 @@ +import { MutexConnector, RemoteMutex } from "mutex-server"; +import { Promisive } from "tgrid/typings/Promisive"; +import { UniqueLock } from "tstl/thread/UniqueLock"; + +import { PaymentConfiguration } from "../PaymentConfiguration"; +import { PaymentGlobal } from "../PaymentGlobal"; +import { PaymentUpdator } from "../PaymentUpdator"; +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`, + ); + + // 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(); + + // 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); +}); diff --git a/packages/payment-backend/src/executable/updator-master.ts b/packages/payment-backend/src/executable/updator-master.ts new file mode 100644 index 0000000..21465b9 --- /dev/null +++ b/packages/payment-backend/src/executable/updator-master.ts @@ -0,0 +1,10 @@ +import { PaymentUpdator } from "../PaymentUpdator"; + +async function main(): Promise { + await PaymentUpdator.master(); + await PaymentUpdator.slave("127.0.0.1"); +} +main().catch((exp) => { + 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 new file mode 100644 index 0000000..1ec724e --- /dev/null +++ b/packages/payment-backend/src/executable/updator-slave.ts @@ -0,0 +1,9 @@ +import { PaymentUpdator } from "../PaymentUpdator"; + +async function main(): Promise { + await PaymentUpdator.slave(); +} +main().catch((exp) => { + console.error(exp); + process.exit(-1); +}); diff --git a/packages/payment-backend/src/index.ts b/packages/payment-backend/src/index.ts new file mode 100644 index 0000000..230bd3f --- /dev/null +++ b/packages/payment-backend/src/index.ts @@ -0,0 +1,5 @@ +import * as pts from "./module"; + +export * from "./module"; + +export default pts; diff --git a/packages/payment-backend/src/migrations/20230919094622_init/migration.sql b/packages/payment-backend/src/migrations/20230919094622_init/migration.sql new file mode 100644 index 0000000..2198afc --- /dev/null +++ b/packages/payment-backend/src/migrations/20230919094622_init/migration.sql @@ -0,0 +1,109 @@ +-- CreateTable +CREATE TABLE "payment_reservations" ( + "id" UUID NOT NULL, + "source_schema" VARCHAR NOT NULL, + "source_table" VARCHAR NOT NULL, + "source_id" VARCHAR NOT NULL, + "vendor_code" VARCHAR NOT NULL, + "vendor_uid" VARCHAR NOT NULL, + "vendor_store_id" VARCHAR NOT NULL, + "title" VARCHAR NOT NULL, + "data" TEXT NOT NULL, + "password" VARCHAR NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "payment_reservations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "payment_histories" ( + "id" UUID NOT NULL, + "source_schema" VARCHAR NOT NULL, + "source_table" VARCHAR NOT NULL, + "source_id" VARCHAR NOT NULL, + "vendor_code" VARCHAR NOT NULL, + "vendor_uid" VARCHAR NOT NULL, + "vendor_store_id" VARCHAR NOT NULL, + "currency" VARCHAR NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "refund" DOUBLE PRECISION, + "webhook_url" VARCHAR(1024) NOT NULL, + "data" TEXT NOT NULL, + "password" VARCHAR NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL, + "paid_at" TIMESTAMPTZ, + "cancelled_at" TIMESTAMPTZ, + + CONSTRAINT "payment_histories_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "payment_history_cancels" ( + "id" UUID NOT NULL, + "payment_history_id" UUID NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "reason" TEXT NOT NULL, + "data" TEXT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "payment_history_cancels_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "payment_history_webhooks" ( + "id" UUID NOT NULL, + "payment_history_id" UUID NOT NULL, + "previous" TEXT NOT NULL, + "current" TEXT NOT NULL, + "data" TEXT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "payment_history_webhooks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "payment_history_webhook_responses" ( + "id" UUID NOT NULL, + "payment_history_webhook_id" UUID NOT NULL, + "status" INTEGER, + "body" TEXT, + "created_at" TIMESTAMPTZ NOT NULL, + + CONSTRAINT "payment_history_webhook_responses_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "payment_reservations_created_at_idx" ON "payment_reservations"("created_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_reservations_source_schema_source_table_source_id_key" ON "payment_reservations"("source_schema", "source_table", "source_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_reservations_vendor_code_vendor_uid_key" ON "payment_reservations"("vendor_code", "vendor_uid"); + +-- CreateIndex +CREATE INDEX "payment_histories_created_at_idx" ON "payment_histories"("created_at"); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_histories_source_schema_source_table_source_id_key" ON "payment_histories"("source_schema", "source_table", "source_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "payment_histories_vendor_code_vendor_uid_key" ON "payment_histories"("vendor_code", "vendor_uid"); + +-- CreateIndex +CREATE INDEX "payment_history_cancels_payment_history_id_idx" ON "payment_history_cancels"("payment_history_id"); + +-- CreateIndex +CREATE INDEX "payment_history_webhooks_payment_history_id_idx" ON "payment_history_webhooks"("payment_history_id"); + +-- CreateIndex +CREATE INDEX "payment_history_webhook_responses_payment_history_webhook_i_idx" ON "payment_history_webhook_responses"("payment_history_webhook_id"); + +-- AddForeignKey +ALTER TABLE "payment_history_cancels" ADD CONSTRAINT "payment_history_cancels_payment_history_id_fkey" FOREIGN KEY ("payment_history_id") REFERENCES "payment_histories"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "payment_history_webhooks" ADD CONSTRAINT "payment_history_webhooks_payment_history_id_fkey" FOREIGN KEY ("payment_history_id") REFERENCES "payment_histories"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "payment_history_webhook_responses" ADD CONSTRAINT "payment_history_webhook_responses_payment_history_webhook__fkey" FOREIGN KEY ("payment_history_webhook_id") REFERENCES "payment_history_webhooks"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/payment-backend/src/migrations/migration_lock.toml b/packages/payment-backend/src/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/packages/payment-backend/src/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/packages/payment-backend/src/module.ts b/packages/payment-backend/src/module.ts new file mode 100644 index 0000000..5341a4e --- /dev/null +++ b/packages/payment-backend/src/module.ts @@ -0,0 +1,6 @@ +export * from "./PaymentAsset"; +export * from "./PaymentBackend"; +export * from "./PaymentConfiguration"; +export * from "./PaymentGlobal"; +export * from "./PaymentSetupWizard"; +export * from "./PaymentUpdator"; diff --git a/packages/payment-backend/src/modules/express.ts b/packages/payment-backend/src/modules/express.ts new file mode 100644 index 0000000..3a67624 --- /dev/null +++ b/packages/payment-backend/src/modules/express.ts @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..b375de2 --- /dev/null +++ b/packages/payment-backend/src/modules/nestjs.ts @@ -0,0 +1,3 @@ +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 new file mode 100644 index 0000000..b606ee9 --- /dev/null +++ b/packages/payment-backend/src/providers/monitors/SystemProvider.ts @@ -0,0 +1,54 @@ +import fs from "fs"; +import git from "git-last-commit"; +import { Singleton } from "tstl/thread/Singleton"; +import { v4 } from "uuid"; + +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 package(): Promise { + return package_.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, + ), + }); + }); + }), +); +const package_: Singleton> = new Singleton( + async () => { + const content: string = await fs.promises.readFile( + `${__dirname}/../../../../package.json`, + "utf8", + ); + return JSON.parse(content); + }, +); + +commit_.get().catch(() => {}); +package_.get().catch(() => {}); diff --git a/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts b/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts new file mode 100644 index 0000000..e2c18de --- /dev/null +++ b/packages/payment-backend/src/providers/payments/FakePaymentStorage.ts @@ -0,0 +1,6 @@ +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; +import { Vector } from "tstl/container/Vector"; + +export namespace FakePaymentStorage { + 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 new file mode 100644 index 0000000..df512a4 --- /dev/null +++ b/packages/payment-backend/src/providers/payments/PaymentCancelHistoryProvider.ts @@ -0,0 +1,109 @@ +import nest from "@modules/nestjs"; +import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { Prisma } from "@prisma/client"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IEntity } from "payment-api/lib/structures/common/IEntity"; +import { IPaymentCancelHistory } from "payment-api/lib/structures/payments/IPaymentCancelHistory"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { sleep_for } from "tstl"; +import { v4 } from "uuid"; + +import { PaymentGlobal } from "../../PaymentGlobal"; +import { IamportPaymentService } from "../../services/iamport/IamportPaymentService"; +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 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); + await sleep_for(50); + 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, + }); + + 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.`); + }; +} + +const encrypt = (value: string) => AesPkcs5.encrypt(value, KEY(), IV()); +// const decrypt = (value: string) => AesPkcs5.decrypt(value, KEY(), IV()); +const KEY = () => PaymentGlobal.env.DB_CANCEL_HISTORY_ENCRYPTION_KEY ?? ""; +const IV = () => PaymentGlobal.env.DB_CANCEL_HISTORY_ENCRYPTION_IV ?? ""; diff --git a/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts b/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts new file mode 100644 index 0000000..fcc48a1 --- /dev/null +++ b/packages/payment-backend/src/providers/payments/PaymentHistoryProvider.ts @@ -0,0 +1,205 @@ +import nest from "@modules/nestjs"; +import "@nestia/fetcher"; +import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { Prisma } from "@prisma/client"; +import { HttpError } from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IEntity } from "payment-api/lib/structures/common/IEntity"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { v4 } from "uuid"; + +import { PaymentGlobal } from "../../PaymentGlobal"; +import { IamportPaymentService } from "../../services/iamport/IamportPaymentService"; +import { TossPaymentService } from "../../services/toss/TossPaymentService"; +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 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); + }; + + /* ----------------------------------------------------------- + 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(), + ); + } + + /* ----------------------------------------------------------- + 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 const update = + (history: IEntity) => + async (input: IPaymentHistory.IProps): Promise => { + // RE-CONSTRUCT CANCEL HISTORIES + await PaymentGlobal.prisma.payment_history_cancels.deleteMany({ + where: { payment_history_id: history.id }, + }); + await PaymentGlobal.prisma.payment_history_cancels.createMany({ + data: input.cancels.map( + PaymentCancelHistoryProvider.collect(history), + ), + }); + + // UPDATE HISTORY + const record = 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(), + }); + 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.`); + } +} + +const encrypt = (value: string) => AesPkcs5.encrypt(value, KEY(), IV()); +const decrypt = (value: string) => AesPkcs5.decrypt(value, KEY(), IV()); +const KEY = () => PaymentGlobal.env.DB_HISTORY_ENCRYPTION_KEY ?? ""; +const IV = () => PaymentGlobal.env.DB_HISTORY_ENCRYPTION_IV ?? ""; diff --git a/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts b/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts new file mode 100644 index 0000000..1d2b8cf --- /dev/null +++ b/packages/payment-backend/src/providers/payments/PaymentReservationProvider.ts @@ -0,0 +1,119 @@ +import nest from "@modules/nestjs"; +import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { Prisma } from "@prisma/client"; +import imp from "iamport-server-api"; +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import { IPaymentReservation } from "payment-api/lib/structures/payments/IPaymentReservation"; +import toss from "toss-payments-server-api"; +import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; +import { v4 } from "uuid"; + +import { PaymentGlobal } from "../../PaymentGlobal"; +import { IamportAsset } from "../../services/iamport/IamportAsset"; +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 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 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_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()); +const decrypt = (value: string) => AesPkcs5.decrypt(value, KEY(), IV()); +const KEY = () => PaymentGlobal.env.DB_RESERVATION_ENCRYPTION_KEY ?? ""; +const IV = () => PaymentGlobal.env.DB_RESERVATION_ENCRYPTION_IV ?? ""; diff --git a/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts b/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts new file mode 100644 index 0000000..39917b9 --- /dev/null +++ b/packages/payment-backend/src/providers/payments/PaymentWebhookProvider.ts @@ -0,0 +1,113 @@ +import "@nestia/fetcher"; +import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { payment_histories, payment_history_webhooks } from "@prisma/client"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; +import { InvalidArgument } from "tstl/exception/InvalidArgument"; +import { v4 } from "uuid"; + +import { PaymentConfiguration } from "../../PaymentConfiguration"; +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; + + // 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); + + // 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; + + 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(), + }, + }); + } +} diff --git a/packages/payment-backend/src/schedulers/Scheduler.ts b/packages/payment-backend/src/schedulers/Scheduler.ts new file mode 100644 index 0000000..4ccc9b1 --- /dev/null +++ b/packages/payment-backend/src/schedulers/Scheduler.ts @@ -0,0 +1,30 @@ +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 new file mode 100644 index 0000000..88c1668 --- /dev/null +++ b/packages/payment-backend/src/schedulers/features/schedule_something_in_every_day.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..ca63c5a --- /dev/null +++ b/packages/payment-backend/src/schedulers/features/schedule_something_in_every_hour.ts @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..55f10f3 --- /dev/null +++ b/packages/payment-backend/src/schedulers/features/schedule_something_in_every_minutes.ts @@ -0,0 +1,3 @@ +export async function schedule_something_in_every_minutes(): Promise { + // DO SOMETHING +} diff --git a/packages/payment-backend/src/schema.prisma b/packages/payment-backend/src/schema.prisma new file mode 100644 index 0000000..c7a5e83 --- /dev/null +++ b/packages/payment-backend/src/schema.prisma @@ -0,0 +1,334 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("POSTGRES_URL") +} + +generator markdown { + provider = "prisma-markdown" + title = "Payments System" + output = "../docs/ERD.md" +} + +/// 간편 결제 수단 정보. +/// +/// `payment_reservations` 는 고객이 신용카드 등을 정기 결제 수단으로 등록, +/// 매 결제시마다 반복된 결제 수단 인증 작업을 안 해도 되게끔 하는, +/// 이른바 "간편 결제" 에 대한 레코드 정보를 담은 테이블이다. +/// +/// 참고로 간편 결제에 대한 레코드를 저장할 때, 서비스 시스템은 해당 레코드에 대하여 +/// 비밀번호를 지정하여야 한다. 비밀번호를 유실한 경우, 해당 레코드는 그 누구도 조회할 +/// 수 없으니 서비스 시스템은 이를 잘 관리하여야 한다. 참고로 이 비밀번호는 유저의 +/// 비밀번호와 일절 무관하여야 하며, 오직 서비스 시스템이 스스로 임의 발급한 +/// 비밀번호여야만 한다. +/// +/// 이외에 PG사의 간편 결제 정보는 {@link payment_reservations.data} 에 저장된다. +/// +/// @namespace Payments +/// @author Samchon +model payment_reservations { + //---- + // COLUMNS + //---- + /// @format uuid + id String @id @db.Uuid + + /// 결제를 발생시킨 원천 레코드의 스키마 + source_schema String @db.VarChar + + /// 결제를 발생시킨 원천 레코드의 테이블 + source_table String @db.VarChar + + /// 결제를 발생시킨 원천 레코드의 ID + source_id String @db.VarChar + + /// PG 벤더사의 코드. + /// + /// ex) "iamport" + vendor_code String @db.VarChar + + /// PG 벤더사에서 발급해준 간편 결제 식별자 키 + vendor_uid String @db.VarChar + + /// PG 벤더사에서의 가맹점 ID. + /// + /// PG 벤더사 내에서의 회원사 ID 와 진배 없다. + vendor_store_id String @db.VarChar + + /// 제목. + title String @db.VarChar + + /// PG 벤더사로부터의 간편 결제에 대한 상세 데이터. + data String + + /// 레코드 비밀번호. + /// + /// 서비스 시스템이 임의로 발급한 비밀번호로써, 유저와는 하등 무관해야 함. + password String @db.VarChar + + /// 레코드 생성 일시. + created_at DateTime @db.Timestamptz + + @@unique([source_schema, source_table, source_id]) // 서비스 원천에 대한 UK + @@unique([vendor_code, vendor_uid]) // PG 벤더에 대한 UK + @@index([created_at]) +} + +/// 결제 (신청/지불) 정보. +/// +/// `payment_histories` 는 PG 시스템을 통한 결제 신청 혹은 지불 정보를 기록한 엔티티이다. +/// 서비스 시스템으로부 결제 사건의 귀속 정보 (vendor_X` + `source_Y`) 를 전달받아, +/// 본 Payment 시스템이 PG 사에 추가 정보를 취득하여 레코드가 완성된다. +/// +/// 그리고 만일 현 결제 건이 가상계좌와 같이 레코드 생성 시점에 지불이 이루어지지 않은 +/// 경우라면, 사후 {@link payment_history_webhooks 웹훅 이벤트}를 통하여 지불 완료 +/// 시각을 뜻하는 {@link payment_histories.paid_at} 이 설정될 수 있다. 반대로 이미 +/// 결제가 완료된 경우라도, 환불 등의 이유로 {@link payment_histories.cancelled_at} 이 +/// 사후 기재될 수 있다. +/// +/// @namespace Payments +/// @author Samchon +model payment_histories { + //---- + // COLUMNS + //---- + /// @format uuid + id String @id @db.Uuid + + /// 결제를 발생시킨 원천 레코드의 스키마 + source_schema String @db.VarChar + + /// 결제를 발생시킨 원천 레코드의 테이블 + source_table String @db.VarChar + + /// 결제를 발생시킨 원천 레코드의 ID + source_id String @db.VarChar + + /// PG 벤더사의 코드. + /// + /// ex) "iamport" + vendor_code String @db.VarChar + + /// PG 벤더사에서 발급해준 간편 결제 식별자 키 + vendor_uid String @db.VarChar + + /// PG 벤더사에서의 가맹점 ID. + /// + /// PG 벤더사 내에서의 회원사 ID 와 진배 없다. + vendor_store_id String @db.VarChar + + /// 기준 통화. + /// + /// KRW, USB, JPY 등. + currency String @db.VarChar + + /// 결제 가격. + /// + /// @minimum 0 + price Float @db.DoublePrecision + + /// 결제 취소 시의 환불 금액. + /// + /// @minimum 0 + refund Float? @db.DoublePrecision + + /// 웹훅 URL. + /// + /// 웹훅 데이터를 받아갈 서비스 시스템의 URL. + /// + /// @format url + webhook_url String @db.VarChar(1024) + + /// PG 벤더사로부터의 결제 이력에 대한 상세 데이터. + /// + /// JSON-string 이 암호화되어 저장되어있다. + /// + /// @warning 웹훅에 의해 상시 변동될 수 있음. + data String + + /// 레코드 비밀번호. + /// + /// 서비스 시스템이 임의로 발급한 비밀번호로써, 유저와는 하등 무관해야 함. + password String @db.VarChar + + /// 레코드 생성 일시 + created_at DateTime @db.Timestamptz + + /// 결제 완료 일시. + /// + /// 가상 계좌와 같이, 레코드 생성 일시와 결제 완료 일시가 다를 수 있다. + paid_at DateTime? @db.Timestamptz + + /// 결제 취소 시간. + /// + /// 단, 결제 취소가 여러번에 걸쳐 일어난 경우 (부분 환불), + /// 가장 첫 번째의 취소 시간을 기록한다. + cancelled_at DateTime? @db.Timestamptz + + //---- + // RELATIONs + //---- + // 웹훅 리스트. + webhooks payment_history_webhooks[] + + /// 결제 취소 사건 리스트. + cancels payment_history_cancels[] + + @@unique([source_schema, source_table, source_id]) + @@unique([vendor_code, vendor_uid]) + @@index([created_at]) +} + +/// 결제 취소 이력. +/// +/// 결제 취소 이력을 기록한 엔티티. +/// +/// 단, 본 시스템을 통하지 않고 각각의 PG사로 직접 접속하여 결제 취소한 경우, +/// 그 내역이 정확하지 않을 수 있다. 다만 PG사로부터 웹훅 이벤트 발생시, 매번 데이터를 +/// 갱신하고 있기에, 그 부정확함이 영구적이지는 않다. +/// +/// @namespace Payments +/// @author Samchon +model payment_history_cancels { + //---- + // COLUMNS + //---- + /// @format uuid + id String @id @db.Uuid + + /// 귀속 결제 이력의 {@link payment_histories.id} + /// + /// @format uuid + payment_history_id String @db.Uuid + + /// 환불 금액. + /// + /// @minimum 0 + amount Float @db.DoublePrecision + + /// 취소 사유. + reason String + + /// PG 벤더사로부터의 결제 취소 이력에 대한 상세 데이터. + /// + /// JSON-string 이 암호화되어 저장되어있다. + data String + + /// 결제 취소 시간. + created_at DateTime @db.Timestamptz + + //---- + // RELATIONS + //---- + /// 귀속 결제 이력. + history payment_histories @relation(fields: [payment_history_id], references: [id], onDelete: Cascade) + + @@index([payment_history_id]) +} + +/// 웹훅 이벤트 레코드. +/// +/// `payment_history_webhooks` 는 PG 벤더사로부터의 이벤트를 기록하는 엔티티이다. +/// +/// 웹훅 이벤트는 고객이 가상계좌를 선택하고 사후에 지불을 완료했다던가, 이미 결제한 +/// 금액을 환불하여 결제가 취소되었다던가 하는 등의 이유로 발생한다. 그리고 웹훅 이벤트 +/// 레코드의 발생은 곧, 원천 결제 레코드에 해당하는 {@link payment_histories.data} 의 +/// 수정을 불러온다. +/// +/// 때문에 {@link payment_history_webhooks.previous} 라 하여, 웹훅 이벤트가 발생하기 +/// 전의 {@link payment_histories.data} 를 기록하는 속성이 존재한다. 만일 웹훅 +/// 이벤트가 발생하여 변동된 데이터를 보고 싶다면, 현 웹훅 이벤트가 가장 최신인지 +/// 여부를 따져, {@link payment_histories.data} 를 조회하던가 아니면 그 다음 웹훅 +/// 레코드의 {@link payment_history_webhooks.previous} 를 조회하던가 하면 된다. +/// +/// @namespace Payments +/// @author Samchon +model payment_history_webhooks { + //---- + // COLUMNS + //---- + /// @format uuid + id String @id @db.Uuid + + /// 귀속 결제 이력의 {@link payment_histories.id} + /// + /// @format uuid + payment_history_id String @db.Uuid + + /// 이전 결제 이력의 {@link payment_histories.data} + previous String + + /// 웹훅 이후의 결제 이력에 대한 {@link payment_histories.data} + current String + + /// PG 벤더사로부터 받은 웹훅 데이터. + /// + /// {@link payment_histories.data} 와는 다르다 + data String + + /// 웹훅 레코드 생성 일시. + created_at DateTime @db.Timestamptz + + //---- + // RELATIONS + //---- + /// 귀속 결제 이력. + history payment_histories @relation(fields: [payment_history_id], references: [id], onDelete: Cascade) + + /// 웹훅에 대한 서비스 시스템의 응답 리스트. + /// + /// @minLength 1 + responses payment_history_webhook_responses[] + + @@index([payment_history_id]) +} + +/// 서비스 시스템의 웹훅에 대한 응답 이력 레코드. +/// +/// `payment_history_webhook_responses` 는 서비스 시스템이 본 Payment 시스템으로부터 +/// 전송받은 웹훅 이벤트에 대한 응답을 기록하는 엔티티이다. 만일 서비스 시스템이 +/// 먹통이라 데이터 수신 자체를 못하는 상황이라면, `status` 와 `body` 데이터 모두 +/// `NULL` 값이 부여된다. +/// +/// 그리고 본 Payment 시스템이 서비스 시스템으로 전송하는 웹훅 이벤트 데이터 또한, +/// 본 시스템의 여타 API 와 같이 `body` 부문이 암호화 처리되어 전송된다. 자세한 내용은 +/// 본 저장소의 README 문서를 참고할 것. +/// +/// @namespace Payments +/// @author Samchon +model payment_history_webhook_responses { + //---- + // COLUMNS + //---- + /// @format uuid + id String @id @db.Uuid + + /// 귀속 웹훅의 {@link payment_history_webhooks.id} + /// + /// @format uuid + payment_history_webhook_id String @db.Uuid + + /// HTTP 응답 상태 코드. + /// + /// @type uint + /// @minimum 200 + /// @exclusiveMaximum 600 + status Int? + + /// HTTP 응답 본문. + body String? + + /// 레코드 생성 일시. + created_at DateTime @db.Timestamptz + + //---- + // RELATIONS + //---- + // 귀속 웹훅 정보. + webhook payment_history_webhooks @relation(fields: [payment_history_webhook_id], references: [id], onDelete: Cascade) + + @@index([payment_history_webhook_id]) +} diff --git a/packages/payment-backend/src/services/iamport/IamportAsset.ts b/packages/payment-backend/src/services/iamport/IamportAsset.ts new file mode 100644 index 0000000..410002e --- /dev/null +++ b/packages/payment-backend/src/services/iamport/IamportAsset.ts @@ -0,0 +1,38 @@ +import fake from "fake-iamport-server"; +import imp from "iamport-server-api"; +import { VariadicSingleton } from "tstl/thread/VariadicSingleton"; + +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(); + } + + 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 new file mode 100644 index 0000000..d4e054b --- /dev/null +++ b/packages/payment-backend/src/services/iamport/IamportPaymentService.ts @@ -0,0 +1,82 @@ +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportPaymentCancel } from "iamport-server-api/lib/structures/IIamportPaymentCancel"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { DomainError } from "tstl/exception/DomainError"; + +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 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 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 new file mode 100644 index 0000000..5313b10 --- /dev/null +++ b/packages/payment-backend/src/services/toss/TossAsset.ts @@ -0,0 +1,28 @@ +import btoa from "btoa"; +import fake from "fake-toss-payments-server"; +import toss from "toss-payments-server-api"; + +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.TossFakeConfiguration.API_PORT}` + : "https://api.tosspayments.com"; + const token: string = btoa( + PaymentConfiguration.TOSS_SECRET_KEY(storeId) + ":", + ); + + return { + host, + headers: { + Authorization: `Basic ${token}`, + "Content-Type": "application/json", + }, + }; + } +} diff --git a/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts b/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts new file mode 100644 index 0000000..4e6cc23 --- /dev/null +++ b/packages/payment-backend/src/services/toss/TossPaymentBillingService.ts @@ -0,0 +1,39 @@ +import toss from "toss-payments-server-api"; +import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; + +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 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, + ); + } +} diff --git a/packages/payment-backend/src/services/toss/TossPaymentCardService.ts b/packages/payment-backend/src/services/toss/TossPaymentCardService.ts new file mode 100644 index 0000000..57a9570 --- /dev/null +++ b/packages/payment-backend/src/services/toss/TossPaymentCardService.ts @@ -0,0 +1,16 @@ +import toss from "toss-payments-server-api"; +import { ITossCardPayment } from "toss-payments-server-api/lib/structures/ITossCardPayment"; + +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, + ); + } +} diff --git a/packages/payment-backend/src/services/toss/TossPaymentService.ts b/packages/payment-backend/src/services/toss/TossPaymentService.ts new file mode 100644 index 0000000..3abac08 --- /dev/null +++ b/packages/payment-backend/src/services/toss/TossPaymentService.ts @@ -0,0 +1,99 @@ +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import toss from "toss-payments-server-api"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { ITossPaymentCancel } from "toss-payments-server-api/lib/structures/ITossPaymentCancel"; + +import { ErrorUtil } from "../../utils/ErrorUtil"; +import { TossAsset } from "./TossAsset"; +import { TossPaymentBillingService } from "./TossPaymentBillingService"; +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 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, + }; + } + + /* ---------------------------------------------------------------- + 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 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 new file mode 100644 index 0000000..e347a9a --- /dev/null +++ b/packages/payment-backend/src/services/toss/TossPaymentVirtualAccountService.ts @@ -0,0 +1,16 @@ +import toss from "toss-payments-server-api"; +import { ITossVirtualAccountPayment } from "toss-payments-server-api/lib/structures/ITossVirtualAccountPayment"; + +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, + ); + } +} diff --git a/packages/payment-backend/src/utils/ArgumentParser.ts b/packages/payment-backend/src/utils/ArgumentParser.ts new file mode 100644 index 0000000..7ccf2eb --- /dev/null +++ b/packages/payment-backend/src/utils/ArgumentParser.ts @@ -0,0 +1,79 @@ +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 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); + }); + + 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; + } + })(); + + // 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 new file mode 100644 index 0000000..9009e02 --- /dev/null +++ b/packages/payment-backend/src/utils/BcryptUtil.ts @@ -0,0 +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 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 new file mode 100644 index 0000000..f9fd38e --- /dev/null +++ b/packages/payment-backend/src/utils/DateUtil.ts @@ -0,0 +1,100 @@ +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 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()); + } + + // 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, + ]; +} diff --git a/packages/payment-backend/src/utils/ErrorUtil.ts b/packages/payment-backend/src/utils/ErrorUtil.ts new file mode 100644 index 0000000..daa7712 --- /dev/null +++ b/packages/payment-backend/src/utils/ErrorUtil.ts @@ -0,0 +1,59 @@ +import fs from "fs"; +import { randint } from "tstl/algorithm/random"; +import { Singleton } from "tstl/thread/Singleton"; + +import { PaymentConfiguration } from "../PaymentConfiguration"; + +import serializeError = require("serialize-error"); + +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 {} + } +} + +function cipher(val: number): string { + 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/errors`, + ); + } catch {} +}); diff --git a/packages/payment-backend/src/utils/Terminal.ts b/packages/payment-backend/src/utils/Terminal.ts new file mode 100644 index 0000000..c7174aa --- /dev/null +++ b/packages/payment-backend/src/utils/Terminal.ts @@ -0,0 +1,18 @@ +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)); + }, + ); + }); + } +} diff --git a/packages/payment-backend/src/utils/TokenManager.ts b/packages/payment-backend/src/utils/TokenManager.ts new file mode 100644 index 0000000..cdaf857 --- /dev/null +++ b/packages/payment-backend/src/utils/TokenManager.ts @@ -0,0 +1,90 @@ +import { AesPkcs5 } from "@nestia/fetcher/lib/AesPkcs5"; +import { randint } from "tstl/algorithm/random"; +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), + }; + + // 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; + + // 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; + } + + /* ----------------------------------------------------------- + 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 _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); + + return table; + } + + 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 new file mode 100644 index 0000000..2e43f06 --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment.ts @@ -0,0 +1,128 @@ +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import PaymentAPI from "payment-api"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { PaymentConfiguration } from "../../../src"; +import { IamportAsset } from "../../../src/services/iamport/IamportAsset"; + +export async function test_api_iamport_card_payment( + connection: PaymentAPI.IConnection, +): Promise { + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); + + /** + * 주문 금액. + */ + 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", + + 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); + + /* ----------------------------------------------------------- + 결제 내역 조회하기 + ----------------------------------------------------------- */ + /** + * 결제 내역 조회하기 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 을 통한 하위 타입 특정 + + 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 new file mode 100644 index 0000000..82c9bb6 --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel.ts @@ -0,0 +1,15 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +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, +): Promise { + // 카드 결제하기 + 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 new file mode 100644 index 0000000..91c4a11 --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_card_payment_cancel_partial.ts @@ -0,0 +1,15 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +import { validate_payment_cancel_partial } from "../internal/validate_payment_cancel_partial"; +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, +): Promise { + // 카드 결제하기 + 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 new file mode 100644 index 0000000..b320ee4 --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_subscription_payment.ts @@ -0,0 +1,125 @@ +import imp from "iamport-server-api"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import PaymentAPI from "payment-api"; +import { IPaymentReservation } from "payment-api/lib/structures/payments/IPaymentReservation"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { IamportAsset } from "../../../src/services/iamport/IamportAsset"; + +export async function test_api_iamport_subscription_payment( + connection: PaymentAPI.IConnection, +): Promise { + /** + * 귀하의 백엔드 서버가 발행한 식별자 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 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); + + // 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); + + // 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 new file mode 100644 index 0000000..3ca9a80 --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment.ts @@ -0,0 +1,141 @@ +import imp from "iamport-server-api"; +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import { IIamportResponse } from "iamport-server-api/lib/structures/IIamportResponse"; +import PaymentAPI from "payment-api"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; +import { sleep_for } from "tstl/thread/global"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { PaymentConfiguration } from "../../../src"; +import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentStorage"; +import { IamportAsset } from "../../../src/services/iamport/IamportAsset"; + +export async function test_api_iamport_vbank_payment( + connection: PaymentAPI.IConnection, +): Promise { + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); + + /** + * 주문 금액. + */ + 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); + + /** + * 웹훅 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); + + /* ----------------------------------------------------------- + 웹훅 이벤트 리스닝 + ----------------------------------------------------------- */ + /** + * 입금 시뮬레이션하기. + * + * 고객이 자신 앞을 발급된 계좌에, 결제 금액을 입금하는 상황 시뮬레이션. + */ + await imp.functional.internal.deposit( + await IamportAsset.connection("test-iamport-store-id"), + payment.response.imp_uid, + ); + + // 웹훅 이벤트가 귀하의 백엔드 서버로 전달되기를 기다림. + await sleep_for(1_000); + + /** + * 웹흑 리스닝 시뮬레이션. + * + * 귀하의 백엔드 서버가 웹훅 이벤트를 수신한 상황을 가정한다. + */ + const webhook: IPaymentWebhookHistory | undefined = + FakePaymentStorage.webhooks.back(); + if (webhook === undefined) + throw new Error( + "Bug on PaymentWebhooksController.iamport(): failed to get the webhook event.", + ); + else if (webhook.current.id !== history.id) + throw new Error( + "Bug on PaymentWebhooksController.iamport(): failed to deliver the webhook event.", + ); + else if (webhook.previous.paid_at !== null) + throw new Error( + "Bug on PaymentWebhookProvider.process(): failed to delivery the exact previous data.", + ); + else if (webhook.current.paid_at === null) + throw new Error( + "Bug on PaymentWebhookProvider.process(): failed to delivery the exact current data.", + ); + + // 웹훅 데이터 삭제 + FakePaymentStorage.webhooks.pop_back(); + + 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 new file mode 100644 index 0000000..5811fcb --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel.ts @@ -0,0 +1,20 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +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, +): 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", + })); +} 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 new file mode 100644 index 0000000..8576c1a --- /dev/null +++ b/packages/payment-backend/test/features/iamport/test_api_iamport_vbank_payment_cancel_partial.ts @@ -0,0 +1,20 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +import { validate_payment_cancel_partial } from "../internal/validate_payment_cancel_partial"; +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, +): 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", + })); +} diff --git a/packages/payment-backend/test/features/internal/validate_payment_cancel.ts b/packages/payment-backend/test/features/internal/validate_payment_cancel.ts new file mode 100644 index 0000000..98ea736 --- /dev/null +++ b/packages/payment-backend/test/features/internal/validate_payment_cancel.ts @@ -0,0 +1,60 @@ +import { TestValidator } from "@nestia/e2e"; +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentCancelHistory } from "payment-api/lib/structures/payments/IPaymentCancelHistory"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; +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, +) => { + // 검증기 준비 + 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); + + // 웹훅 검증하기 + 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); +}; 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 new file mode 100644 index 0000000..5a77772 --- /dev/null +++ b/packages/payment-backend/test/features/internal/validate_payment_cancel_partial.ts @@ -0,0 +1,76 @@ +import { ArrayUtil, TestValidator } from "@nestia/e2e"; +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentCancelHistory } from "payment-api/lib/structures/payments/IPaymentCancelHistory"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; + +import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentStorage"; + +export const validate_payment_cancel_partial = async ( + 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, + ); + + if (count !== record.cancels.length) + console.log("cancels.length", count, record.cancels.length); + TestValidator.equals("cancels.length")(count)( + record.cancels.length, + ); + if ( + (history.price / 5) * count !== + record.cancels + .map((c) => c.price) + .reduce((a, b) => a + b, 0) + ) + console.log( + "cancels[].amount", + (history.price / 5) * count, + record.cancels + .map((c) => c.price) + .reduce((a, b) => a + b, 0), + (history.price / 5) * count - + record.cancels + .map((c) => c.price) + .reduce((a, b) => a + b, 0), + ); + 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) { + 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); + }); +}; 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 new file mode 100644 index 0000000..5b458c9 --- /dev/null +++ b/packages/payment-backend/test/features/monitors/test_api_monitor_health_check.ts @@ -0,0 +1,7 @@ +import PaymentAPI from "payment-api"; + +export async function test_api_monitor_health_check( + connection: PaymentAPI.IConnection, +): Promise { + 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 new file mode 100644 index 0000000..afc985a --- /dev/null +++ b/packages/payment-backend/test/features/monitors/test_api_monitor_system.ts @@ -0,0 +1,12 @@ +import PaymentAPI from "payment-api"; +import { ISystem } from "payment-api/lib/structures/monitors/ISystem"; +import typia from "typia"; + +export async function test_api_monitor_system( + connection: PaymentAPI.IConnection, +): Promise { + 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 new file mode 100644 index 0000000..55d2d80 --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_card_payment.ts @@ -0,0 +1,156 @@ +import { IIamportPayment } from "iamport-server-api/lib/structures/IIamportPayment"; +import api from "payment-api"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import toss from "toss-payments-server-api"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { PaymentConfiguration } from "../../../src"; +import { TossAsset } from "../../../src/services/toss/TossAsset"; + +export async function test_api_toss_card_payment( + connection: api.IConnection, +): Promise { + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); + + /** + * 주문 금액. + */ + 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", + + // 주문 정보 + orderId: yourOrderId, + amount: yourOrderPrice, + + // 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); + + /* ----------------------------------------------------------- + 결제 내역 조회하기 + ----------------------------------------------------------- */ + /** + * 결제 내역 조회하기 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 을 통한 하위 타입 특정 + + 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 new file mode 100644 index 0000000..af96e05 --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel.ts @@ -0,0 +1,15 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +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, +): Promise { + // 카드 결제하기 + 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 new file mode 100644 index 0000000..48d5157 --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_card_payment_cancel_partial.ts @@ -0,0 +1,15 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +import { validate_payment_cancel_partial } from "../internal/validate_payment_cancel_partial"; +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, +): Promise { + // 카드 결제하기 + 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 new file mode 100644 index 0000000..c6bd9bc --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_subscription_payment.ts @@ -0,0 +1,134 @@ +import { IIamportSubscription } from "iamport-server-api/lib/structures/IIamportSubscription"; +import api from "payment-api"; +import { IPaymentReservation } from "payment-api/lib/structures/payments/IPaymentReservation"; +import toss from "toss-payments-server-api"; +import { ITossBilling } from "toss-payments-server-api/lib/structures/ITossBilling"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { TossAsset } from "../../../src/services/toss/TossAsset"; + +export async function test_api_toss_subscription_payment( + connection: api.IConnection, +): Promise { + /** + * 귀하의 백엔드 서버가 발행한 식별자 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 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); + + // 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); + + // 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 new file mode 100644 index 0000000..d2c687a --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment.ts @@ -0,0 +1,147 @@ +import api from "payment-api"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; +import { IPaymentWebhookHistory } from "payment-api/lib/structures/payments/IPaymentWebhookHistory"; +import toss from "toss-payments-server-api"; +import { ITossPayment } from "toss-payments-server-api/lib/structures/ITossPayment"; +import { sleep_for } from "tstl/thread/global"; +import typia from "typia"; +import { v4 } from "uuid"; + +import { PaymentConfiguration } from "../../../src"; +import { FakePaymentStorage } from "../../../src/providers/payments/FakePaymentStorage"; +import { TossAsset } from "../../../src/services/toss/TossAsset"; + +export async function test_api_toss_vbank_payment( + connection: api.IConnection, +): Promise { + //---- + // 결제의 원천이 되는 주문 정보 + //---- + /** + * 귀하의 백엔드 서버가 발행한 주문 ID. + */ + const yourOrderId: string = v4(); + + /** + * 주문 금액. + */ + 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", + + // 주문 정보 + orderId: yourOrderId, + orderName: "something", + amount: yourOrderPrice, + + // 고의 미승인 처리 + __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 + }`; + + /** + * 결제 이력 등록하기. + * + * 앞서 토스 페이먼츠의 팝업 창을 이용하여 가상 계좌 결제를 진행하고 발급받은 + * {@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 sleep_for(1_000); + + /** + * 웹흑 리스닝 시뮬레이션. + * + * 귀하의 백엔드 서버가 웹훅 이벤트를 수신한 상황을 가정한다. + */ + const webhook: IPaymentWebhookHistory | undefined = + FakePaymentStorage.webhooks.back(); + if (webhook === undefined) + throw new Error( + "Bug on PaymentWebhooksController.iamport(): failed to get the webhook event.", + ); + else if (webhook.current.id !== history.id) + throw new Error( + "Bug on PaymentWebhooksController.iamport(): failed to deliver the webhook event.", + ); + else if (webhook.previous.paid_at !== null) + throw new Error( + "Bug on PaymentWebhookProvider.process(): failed to delivery the exact previous data.", + ); + else if (webhook.current.paid_at === null) + throw new Error( + "Bug on PaymentWebhookProvider.process(): failed to delivery the exact current data.", + ); + + // 웹훅 데이터 삭제 + FakePaymentStorage.webhooks.pop_back(); + + 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 new file mode 100644 index 0000000..f79d83b --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel.ts @@ -0,0 +1,20 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +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, +): Promise { + // 카드 결제하기 + 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 new file mode 100644 index 0000000..b878e33 --- /dev/null +++ b/packages/payment-backend/test/features/toss/test_api_toss_vbank_payment_cancel_partial.ts @@ -0,0 +1,20 @@ +import PaymentAPI from "payment-api/lib/index"; +import { IPaymentHistory } from "payment-api/lib/structures/payments/IPaymentHistory"; + +import { validate_payment_cancel_partial } from "../internal/validate_payment_cancel_partial"; +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, +): Promise { + // 카드 결제하기 + 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 new file mode 100644 index 0000000..672b98f --- /dev/null +++ b/packages/payment-backend/test/index.ts @@ -0,0 +1,139 @@ +import { DynamicExecutor, StopWatch } from "@nestia/e2e"; +import fs from "fs"; +import api from "payment-api/lib/index"; +import { Singleton, randint } from "tstl"; +import { sleep_for } from "tstl/thread/global"; + +import { + PaymentBackend, + PaymentConfiguration, + PaymentGlobal, + PaymentSetupWizard, +} from "../src"; +import { ArgumentParser } from "../src/utils/ArgumentParser"; +import { ErrorUtil } from "../src/utils/ErrorUtil"; + +interface IOptions { + 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; + }); + }); + +function cipher(val: number): string { + 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 {} +} + +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(PaymentGlobal.prisma), + ); + await StopWatch.trace("Seed Data")(PaymentSetupWizard.seed); + } + + // OPEN SERVER + 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(); + + 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); +}); + +const directory = new Singleton(async () => { + 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 {} +} diff --git a/packages/payment-backend/test/manual/password.ts b/packages/payment-backend/test/manual/password.ts new file mode 100644 index 0000000..9b4b0ce --- /dev/null +++ b/packages/payment-backend/test/manual/password.ts @@ -0,0 +1,13 @@ +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(""); + +console.log(alphaNumeric(32)); +console.log(alphaNumeric(16)); +console.log(alphaNumeric(16)); diff --git a/packages/payment-backend/test/tsconfig.json b/packages/payment-backend/test/tsconfig.json new file mode 100644 index 0000000..87bfe67 --- /dev/null +++ b/packages/payment-backend/test/tsconfig.json @@ -0,0 +1,7 @@ +{ + "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 new file mode 100644 index 0000000..c96055d --- /dev/null +++ b/packages/payment-backend/tsconfig.json @@ -0,0 +1,77 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./lib", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + "noUnusedLocals": true, /* Report errors on unused locals. */ + "noUnusedParameters": true, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + /* Module Resolution Options */ + // "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/*"], + "payment-api/lib/*": ["./src/api/*"], + "payment-api/lib/": ["./src/api"], + "payment-api": ["./src/api"], + }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "plugins": [ + { "transform": "typescript-transform-paths" }, + { "transform": "typia/lib/transform" }, + { + "transform": "@nestia/core/lib/transform", + "stringify": null, + }, + ] + }, + "include": ["src"] +} \ No newline at end of file diff --git a/packages/toss-payments-server-api/README.md b/packages/toss-payments-server-api/README.md new file mode 100644 index 0000000..1ac96c5 --- /dev/null +++ b/packages/toss-payments-server-api/README.md @@ -0,0 +1,38 @@ +# SDK Library +This is a SDK library generated by [`nestia`](https://nestia.io). + +With this SDK library, you can easily and safely interact with backend server. + +Just import and call some API functions like gif image below: + +![nestia-sdk-demo](https://user-images.githubusercontent.com/13158709/215004990-368c589d-7101-404e-b81b-fbc936382f05.gif) + +> Left is server code, and right is client code utilizing the SDK + + + + +# What [`Nestia`](https://nestia.io) is: +![Nestia Logo](https://nestia.io/logo.png) + +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/samchon/nestia/blob/master/LICENSE) +[![npm version](https://img.shields.io/npm/v/@nestia/core.svg)](https://www.npmjs.com/package/@nestia/core) +[![Downloads](https://img.shields.io/npm/dm/nestia.svg)](https://www.npmjs.com/package/nestia) +[![Build Status](https://github.com/samchon/nestia/workflows/build/badge.svg)](https://github.com/samchon/nestia/actions?query=workflow%3Abuild) +[![Guide Documents](https://img.shields.io/badge/guide-documents-forestgreen)](https://nestia.io/docs/) + +[Nestia](https://nestia.io) is a set of helper libraries for NestJS, supporting below features: + + - `@nestia/core`: super-fast decorators + - `@nestia/sdk` + - SDK generator for clients + - Swagger generator evolved than ever + - Automatic E2E test functions generator + - `nestia`: just CLI (command line interface) tool + +> **Note** +> +> - **Only one line** required, with pure TypeScript type +> - Runtime validator is **20,000x faster** than `class-validator` +> - JSON serialization is **200x faster** than `class-transformer` +> - SDK is similar with [tRPC](https://trpc.io), but much advanced \ No newline at end of file diff --git a/packages/toss-payments-server-api/package.json b/packages/toss-payments-server-api/package.json new file mode 100644 index 0000000..6650d2d --- /dev/null +++ b/packages/toss-payments-server-api/package.json @@ -0,0 +1,39 @@ +{ + "name": "toss-payments-server-api", + "version": "4.0.0-dev.20230920", + "description": "SDK library generated by Nestia", + "main": "lib/index.js", + "typings": "lib/index.d.ts", + "scripts": { + "build": "npm run build:sdk && npm run compile", + "build:sdk": "rimraf ../fake-toss-payments-server/src/api/functional && cd ../fake-toss-payments-server && npx nestia sdk && cd ../toss-payments-server-api", + "compile": "rimraf lib && tsc", + "deploy": "npm run build && npm publish", + "postinstall": "ts-patch install" + }, + "repository": { + "type": "git", + "url": "https://github.com/samchon/nestia" + }, + "author": "Jeongho Nam", + "license": "MIT", + "bugs": { + "url": "https://github.com/samchon/nestia/issues" + }, + "homepage": "https://nestia.io", + "files": [ + "lib", + "package.json", + "README.md" + ], + "devDependencies": { + "rimraf": "^5.0.1", + "ts-node": "^10.9.1", + "ts-patch": "^3.0.2", + "typescript": "^5.2.2" + }, + "dependencies": { + "@nestia/fetcher": "^2.0.4", + "typia": "^5.0.4" + } +} \ No newline at end of file diff --git a/packages/toss-payments-server-api/tsconfig.json b/packages/toss-payments-server-api/tsconfig.json new file mode 100644 index 0000000..5bde0b8 --- /dev/null +++ b/packages/toss-payments-server-api/tsconfig.json @@ -0,0 +1,97 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": [ + "DOM", + "ES2015" + ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */// "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */// "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */// "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./lib", /* Specify an output folder for all emitted files. */// "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + "newLine": "lf", /* Set the newline character for emitting files. */// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. *//* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "plugins": [ + { + "transform": "typia/lib/transform" + } + ], + "strictNullChecks": true + }, + "include": [ + "../fake-toss-payments-server/src/api" + ] +} \ No newline at end of file diff --git a/prettier.config.js b/prettier.config.js index cd8349b..a9391a4 100644 --- a/prettier.config.js +++ b/prettier.config.js @@ -6,7 +6,6 @@ module.exports = { trailingComma: "all", importOrder: [ "", - "toss-payments*", "^[./]", ], importOrderSeparation: true,