From 845d841bcac902b82039c9ef4718f2af60e84a1a Mon Sep 17 00:00:00 2001 From: Developer Date: Sun, 30 Nov 2025 09:13:57 -0800 Subject: [PATCH] feat(wallet-service): Implement complete wallet service with DDD architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add domain layer with aggregates (WalletAccount, LedgerEntry, DepositOrder, SettlementOrder) - Add value objects (Money, Balance, Hashpower, UserId, etc.) - Add domain events for event-driven architecture - Implement application layer with CQRS commands and queries - Add infrastructure layer with Prisma repositories - Implement REST API with NestJS controllers - Add JWT authentication with guards and strategies - Add comprehensive unit tests (69 tests) and E2E tests (23 tests) - Add documentation: ARCHITECTURE.md, API.md, DEVELOPMENT.md, TESTING.md, DEPLOYMENT.md - Add E2E testing guide for WSL2 environment ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/services/wallet-service/.env.example | 4 + backend/services/wallet-service/.gitignore | 45 + .../wallet-service/DEVELOPMENT_GUIDE.md | 757 ----------------- backend/services/wallet-service/docs/API.md | 409 +++++++++ .../wallet-service/docs/ARCHITECTURE.md | 383 +++++++++ .../wallet-service/docs/DEPLOYMENT.md | 787 ++++++++++++++++++ .../wallet-service/docs/DEVELOPMENT.md | 478 +++++++++++ .../wallet-service/docs/E2E-TESTING-WSL2.md | 556 +++++++++++++ .../services/wallet-service/docs/TESTING.md | 752 +++++++++++++++++ backend/services/wallet-service/nest-cli.json | 8 + backend/services/wallet-service/package.json | 98 +++ .../wallet-service/prisma/schema.prisma | 159 ++++ .../wallet-service/src/api/api.module.ts | 36 + .../src/api/controllers/deposit.controller.ts | 30 + .../src/api/controllers/health.controller.ts | 18 + .../src/api/controllers/index.ts | 4 + .../src/api/controllers/ledger.controller.ts | 35 + .../src/api/controllers/wallet.controller.ts | 50 ++ .../src/api/dto/request/deposit.dto.ts | 24 + .../src/api/dto/request/index.ts | 3 + .../src/api/dto/request/ledger-query.dto.ts | 41 + .../src/api/dto/request/settlement.dto.ts | 14 + .../src/api/dto/response/index.ts | 2 + .../src/api/dto/response/ledger.dto.ts | 47 ++ .../src/api/dto/response/wallet.dto.ts | 75 ++ .../services/wallet-service/src/app.module.ts | 39 + .../commands/add-rewards.command.ts | 9 + .../commands/claim-rewards.command.ts | 5 + .../commands/deduct-for-planting.command.ts | 7 + .../commands/handle-deposit.command.ts | 10 + .../src/application/commands/index.ts | 5 + .../commands/settle-rewards.command.ts | 9 + .../queries/get-my-ledger.query.ts | 13 + .../queries/get-my-wallet.query.ts | 5 + .../src/application/queries/index.ts | 2 + .../src/application/services/index.ts | 1 + .../wallet-application.service.spec.ts | 206 +++++ .../services/wallet-application.service.ts | 354 ++++++++ .../deposit-order.aggregate.spec.ts | 110 +++ .../aggregates/deposit-order.aggregate.ts | 101 +++ .../src/domain/aggregates/index.ts | 4 + .../aggregates/ledger-entry.aggregate.spec.ts | 84 ++ .../aggregates/ledger-entry.aggregate.ts | 103 +++ .../settlement-order.aggregate.spec.ts | 135 +++ .../aggregates/settlement-order.aggregate.ts | 116 +++ .../wallet-account.aggregate.spec.ts | 129 +++ .../aggregates/wallet-account.aggregate.ts | 399 +++++++++ .../domain/events/balance-deducted.event.ts | 24 + .../domain/events/deposit-completed.event.ts | 24 + .../src/domain/events/domain-event.base.ts | 25 + .../wallet-service/src/domain/events/index.ts | 8 + .../src/domain/events/reward-added.event.ts | 23 + .../src/domain/events/reward-expired.event.ts | 21 + .../reward-moved-to-settleable.event.ts | 21 + .../events/settlement-completed.event.ts | 24 + .../events/withdrawal-requested.event.ts | 22 + .../deposit-order.repository.interface.ts | 12 + .../src/domain/repositories/index.ts | 4 + .../ledger-entry.repository.interface.ts | 32 + .../settlement-order.repository.interface.ts | 11 + .../wallet-account.repository.interface.ts | 11 + .../domain/value-objects/asset-type.enum.ts | 8 + .../domain/value-objects/balance.vo.spec.ts | 88 ++ .../src/domain/value-objects/balance.vo.ts | 77 ++ .../domain/value-objects/chain-type.enum.ts | 5 + .../value-objects/deposit-status.enum.ts | 5 + .../domain/value-objects/hashpower.vo.spec.ts | 76 ++ .../src/domain/value-objects/hashpower.vo.ts | 65 ++ .../src/domain/value-objects/index.ts | 11 + .../value-objects/ledger-entry-type.enum.ts | 16 + .../src/domain/value-objects/money.vo.spec.ts | 94 +++ .../src/domain/value-objects/money.vo.ts | 134 +++ .../value-objects/settlement-status.enum.ts | 13 + .../src/domain/value-objects/user-id.vo.ts | 29 + .../src/domain/value-objects/wallet-id.vo.ts | 29 + .../value-objects/wallet-status.enum.ts | 5 + .../infrastructure/infrastructure.module.ts | 40 + .../persistence/prisma/prisma.service.ts | 19 + .../deposit-order.repository.impl.ts | 89 ++ .../persistence/repositories/index.ts | 4 + .../ledger-entry.repository.impl.ts | 136 +++ .../settlement-order.repository.impl.ts | 88 ++ .../wallet-account.repository.impl.ts | 143 ++++ backend/services/wallet-service/src/main.ts | 45 + .../decorators/current-user.decorator.ts | 18 + .../src/shared/decorators/index.ts | 2 + .../src/shared/decorators/public.decorator.ts | 4 + .../src/shared/exceptions/domain.exception.ts | 41 + .../src/shared/exceptions/index.ts | 1 + .../shared/filters/domain-exception.filter.ts | 38 + .../src/shared/guards/jwt-auth.guard.ts | 22 + .../interceptors/transform.interceptor.ts | 22 + .../src/shared/strategies/jwt.strategy.ts | 29 + .../wallet-service/test/app.e2e-spec.ts | 356 ++++++++ .../wallet-service/test/jest-e2e.json | 14 + .../wallet-service/test/simple.e2e-spec.ts | 44 + .../wallet-service/tsconfig.build.json | 4 + backend/services/wallet-service/tsconfig.json | 24 + 98 files changed, 8004 insertions(+), 757 deletions(-) create mode 100644 backend/services/wallet-service/.env.example create mode 100644 backend/services/wallet-service/.gitignore delete mode 100644 backend/services/wallet-service/DEVELOPMENT_GUIDE.md create mode 100644 backend/services/wallet-service/docs/API.md create mode 100644 backend/services/wallet-service/docs/ARCHITECTURE.md create mode 100644 backend/services/wallet-service/docs/DEPLOYMENT.md create mode 100644 backend/services/wallet-service/docs/DEVELOPMENT.md create mode 100644 backend/services/wallet-service/docs/E2E-TESTING-WSL2.md create mode 100644 backend/services/wallet-service/docs/TESTING.md create mode 100644 backend/services/wallet-service/nest-cli.json create mode 100644 backend/services/wallet-service/prisma/schema.prisma create mode 100644 backend/services/wallet-service/src/api/api.module.ts create mode 100644 backend/services/wallet-service/src/api/controllers/deposit.controller.ts create mode 100644 backend/services/wallet-service/src/api/controllers/health.controller.ts create mode 100644 backend/services/wallet-service/src/api/controllers/index.ts create mode 100644 backend/services/wallet-service/src/api/controllers/ledger.controller.ts create mode 100644 backend/services/wallet-service/src/api/controllers/wallet.controller.ts create mode 100644 backend/services/wallet-service/src/api/dto/request/deposit.dto.ts create mode 100644 backend/services/wallet-service/src/api/dto/request/index.ts create mode 100644 backend/services/wallet-service/src/api/dto/request/ledger-query.dto.ts create mode 100644 backend/services/wallet-service/src/api/dto/request/settlement.dto.ts create mode 100644 backend/services/wallet-service/src/api/dto/response/index.ts create mode 100644 backend/services/wallet-service/src/api/dto/response/ledger.dto.ts create mode 100644 backend/services/wallet-service/src/api/dto/response/wallet.dto.ts create mode 100644 backend/services/wallet-service/src/app.module.ts create mode 100644 backend/services/wallet-service/src/application/commands/add-rewards.command.ts create mode 100644 backend/services/wallet-service/src/application/commands/claim-rewards.command.ts create mode 100644 backend/services/wallet-service/src/application/commands/deduct-for-planting.command.ts create mode 100644 backend/services/wallet-service/src/application/commands/handle-deposit.command.ts create mode 100644 backend/services/wallet-service/src/application/commands/index.ts create mode 100644 backend/services/wallet-service/src/application/commands/settle-rewards.command.ts create mode 100644 backend/services/wallet-service/src/application/queries/get-my-ledger.query.ts create mode 100644 backend/services/wallet-service/src/application/queries/get-my-wallet.query.ts create mode 100644 backend/services/wallet-service/src/application/queries/index.ts create mode 100644 backend/services/wallet-service/src/application/services/index.ts create mode 100644 backend/services/wallet-service/src/application/services/wallet-application.service.spec.ts create mode 100644 backend/services/wallet-service/src/application/services/wallet-application.service.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.spec.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/index.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.spec.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/settlement-order.aggregate.spec.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/settlement-order.aggregate.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.spec.ts create mode 100644 backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts create mode 100644 backend/services/wallet-service/src/domain/events/balance-deducted.event.ts create mode 100644 backend/services/wallet-service/src/domain/events/deposit-completed.event.ts create mode 100644 backend/services/wallet-service/src/domain/events/domain-event.base.ts create mode 100644 backend/services/wallet-service/src/domain/events/index.ts create mode 100644 backend/services/wallet-service/src/domain/events/reward-added.event.ts create mode 100644 backend/services/wallet-service/src/domain/events/reward-expired.event.ts create mode 100644 backend/services/wallet-service/src/domain/events/reward-moved-to-settleable.event.ts create mode 100644 backend/services/wallet-service/src/domain/events/settlement-completed.event.ts create mode 100644 backend/services/wallet-service/src/domain/events/withdrawal-requested.event.ts create mode 100644 backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts create mode 100644 backend/services/wallet-service/src/domain/repositories/index.ts create mode 100644 backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts create mode 100644 backend/services/wallet-service/src/domain/repositories/settlement-order.repository.interface.ts create mode 100644 backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/asset-type.enum.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/balance.vo.spec.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/balance.vo.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/chain-type.enum.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/deposit-status.enum.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/hashpower.vo.spec.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/hashpower.vo.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/index.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/ledger-entry-type.enum.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/money.vo.spec.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/money.vo.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/settlement-status.enum.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/user-id.vo.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/wallet-id.vo.ts create mode 100644 backend/services/wallet-service/src/domain/value-objects/wallet-status.enum.ts create mode 100644 backend/services/wallet-service/src/infrastructure/infrastructure.module.ts create mode 100644 backend/services/wallet-service/src/infrastructure/persistence/prisma/prisma.service.ts create mode 100644 backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts create mode 100644 backend/services/wallet-service/src/infrastructure/persistence/repositories/index.ts create mode 100644 backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts create mode 100644 backend/services/wallet-service/src/infrastructure/persistence/repositories/settlement-order.repository.impl.ts create mode 100644 backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts create mode 100644 backend/services/wallet-service/src/main.ts create mode 100644 backend/services/wallet-service/src/shared/decorators/current-user.decorator.ts create mode 100644 backend/services/wallet-service/src/shared/decorators/index.ts create mode 100644 backend/services/wallet-service/src/shared/decorators/public.decorator.ts create mode 100644 backend/services/wallet-service/src/shared/exceptions/domain.exception.ts create mode 100644 backend/services/wallet-service/src/shared/exceptions/index.ts create mode 100644 backend/services/wallet-service/src/shared/filters/domain-exception.filter.ts create mode 100644 backend/services/wallet-service/src/shared/guards/jwt-auth.guard.ts create mode 100644 backend/services/wallet-service/src/shared/interceptors/transform.interceptor.ts create mode 100644 backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts create mode 100644 backend/services/wallet-service/test/app.e2e-spec.ts create mode 100644 backend/services/wallet-service/test/jest-e2e.json create mode 100644 backend/services/wallet-service/test/simple.e2e-spec.ts create mode 100644 backend/services/wallet-service/tsconfig.build.json diff --git a/backend/services/wallet-service/.env.example b/backend/services/wallet-service/.env.example new file mode 100644 index 00000000..7fd479d9 --- /dev/null +++ b/backend/services/wallet-service/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL="postgresql://user:password@host:5432/database?schema=public" +NODE_ENV=development +APP_PORT=3002 +JWT_SECRET=your-jwt-secret-key diff --git a/backend/services/wallet-service/.gitignore b/backend/services/wallet-service/.gitignore new file mode 100644 index 00000000..a179970d --- /dev/null +++ b/backend/services/wallet-service/.gitignore @@ -0,0 +1,45 @@ +# Dependencies +node_modules/ +package-lock.json + +# Build output +dist/ +build/ + +# Environment files +.env +.env.local +.env.development +.env.test +.env.production + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Test coverage +coverage/ + +# Prisma +prisma/*.db +prisma/*.db-journal + +# Claude Code +.claude/ + +# Misc +nul +*.tmp +*.temp diff --git a/backend/services/wallet-service/DEVELOPMENT_GUIDE.md b/backend/services/wallet-service/DEVELOPMENT_GUIDE.md deleted file mode 100644 index a6f5bb44..00000000 --- a/backend/services/wallet-service/DEVELOPMENT_GUIDE.md +++ /dev/null @@ -1,757 +0,0 @@ -# Wallet Service ๅผ€ๅ‘ๆŒ‡ๅฏผ - -## ้กน็›ฎๆฆ‚่ฟฐ - -Wallet Service ๆ˜ฏ RWA ๆฆด่Žฒๅฅณ็š‡ๅนณๅฐ็š„้’ฑๅŒ…่ดฆๆœฌๅพฎๆœๅŠก๏ผŒ่ดŸ่ดฃ็ฎก็†็”จๆˆท็š„ๅนณๅฐๅ†…้ƒจไฝ™้ขใ€ๅ……ๅ€ผๅ…ฅ่ดฆใ€ๆ็Žฐใ€่ต„้‡‘ๆตๆฐด่ฎฐ่ดฆ็ญ‰ๅŠŸ่ƒฝใ€‚ - -## ๆŠ€ๆœฏๆ ˆ - -- **ๆก†ๆžถ**: NestJS -- **ๆ•ฐๆฎๅบ“**: PostgreSQL + Prisma ORM -- **ๆžถๆž„**: DDD + Hexagonal Architecture (ๅ…ญ่พนๅฝขๆžถๆž„) -- **่ฏญ่จ€**: TypeScript - -## ๆžถๆž„ๅ‚่€ƒ - -่ฏทๅ‚่€ƒ `identity-service` ็š„ๆžถๆž„ๆจกๅผ๏ผŒไฟๆŒไธ€่‡ดๆ€ง๏ผš - -``` -wallet-service/ -โ”œโ”€โ”€ prisma/ -โ”‚ โ””โ”€โ”€ schema.prisma # ๆ•ฐๆฎๅบ“ๆจกๅž‹ -โ”œโ”€โ”€ src/ -โ”‚ โ”œโ”€โ”€ api/ # Presentation Layer (APIๅฑ‚) -โ”‚ โ”‚ โ”œโ”€โ”€ controllers/ # HTTP ๆŽงๅˆถๅ™จ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet.controller.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ledger.controller.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deposit.controller.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ settlement.controller.ts -โ”‚ โ”‚ โ”œโ”€โ”€ dto/ # ๆ•ฐๆฎไผ ่พ“ๅฏน่ฑก -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet.dto.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ledger.dto.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deposit.dto.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ settlement.dto.ts -โ”‚ โ”‚ โ””โ”€โ”€ api.module.ts -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ application/ # Application Layer (ๅบ”็”จๅฑ‚) -โ”‚ โ”‚ โ”œโ”€โ”€ commands/ # ๅ‘ฝไปคๅฏน่ฑก -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ handle-deposit.command.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deduct-for-planting.command.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ allocate-funds.command.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ add-rewards.command.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ settle-rewards.command.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ withdraw.command.ts -โ”‚ โ”‚ โ”œโ”€โ”€ queries/ # ๆŸฅ่ฏขๅฏน่ฑก -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ get-my-wallet.query.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ get-my-ledger.query.ts -โ”‚ โ”‚ โ””โ”€โ”€ services/ -โ”‚ โ”‚ โ””โ”€โ”€ wallet-application.service.ts -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ domain/ # Domain Layer (้ข†ๅŸŸๅฑ‚) -โ”‚ โ”‚ โ”œโ”€โ”€ aggregates/ # ่šๅˆๆ น -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet-account.aggregate.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ledger-entry.aggregate.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deposit-order.aggregate.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ settlement-order.aggregate.ts -โ”‚ โ”‚ โ”œโ”€โ”€ value-objects/ # ๅ€ผๅฏน่ฑก -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet-id.vo.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ money.vo.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ balance.vo.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet-balances.vo.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet-rewards.vo.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ hashpower.vo.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ asset-type.enum.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ chain-type.enum.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet-status.enum.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ledger-entry-type.enum.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deposit-status.enum.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ settlement-status.enum.ts -โ”‚ โ”‚ โ”œโ”€โ”€ events/ # ้ข†ๅŸŸไบ‹ไปถ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deposit-completed.event.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ withdrawal-requested.event.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ reward-moved-to-settleable.event.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ reward-expired.event.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ settlement-completed.event.ts -โ”‚ โ”‚ โ”œโ”€โ”€ repositories/ # ไป“ๅ‚จๆŽฅๅฃ (Port) -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ wallet-account.repository.interface.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ ledger-entry.repository.interface.ts -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ deposit-order.repository.interface.ts -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ settlement-order.repository.interface.ts -โ”‚ โ”‚ โ””โ”€โ”€ services/ # ้ข†ๅŸŸๆœๅŠก -โ”‚ โ”‚ โ””โ”€โ”€ wallet-ledger.service.ts -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ infrastructure/ # Infrastructure Layer (ๅŸบ็ก€่ฎพๆ–ฝๅฑ‚) -โ”‚ โ”‚ โ”œโ”€โ”€ persistence/ # ๆŒไน…ๅŒ–ๅฎž็Žฐ (Adapter) -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ entities/ # Prisma ๅฎžไฝ“ๆ˜ ๅฐ„ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ mappers/ # ้ข†ๅŸŸๆจกๅž‹ไธŽๅฎžไฝ“็š„ๆ˜ ๅฐ„ -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ repositories/ # ไป“ๅ‚จๅฎž็Žฐ -โ”‚ โ”‚ โ””โ”€โ”€ infrastructure.module.ts -โ”‚ โ”‚ -โ”‚ โ”œโ”€โ”€ app.module.ts -โ”‚ โ””โ”€โ”€ main.ts -โ”œโ”€โ”€ .env.development -โ”œโ”€โ”€ .env.example -โ”œโ”€โ”€ package.json -โ””โ”€โ”€ tsconfig.json -``` - ---- - -## ็ฌฌไธ€้˜ถๆฎต๏ผš้กน็›ฎๅˆๅง‹ๅŒ– - -### 1.1 ๅˆ›ๅปบ NestJS ้กน็›ฎ - -```bash -cd backend/services -npx @nestjs/cli new wallet-service --skip-git --package-manager npm -cd wallet-service -``` - -### 1.2 ๅฎ‰่ฃ…ไพ่ต– - -```bash -npm install @nestjs/config @prisma/client class-validator class-transformer uuid -npm install -D prisma @types/uuid -``` - -### 1.3 ้…็ฝฎ็Žฏๅขƒๅ˜้‡ - -ๅˆ›ๅปบ `.env.development`: -```env -DATABASE_URL="postgresql://postgres:postgres@localhost:5432/rwadurian_wallet?schema=public" -NODE_ENV=development -PORT=3002 -``` - -ๅˆ›ๅปบ `.env.example`: -```env -DATABASE_URL="postgresql://user:password@host:5432/database?schema=public" -NODE_ENV=development -PORT=3002 -``` - ---- - -## ็ฌฌไบŒ้˜ถๆฎต๏ผšๆ•ฐๆฎๅบ“่ฎพ่ฎก (Prisma Schema) - -### 2.1 ๅˆ›ๅปบ prisma/schema.prisma - -```prisma -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// ============================================ -// ้’ฑๅŒ…่ดฆๆˆท่กจ (็Šถๆ€่กจ) -// ============================================ -model WalletAccount { - id BigInt @id @default(autoincrement()) @map("wallet_id") - userId BigInt @unique @map("user_id") - - // USDT ไฝ™้ข - usdtAvailable Decimal @default(0) @map("usdt_available") @db.Decimal(20, 8) - usdtFrozen Decimal @default(0) @map("usdt_frozen") @db.Decimal(20, 8) - - // DST ไฝ™้ข - dstAvailable Decimal @default(0) @map("dst_available") @db.Decimal(20, 8) - dstFrozen Decimal @default(0) @map("dst_frozen") @db.Decimal(20, 8) - - // BNB ไฝ™้ข - bnbAvailable Decimal @default(0) @map("bnb_available") @db.Decimal(20, 8) - bnbFrozen Decimal @default(0) @map("bnb_frozen") @db.Decimal(20, 8) - - // OG ไฝ™้ข - ogAvailable Decimal @default(0) @map("og_available") @db.Decimal(20, 8) - ogFrozen Decimal @default(0) @map("og_frozen") @db.Decimal(20, 8) - - // RWAD ไฝ™้ข - rwadAvailable Decimal @default(0) @map("rwad_available") @db.Decimal(20, 8) - rwadFrozen Decimal @default(0) @map("rwad_frozen") @db.Decimal(20, 8) - - // ็ฎ—ๅŠ› - hashpower Decimal @default(0) @map("hashpower") @db.Decimal(20, 8) - - // ๅพ…้ข†ๅ–ๆ”ถ็›Š - pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8) - pendingHashpower Decimal @default(0) @map("pending_hashpower") @db.Decimal(20, 8) - pendingExpireAt DateTime? @map("pending_expire_at") - - // ๅฏ็ป“็ฎ—ๆ”ถ็›Š - settleableUsdt Decimal @default(0) @map("settleable_usdt") @db.Decimal(20, 8) - settleableHashpower Decimal @default(0) @map("settleable_hashpower") @db.Decimal(20, 8) - - // ๅทฒ็ป“็ฎ—ๆ€ป้ข - settledTotalUsdt Decimal @default(0) @map("settled_total_usdt") @db.Decimal(20, 8) - settledTotalHashpower Decimal @default(0) @map("settled_total_hashpower") @db.Decimal(20, 8) - - // ๅทฒ่ฟ‡ๆœŸๆ€ป้ข - expiredTotalUsdt Decimal @default(0) @map("expired_total_usdt") @db.Decimal(20, 8) - expiredTotalHashpower Decimal @default(0) @map("expired_total_hashpower") @db.Decimal(20, 8) - - // ็Šถๆ€ - status String @default("ACTIVE") @map("status") @db.VarChar(20) - - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - - @@map("wallet_accounts") - @@index([userId]) - @@index([usdtAvailable(sort: Desc)]) - @@index([hashpower(sort: Desc)]) - @@index([status]) -} - -// ============================================ -// ่ดฆๆœฌๆตๆฐด่กจ (่กŒไธบ่กจ, append-only) -// ============================================ -model LedgerEntry { - id BigInt @id @default(autoincrement()) @map("entry_id") - userId BigInt @map("user_id") - - // ๆตๆฐด็ฑปๅž‹ - entryType String @map("entry_type") @db.VarChar(50) - - // ้‡‘้ขๅ˜ๅŠจ (ๆญฃๆ•ฐๅ…ฅ่ดฆ, ่ดŸๆ•ฐๆ”ฏๅ‡บ) - amount Decimal @map("amount") @db.Decimal(20, 8) - assetType String @map("asset_type") @db.VarChar(20) - - // ไฝ™้ขๅฟซ็…ง (ๆ“ไฝœๅŽไฝ™้ข) - balanceAfter Decimal? @map("balance_after") @db.Decimal(20, 8) - - // ๅ…ณ่”ๅผ•็”จ - refOrderId String? @map("ref_order_id") @db.VarChar(100) - refTxHash String? @map("ref_tx_hash") @db.VarChar(100) - - // ๅค‡ๆณจ - memo String? @map("memo") @db.VarChar(500) - - // ๆ‰ฉๅฑ•ๆ•ฐๆฎ - payloadJson Json? @map("payload_json") - - createdAt DateTime @default(now()) @map("created_at") - - @@map("wallet_ledger_entries") - @@index([userId, createdAt(sort: Desc)]) - @@index([entryType]) - @@index([assetType]) - @@index([refOrderId]) - @@index([refTxHash]) - @@index([createdAt]) -} - -// ============================================ -// ๅ……ๅ€ผ่ฎขๅ•่กจ -// ============================================ -model DepositOrder { - id BigInt @id @default(autoincrement()) @map("order_id") - userId BigInt @map("user_id") - - // ๅ……ๅ€ผไฟกๆฏ - chainType String @map("chain_type") @db.VarChar(20) - amount Decimal @map("amount") @db.Decimal(20, 8) - txHash String @unique @map("tx_hash") @db.VarChar(100) - - // ็Šถๆ€ - status String @default("PENDING") @map("status") @db.VarChar(20) - confirmedAt DateTime? @map("confirmed_at") - - createdAt DateTime @default(now()) @map("created_at") - - @@map("deposit_orders") - @@index([userId]) - @@index([txHash]) - @@index([status]) - @@index([chainType]) -} - -// ============================================ -// ็ป“็ฎ—่ฎขๅ•่กจ -// ============================================ -model SettlementOrder { - id BigInt @id @default(autoincrement()) @map("order_id") - userId BigInt @map("user_id") - - // ็ป“็ฎ—ไฟกๆฏ - usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8) - settleCurrency String @map("settle_currency") @db.VarChar(10) - - // SWAP ไฟกๆฏ - swapTxHash String? @map("swap_tx_hash") @db.VarChar(100) - receivedAmount Decimal? @map("received_amount") @db.Decimal(20, 8) - - // ็Šถๆ€ - status String @default("PENDING") @map("status") @db.VarChar(20) - settledAt DateTime? @map("settled_at") - - createdAt DateTime @default(now()) @map("created_at") - - @@map("settlement_orders") - @@index([userId]) - @@index([status]) - @@index([settleCurrency]) - @@index([createdAt]) -} -``` - -### 2.2 ๅˆๅง‹ๅŒ–ๆ•ฐๆฎๅบ“ - -```bash -npx prisma migrate dev --name init -npx prisma generate -``` - ---- - -## ็ฌฌไธ‰้˜ถๆฎต๏ผš้ข†ๅŸŸๅฑ‚ๅฎž็Žฐ - -### 3.1 ๅ€ผๅฏน่ฑก (Value Objects) - -#### 3.1.1 asset-type.enum.ts -```typescript -export enum AssetType { - USDT = 'USDT', - DST = 'DST', - BNB = 'BNB', - OG = 'OG', - RWAD = 'RWAD', - HASHPOWER = 'HASHPOWER', -} -``` - -#### 3.1.2 chain-type.enum.ts -```typescript -export enum ChainType { - KAVA = 'KAVA', - DST = 'DST', - BSC = 'BSC', -} -``` - -#### 3.1.3 wallet-status.enum.ts -```typescript -export enum WalletStatus { - ACTIVE = 'ACTIVE', - FROZEN = 'FROZEN', - CLOSED = 'CLOSED', -} -``` - -#### 3.1.4 ledger-entry-type.enum.ts -```typescript -export enum LedgerEntryType { - DEPOSIT_KAVA = 'DEPOSIT_KAVA', - DEPOSIT_BSC = 'DEPOSIT_BSC', - PLANT_PAYMENT = 'PLANT_PAYMENT', - REWARD_PENDING = 'REWARD_PENDING', - REWARD_TO_SETTLEABLE = 'REWARD_TO_SETTLEABLE', - REWARD_EXPIRED = 'REWARD_EXPIRED', - REWARD_SETTLED = 'REWARD_SETTLED', - TRANSFER_TO_POOL = 'TRANSFER_TO_POOL', - SWAP_EXECUTED = 'SWAP_EXECUTED', - WITHDRAWAL = 'WITHDRAWAL', - TRANSFER_IN = 'TRANSFER_IN', - TRANSFER_OUT = 'TRANSFER_OUT', - FREEZE = 'FREEZE', - UNFREEZE = 'UNFREEZE', -} -``` - -#### 3.1.5 deposit-status.enum.ts -```typescript -export enum DepositStatus { - PENDING = 'PENDING', - CONFIRMED = 'CONFIRMED', - FAILED = 'FAILED', -} -``` - -#### 3.1.6 settlement-status.enum.ts -```typescript -export enum SettlementStatus { - PENDING = 'PENDING', - SWAPPING = 'SWAPPING', - COMPLETED = 'COMPLETED', - FAILED = 'FAILED', -} - -export enum SettleCurrency { - BNB = 'BNB', - OG = 'OG', - USDT = 'USDT', - DST = 'DST', -} -``` - -#### 3.1.7 money.vo.ts -```typescript -export class Money { - private constructor( - public readonly amount: number, - public readonly currency: string, - ) { - if (amount < 0) { - throw new Error('Money amount cannot be negative'); - } - } - - static create(amount: number, currency: string): Money { - return new Money(amount, currency); - } - - static USDT(amount: number): Money { - return new Money(amount, 'USDT'); - } - - static zero(currency: string = 'USDT'): Money { - return new Money(0, currency); - } - - add(other: Money): Money { - this.ensureSameCurrency(other); - return new Money(this.amount + other.amount, this.currency); - } - - subtract(other: Money): Money { - this.ensureSameCurrency(other); - if (this.amount < other.amount) { - throw new Error('Insufficient balance'); - } - return new Money(this.amount - other.amount, this.currency); - } - - lessThan(other: Money): boolean { - this.ensureSameCurrency(other); - return this.amount < other.amount; - } - - greaterThan(other: Money): boolean { - this.ensureSameCurrency(other); - return this.amount > other.amount; - } - - isZero(): boolean { - return this.amount === 0; - } - - private ensureSameCurrency(other: Money): void { - if (this.currency !== other.currency) { - throw new Error('Currency mismatch'); - } - } -} -``` - -#### 3.1.8 balance.vo.ts -```typescript -import { Money } from './money.vo'; - -export class Balance { - constructor( - public readonly available: Money, - public readonly frozen: Money, - ) {} - - static zero(): Balance { - return new Balance(Money.zero(), Money.zero()); - } - - add(amount: Money): Balance { - return new Balance(this.available.add(amount), this.frozen); - } - - deduct(amount: Money): Balance { - if (this.available.lessThan(amount)) { - throw new Error('Insufficient available balance'); - } - return new Balance(this.available.subtract(amount), this.frozen); - } - - freeze(amount: Money): Balance { - if (this.available.lessThan(amount)) { - throw new Error('Insufficient available balance to freeze'); - } - return new Balance( - this.available.subtract(amount), - this.frozen.add(amount), - ); - } - - unfreeze(amount: Money): Balance { - if (this.frozen.lessThan(amount)) { - throw new Error('Insufficient frozen balance to unfreeze'); - } - return new Balance( - this.available.add(amount), - this.frozen.subtract(amount), - ); - } - - get total(): Money { - return this.available.add(this.frozen); - } -} -``` - -#### 3.1.9 hashpower.vo.ts -```typescript -export class Hashpower { - private constructor(public readonly value: number) { - if (value < 0) { - throw new Error('Hashpower cannot be negative'); - } - } - - static create(value: number): Hashpower { - return new Hashpower(value); - } - - static zero(): Hashpower { - return new Hashpower(0); - } - - add(other: Hashpower): Hashpower { - return new Hashpower(this.value + other.value); - } - - subtract(other: Hashpower): Hashpower { - if (this.value < other.value) { - throw new Error('Insufficient hashpower'); - } - return new Hashpower(this.value - other.value); - } - - isZero(): boolean { - return this.value === 0; - } -} -``` - -### 3.2 ่šๅˆๆ น (Aggregates) - -#### 3.2.1 wallet-account.aggregate.ts - -ๅฎž็Žฐ `WalletAccount` ่šๅˆๆ น๏ผŒๅŒ…ๅซ๏ผš -- ไฝ™้ข็ฎก็† (USDT/DST/BNB/OG/RWAD/็ฎ—ๅŠ›) -- ๆ”ถ็›Šๆฑ‡ๆ€ป (ๅพ…้ข†ๅ–/ๅฏ็ป“็ฎ—/ๅทฒ็ป“็ฎ—/่ฟ‡ๆœŸ) -- ๆ ธๅฟƒ้ข†ๅŸŸ่กŒไธบ๏ผšdeposit, deduct, freeze, unfreeze, addPendingReward, movePendingToSettleable, settleReward, withdraw - -#### 3.2.2 ledger-entry.aggregate.ts - -ๅฎž็Žฐ `LedgerEntry` ่šๅˆๆ น (append-only)๏ผŒๅŒ…ๅซ๏ผš -- ๆตๆฐด็ฑปๅž‹ -- ้‡‘้ขๅ˜ๅŠจ -- ไฝ™้ขๅฟซ็…ง -- ๅ…ณ่”ๅผ•็”จ - -### 3.3 ไป“ๅ‚จๆŽฅๅฃ (Repository Interfaces) - -#### 3.3.1 wallet-account.repository.interface.ts -```typescript -export interface IWalletAccountRepository { - save(wallet: WalletAccount): Promise; - findById(walletId: bigint): Promise; - findByUserId(userId: bigint): Promise; - getOrCreate(userId: bigint): Promise; - findByUserIds(userIds: bigint[]): Promise>; -} - -export const WALLET_ACCOUNT_REPOSITORY = Symbol('IWalletAccountRepository'); -``` - -#### 3.3.2 ledger-entry.repository.interface.ts -```typescript -export interface ILedgerEntryRepository { - save(entry: LedgerEntry): Promise; - saveAll(entries: LedgerEntry[]): Promise; - findByUserId(userId: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise; - findByRefOrderId(refOrderId: string): Promise; -} - -export const LEDGER_ENTRY_REPOSITORY = Symbol('ILedgerEntryRepository'); -``` - ---- - -## ็ฌฌๅ››้˜ถๆฎต๏ผšๅŸบ็ก€่ฎพๆ–ฝๅฑ‚ๅฎž็Žฐ - -### 4.1 ๅฎžไฝ“ๆ˜ ๅฐ„ (Entities) - -ๅˆ›ๅปบ Prisma ๅฎžไฝ“ๅˆฐ้ข†ๅŸŸๆจกๅž‹็š„ๆ˜ ๅฐ„็ฑปใ€‚ - -### 4.2 ไป“ๅ‚จๅฎž็Žฐ (Repository Implementations) - -ๅฎž็Žฐๆ‰€ๆœ‰ไป“ๅ‚จๆŽฅๅฃ๏ผŒไฝฟ็”จ Prisma Client ่ฟ›่กŒๆ•ฐๆฎๅบ“ๆ“ไฝœใ€‚ - ---- - -## ็ฌฌไบ”้˜ถๆฎต๏ผšๅบ”็”จๅฑ‚ๅฎž็Žฐ - -### 5.1 ๅ‘ฝไปคๅฏน่ฑก (Commands) - -```typescript -// handle-deposit.command.ts -export class HandleDepositCommand { - constructor( - public readonly userId: string, - public readonly amount: number, - public readonly chainType: ChainType, - public readonly txHash: string, - ) {} -} - -// deduct-for-planting.command.ts -export class DeductForPlantingCommand { - constructor( - public readonly userId: string, - public readonly amount: number, - public readonly orderId: string, - ) {} -} -``` - -### 5.2 ๅบ”็”จๆœๅŠก (Application Service) - -ๅฎž็Žฐ `WalletApplicationService`๏ผŒๅŒ…ๅซๆ‰€ๆœ‰็”จไพ‹๏ผš -- handleDeposit - ๅค„็†ๅ……ๅ€ผ -- deductForPlanting - ่ฎค็งๆ‰ฃๆฌพ -- allocateFunds - ่ต„้‡‘ๅˆ†้… -- addRewards - ๅขžๅŠ ๅฅ–ๅŠฑ -- movePendingToSettleable - ๅพ…้ข†ๅ–โ†’ๅฏ็ป“็ฎ— -- settleRewards - ็ป“็ฎ—ๆ”ถ็›Š -- getMyWallet - ๆŸฅ่ฏขๆˆ‘็š„้’ฑๅŒ… -- getMyLedger - ๆŸฅ่ฏขๆˆ‘็š„ๆตๆฐด - ---- - -## ็ฌฌๅ…ญ้˜ถๆฎต๏ผšAPIๅฑ‚ๅฎž็Žฐ - -### 6.1 DTO ๅฎšไน‰ - -```typescript -// wallet.dto.ts -export class WalletDTO { - walletId: string; - userId: string; - balances: BalancesDTO; - rewards: RewardsDTO; - status: string; -} - -export class BalancesDTO { - usdt: BalanceDTO; - dst: BalanceDTO; - bnb: BalanceDTO; - og: BalanceDTO; - rwad: BalanceDTO; - hashpower: number; -} - -export class BalanceDTO { - available: number; - frozen: number; -} -``` - -### 6.2 ๆŽงๅˆถๅ™จๅฎž็Žฐ - -```typescript -// wallet.controller.ts -@Controller('wallet') -export class WalletController { - constructor(private readonly walletService: WalletApplicationService) {} - - @Get('my-wallet') - @UseGuards(JwtAuthGuard) - async getMyWallet(@CurrentUser() user: User): Promise { - return this.walletService.getMyWallet({ userId: user.id }); - } -} - -// ledger.controller.ts -@Controller('wallet/ledger') -export class LedgerController { - constructor(private readonly walletService: WalletApplicationService) {} - - @Get('my-ledger') - @UseGuards(JwtAuthGuard) - async getMyLedger( - @CurrentUser() user: User, - @Query() query: GetMyLedgerQueryDTO, - ): Promise { - return this.walletService.getMyLedger({ - userId: user.id, - ...query, - }); - } -} -``` - ---- - -## ็ฌฌไธƒ้˜ถๆฎต๏ผšๆจกๅ—้…็ฝฎ - -### 7.1 app.module.ts - -```typescript -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - envFilePath: `.env.${process.env.NODE_ENV || 'development'}`, - }), - ApiModule, - InfrastructureModule, - ], -}) -export class AppModule {} -``` - ---- - -## ๅ…ณ้”ฎไธšๅŠก่ง„ๅˆ™ (ไธๅ˜ๅผ) - -1. **ไฝ™้ขไธ่ƒฝไธบ่ดŸ**: ไปปไฝ•ๅธ็ง็š„ๅฏ็”จไฝ™้ขไธ่ƒฝไธบ่ดŸๆ•ฐ -2. **ๆตๆฐด่กจๅช่ƒฝ่ฟฝๅŠ **: ่ดฆๆœฌๆตๆฐด่กจๅช่ƒฝ INSERT๏ผŒไธ่ƒฝ UPDATE/DELETE -3. **ๆฏ็ฌ”ๆตๆฐดๅฟ…้กปๆœ‰ไฝ™้ขๅฟซ็…ง**: ๆฏ็ฌ”ๆตๆฐดๅฟ…้กป่ฎฐๅฝ•ๆ“ไฝœๅŽ็š„ไฝ™้ขๅฟซ็…ง -4. **็ป“็ฎ—้‡‘้ขไธ่ƒฝ่ถ…่ฟ‡ๅฏ็ป“็ฎ—ไฝ™้ข**: ็ป“็ฎ—้‡‘้ขๅฟ…้กป โ‰ค ๅฏ็ป“็ฎ—ไฝ™้ข - ---- - -## API ็ซฏ็‚นๆฑ‡ๆ€ป - -| ๆ–นๆณ• | ่ทฏๅพ„ | ๆ่ฟฐ | ่ฎค่ฏ | -|------|------|------|------| -| GET | /wallet/my-wallet | ๆŸฅ่ฏขๆˆ‘็š„้’ฑๅŒ… | ้œ€่ฆ | -| GET | /wallet/ledger/my-ledger | ๆŸฅ่ฏขๆˆ‘็š„ๆตๆฐด | ้œ€่ฆ | -| POST | /wallet/deposit | ๅ……ๅ€ผๅ…ฅ่ดฆ (ๅ†…้ƒจ) | ๆœๅŠก้—ด | -| POST | /wallet/withdraw | ๆ็Žฐ็”ณ่ฏท | ้œ€่ฆ | -| POST | /wallet/settle | ็ป“็ฎ—ๆ”ถ็›Š | ้œ€่ฆ | - ---- - -## ๅผ€ๅ‘้กบๅบๅปบ่ฎฎ - -1. ้กน็›ฎๅˆๅง‹ๅŒ–ๅ’Œ Prisma Schema -2. ๅ€ผๅฏน่ฑกๅฎž็Žฐ -3. ่šๅˆๆ นๅฎž็Žฐ -4. ไป“ๅ‚จๆŽฅๅฃๅฎšไน‰ -5. ไป“ๅ‚จๅฎž็Žฐ -6. ้ข†ๅŸŸๆœๅŠกๅฎž็Žฐ -7. ๅบ”็”จๆœๅŠกๅฎž็Žฐ -8. DTO ๅ’ŒๆŽงๅˆถๅ™จๅฎž็Žฐ -9. ๆจกๅ—้…็ฝฎๅ’Œๆต‹่ฏ• - ---- - -## ๆณจๆ„ไบ‹้กน - -1. ๆ‰€ๆœ‰้‡‘้ขไฝฟ็”จ `Decimal(20, 8)` ๅญ˜ๅ‚จ๏ผŒ้ฟๅ…ๆตฎ็‚นๆ•ฐ็ฒพๅบฆ้—ฎ้ข˜ -2. ๆตๆฐด่กจๆ˜ฏ append-only๏ผŒไธๅ…่ฎธๆ›ดๆ–ฐๆˆ–ๅˆ ้™ค -3. ๆฏๆฌกไฝ™้ขๅ˜ๅŠจ้ƒฝ่ฆๅˆ›ๅปบๅฏนๅบ”็š„ๆตๆฐด่ฎฐๅฝ• -4. ไฝฟ็”จไบ‹ๅŠก็กฎไฟไฝ™้ขๅ’Œๆตๆฐด็š„ไธ€่‡ดๆ€ง -5. ๅ‚่€ƒ identity-service ็š„ไปฃ็ ้ฃŽๆ ผๅ’Œๅ‘ฝๅ่ง„่Œƒ diff --git a/backend/services/wallet-service/docs/API.md b/backend/services/wallet-service/docs/API.md new file mode 100644 index 00000000..10f2de20 --- /dev/null +++ b/backend/services/wallet-service/docs/API.md @@ -0,0 +1,409 @@ +# Wallet Service API ๆ–‡ๆกฃ + +## ๆฆ‚่ฟฐ + +ๆœฌๆ–‡ๆกฃๆ่ฟฐ Wallet Service ็š„ๆ‰€ๆœ‰ HTTP API ๆŽฅๅฃใ€‚ๆœๅŠกๅŸบ็ก€่ทฏๅพ„: `/api/v1` + +## ่ฎค่ฏ + +้™คๆ ‡ๆณจไธบ `ๅ…ฌๅผ€` ็š„ๆŽฅๅฃๅค–๏ผŒๆ‰€ๆœ‰ๆŽฅๅฃ้ƒฝ้œ€่ฆ JWT Bearer Token ่ฎค่ฏ๏ผš + +```http +Authorization: Bearer +``` + +JWT Payload ็ป“ๆž„๏ผš +```json +{ + "sub": "็”จๆˆทID", + "seq": 1001, + "exp": 1700000000 +} +``` + +## ๅ“ๅบ”ๆ ผๅผ + +### ๆˆๅŠŸๅ“ๅบ” + +```json +{ + "success": true, + "data": { ... }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +### ้”™่ฏฏๅ“ๅบ” + +```json +{ + "success": false, + "code": "ERROR_CODE", + "message": "้”™่ฏฏๆ่ฟฐ", + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +--- + +## 1. ๅฅๅบทๆฃ€ๆŸฅ + +### GET /api/v1/health + +ๆฃ€ๆŸฅๆœๅŠกๅฅๅบท็Šถๆ€ใ€‚ + +**่ฎค่ฏ**: ๅ…ฌๅผ€ + +**ๅ“ๅบ”็คบไพ‹**: +```json +{ + "success": true, + "data": { + "status": "ok", + "service": "wallet-service", + "timestamp": "2024-01-01T00:00:00.000Z" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +--- + +## 2. ้’ฑๅŒ…ๆŽฅๅฃ + +### GET /api/v1/wallet/my-wallet + +ๆŸฅ่ฏขๅฝ“ๅ‰็”จๆˆท็š„้’ฑๅŒ…ไฟกๆฏใ€‚ + +**่ฎค่ฏ**: ้œ€่ฆ + +**ๅ“ๅบ”็คบไพ‹**: +```json +{ + "success": true, + "data": { + "walletId": "1", + "userId": "12345", + "balances": { + "usdt": { "available": 1000.5, "frozen": 0 }, + "dst": { "available": 0, "frozen": 0 }, + "bnb": { "available": 0.1, "frozen": 0 }, + "og": { "available": 0, "frozen": 0 }, + "rwad": { "available": 100, "frozen": 0 } + }, + "hashpower": 500, + "rewards": { + "pendingUsdt": 50, + "pendingHashpower": 100, + "pendingExpireAt": "2024-01-02T00:00:00.000Z", + "settleableUsdt": 200, + "settleableHashpower": 400, + "settledTotalUsdt": 1000, + "settledTotalHashpower": 2000, + "expiredTotalUsdt": 50, + "expiredTotalHashpower": 100 + }, + "status": "ACTIVE" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +**ๅญ—ๆฎต่ฏดๆ˜Ž**: + +| ๅญ—ๆฎต | ็ฑปๅž‹ | ๆ่ฟฐ | +|-----|------|-----| +| walletId | string | ้’ฑๅŒ…ID | +| userId | string | ็”จๆˆทID | +| balances.{coin}.available | number | ๅฏ็”จไฝ™้ข | +| balances.{coin}.frozen | number | ๅ†ป็ป“ไฝ™้ข | +| hashpower | number | ๅฝ“ๅ‰็ฎ—ๅŠ› | +| rewards.pendingUsdt | number | ๅพ…้ข†ๅ–USDTๅฅ–ๅŠฑ | +| rewards.pendingHashpower | number | ๅพ…้ข†ๅ–็ฎ—ๅŠ›ๅฅ–ๅŠฑ | +| rewards.pendingExpireAt | string | ๅพ…้ข†ๅ–ๅฅ–ๅŠฑ่ฟ‡ๆœŸๆ—ถ้—ด | +| rewards.settleableUsdt | number | ๅฏ็ป“็ฎ—USDT | +| rewards.settleableHashpower | number | ๅฏ็ป“็ฎ—็ฎ—ๅŠ› | +| rewards.settledTotalUsdt | number | ๅทฒ็ป“็ฎ—USDT็ดฏ่ฎก | +| rewards.settledTotalHashpower | number | ๅทฒ็ป“็ฎ—็ฎ—ๅŠ›็ดฏ่ฎก | +| rewards.expiredTotalUsdt | number | ๅทฒ่ฟ‡ๆœŸUSDT็ดฏ่ฎก | +| rewards.expiredTotalHashpower | number | ๅทฒ่ฟ‡ๆœŸ็ฎ—ๅŠ›็ดฏ่ฎก | +| status | string | ้’ฑๅŒ…็Šถๆ€: ACTIVE, FROZEN | + +--- + +### POST /api/v1/wallet/claim-rewards + +้ข†ๅ–ๅพ…้ข†ๅ–็š„ๅฅ–ๅŠฑ๏ผŒๅฐ†ๅ…ถ่ฝฌไธบๅฏ็ป“็ฎ—็Šถๆ€ใ€‚ + +**่ฎค่ฏ**: ้œ€่ฆ + +**่ฏทๆฑ‚ไฝ“**: ๆ—  + +**ๅ“ๅบ”็คบไพ‹**: +```json +{ + "success": true, + "data": { + "message": "Rewards claimed successfully" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +**้”™่ฏฏ็ **: + +| ้”™่ฏฏ็  | HTTP็Šถๆ€ | ๆ่ฟฐ | +|-------|---------|------| +| NO_PENDING_REWARDS | 400 | ๆฒกๆœ‰ๅพ…้ข†ๅ–็š„ๅฅ–ๅŠฑ | +| WALLET_FROZEN | 403 | ้’ฑๅŒ…ๅทฒๅ†ป็ป“ | + +--- + +### POST /api/v1/wallet/settle + +็ป“็ฎ—ๅฏ็ป“็ฎ—็š„USDTๅฅ–ๅŠฑไธบๆŒ‡ๅฎšๅธ็งใ€‚ + +**่ฎค่ฏ**: ้œ€่ฆ + +**่ฏทๆฑ‚ไฝ“**: +```json +{ + "usdtAmount": 100, + "settleCurrency": "BNB" +} +``` + +| ๅญ—ๆฎต | ็ฑปๅž‹ | ๅฟ…ๅกซ | ๆ่ฟฐ | +|-----|------|-----|------| +| usdtAmount | number | ๆ˜ฏ | ็ป“็ฎ—USDT้‡‘้ข (>0) | +| settleCurrency | string | ๆ˜ฏ | ็ป“็ฎ—็›ฎๆ ‡ๅธ็ง: BNB, USDT, DST, OG, RWAD | + +**ๅ“ๅบ”็คบไพ‹**: +```json +{ + "success": true, + "data": { + "settlementOrderId": "12345" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +**้”™่ฏฏ็ **: + +| ้”™่ฏฏ็  | HTTP็Šถๆ€ | ๆ่ฟฐ | +|-------|---------|------| +| INSUFFICIENT_BALANCE | 400 | ๅฏ็ป“็ฎ—ไฝ™้ขไธ่ถณ | +| WALLET_FROZEN | 403 | ้’ฑๅŒ…ๅทฒๅ†ป็ป“ | +| VALIDATION_ERROR | 400 | ๅ‚ๆ•ฐ้ชŒ่ฏๅคฑ่ดฅ | + +--- + +## 3. ๆตๆฐดๆŽฅๅฃ + +### GET /api/v1/wallet/ledger/my-ledger + +ๆŸฅ่ฏขๅฝ“ๅ‰็”จๆˆท็š„่ดฆๆœฌๆตๆฐด่ฎฐๅฝ•ใ€‚ + +**่ฎค่ฏ**: ้œ€่ฆ + +**ๆŸฅ่ฏขๅ‚ๆ•ฐ**: + +| ๅ‚ๆ•ฐ | ็ฑปๅž‹ | ๅฟ…ๅกซ | ้ป˜่ฎคๅ€ผ | ๆ่ฟฐ | +|-----|------|-----|-------|------| +| page | number | ๅฆ | 1 | ้กต็  (>=1) | +| pageSize | number | ๅฆ | 20 | ๆฏ้กตๆ•ฐ้‡ (1-100) | +| entryType | string | ๅฆ | - | ๆตๆฐด็ฑปๅž‹่ฟ‡ๆปค | +| assetType | string | ๅฆ | - | ่ต„ไบง็ฑปๅž‹่ฟ‡ๆปค | +| startDate | string | ๅฆ | - | ๅผ€ๅง‹ๆ—ฅๆœŸ (ISO8601) | +| endDate | string | ๅฆ | - | ็ป“ๆŸๆ—ฅๆœŸ (ISO8601) | + +**entryType ๅฏ้€‰ๅ€ผ**: +- `DEPOSIT_KAVA` - KAVA้“พๅ……ๅ€ผ +- `DEPOSIT_BSC` - BSC้“พๅ……ๅ€ผ +- `PLANT_PAYMENT` - ่ฎค็งๆ”ฏไป˜ +- `REWARD_PENDING` - ๅฅ–ๅŠฑๅพ…้ข†ๅ– +- `REWARD_TO_SETTLEABLE` - ๅฅ–ๅŠฑ่ฝฌๅฏ็ป“็ฎ— +- `REWARD_SETTLED` - ๅฅ–ๅŠฑๅทฒ็ป“็ฎ— +- `REWARD_EXPIRED` - ๅฅ–ๅŠฑๅทฒ่ฟ‡ๆœŸ +- `WITHDRAWAL` - ๆ็Žฐ +- `ADMIN_ADJUST` - ็ฎก็†ๅ‘˜่ฐƒๆ•ด + +**assetType ๅฏ้€‰ๅ€ผ**: +- `USDT`, `DST`, `BNB`, `OG`, `RWAD`, `HASHPOWER` + +**ๅ“ๅบ”็คบไพ‹**: +```json +{ + "success": true, + "data": { + "data": [ + { + "id": "1001", + "entryType": "DEPOSIT_KAVA", + "amount": 100, + "assetType": "USDT", + "balanceAfter": 1100, + "refOrderId": null, + "refTxHash": "0x1234...5678", + "memo": "Deposit from KAVA", + "createdAt": "2024-01-01T10:00:00.000Z" + }, + { + "id": "1002", + "entryType": "PLANT_PAYMENT", + "amount": -50, + "assetType": "USDT", + "balanceAfter": 1050, + "refOrderId": "order_123", + "refTxHash": null, + "memo": "Plant payment", + "createdAt": "2024-01-01T11:00:00.000Z" + } + ], + "total": 100, + "page": 1, + "pageSize": 20, + "totalPages": 5 + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +**ๅญ—ๆฎต่ฏดๆ˜Ž**: + +| ๅญ—ๆฎต | ็ฑปๅž‹ | ๆ่ฟฐ | +|-----|------|-----| +| id | string | ๆตๆฐดID | +| entryType | string | ๆตๆฐด็ฑปๅž‹ | +| amount | number | ้‡‘้ข (ๆญฃๅ…ฅ่ดฆ/่ดŸๆ”ฏๅ‡บ) | +| assetType | string | ่ต„ไบง็ฑปๅž‹ | +| balanceAfter | number | ๆ“ไฝœๅŽไฝ™้ข | +| refOrderId | string | ๅ…ณ่”่ฎขๅ•ๅท | +| refTxHash | string | ๅ…ณ่”ไบคๆ˜“ๅ“ˆๅธŒ | +| memo | string | ๅค‡ๆณจ | +| createdAt | string | ๅˆ›ๅปบๆ—ถ้—ด | + +--- + +## 4. ๅ……ๅ€ผๆŽฅๅฃ (ๅ†…้ƒจๆœๅŠก) + +### POST /api/v1/wallet/deposit + +ๅค„็†้“พไธŠๅ……ๅ€ผ็กฎ่ฎคๅŽ็š„ๅ…ฅ่ดฆใ€‚ + +**่ฎค่ฏ**: ๅ…ฌๅผ€ (ไป…้™ๅ†…้ƒจๆœๅŠก่ฐƒ็”จ) + +**่ฏทๆฑ‚ไฝ“**: +```json +{ + "userId": "12345", + "amount": 100, + "chainType": "KAVA", + "txHash": "0x1234567890abcdef..." +} +``` + +| ๅญ—ๆฎต | ็ฑปๅž‹ | ๅฟ…ๅกซ | ๆ่ฟฐ | +|-----|------|-----|------| +| userId | string | ๆ˜ฏ | ็”จๆˆทID | +| amount | number | ๆ˜ฏ | ๅ……ๅ€ผ้‡‘้ข (>0) | +| chainType | string | ๆ˜ฏ | ้“พ็ฑปๅž‹: KAVA, BSC | +| txHash | string | ๆ˜ฏ | ไบคๆ˜“ๅ“ˆๅธŒ (ๅ”ฏไธ€) | + +**ๅ“ๅบ”็คบไพ‹**: +```json +{ + "success": true, + "data": { + "message": "Deposit processed successfully" + }, + "timestamp": "2024-01-01T00:00:00.000Z" +} +``` + +**้”™่ฏฏ็ **: + +| ้”™่ฏฏ็  | HTTP็Šถๆ€ | ๆ่ฟฐ | +|-------|---------|------| +| DUPLICATE_TRANSACTION | 409 | ้‡ๅคไบคๆ˜“ (txHashๅทฒๅญ˜ๅœจ) | +| VALIDATION_ERROR | 400 | ๅ‚ๆ•ฐ้ชŒ่ฏๅคฑ่ดฅ | + +--- + +## ้”™่ฏฏ็ ๆฑ‡ๆ€ป + +| ้”™่ฏฏ็  | HTTP็Šถๆ€ | ๆ่ฟฐ | +|-------|---------|------| +| VALIDATION_ERROR | 400 | ๅ‚ๆ•ฐ้ชŒ่ฏๅคฑ่ดฅ | +| UNAUTHORIZED | 401 | ๆœช่ฎค่ฏ | +| FORBIDDEN | 403 | ๆ— ๆƒ้™ | +| NOT_FOUND | 404 | ่ต„ๆบไธๅญ˜ๅœจ | +| DUPLICATE_TRANSACTION | 409 | ้‡ๅคไบคๆ˜“ | +| INSUFFICIENT_BALANCE | 400 | ไฝ™้ขไธ่ถณ | +| WALLET_FROZEN | 403 | ้’ฑๅŒ…ๅทฒๅ†ป็ป“ | +| WALLET_NOT_FOUND | 404 | ้’ฑๅŒ…ไธๅญ˜ๅœจ | +| NO_PENDING_REWARDS | 400 | ๆฒกๆœ‰ๅพ…้ข†ๅ–็š„ๅฅ–ๅŠฑ | +| INTERNAL_ERROR | 500 | ๅ†…้ƒจ้”™่ฏฏ | + +--- + +## Swagger ๆ–‡ๆกฃ + +ๅฏๅŠจๆœๅŠกๅŽ่ฎฟ้—ฎ: `http://localhost:3000/api-docs` + +--- + +## ่ฐƒ็”จ็คบไพ‹ + +### cURL + +```bash +# ่Žทๅ–้’ฑๅŒ…ไฟกๆฏ +curl -X GET "http://localhost:3000/api/v1/wallet/my-wallet" \ + -H "Authorization: Bearer " + +# ้ข†ๅ–ๅฅ–ๅŠฑ +curl -X POST "http://localhost:3000/api/v1/wallet/claim-rewards" \ + -H "Authorization: Bearer " + +# ็ป“็ฎ—ๅฅ–ๅŠฑ +curl -X POST "http://localhost:3000/api/v1/wallet/settle" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"usdtAmount": 100, "settleCurrency": "BNB"}' + +# ๆŸฅ่ฏขๆตๆฐด +curl -X GET "http://localhost:3000/api/v1/wallet/ledger/my-ledger?page=1&pageSize=10" \ + -H "Authorization: Bearer " + +# ๅ……ๅ€ผๅ…ฅ่ดฆ (ๅ†…้ƒจๆœๅŠก) +curl -X POST "http://localhost:3000/api/v1/wallet/deposit" \ + -H "Content-Type: application/json" \ + -d '{"userId": "12345", "amount": 100, "chainType": "KAVA", "txHash": "0x..."}' +``` + +### JavaScript (Fetch) + +```javascript +// ่Žทๅ–้’ฑๅŒ…ไฟกๆฏ +const response = await fetch('/api/v1/wallet/my-wallet', { + headers: { + 'Authorization': `Bearer ${token}` + } +}); +const data = await response.json(); + +// ็ป“็ฎ—ๅฅ–ๅŠฑ +const settleResponse = await fetch('/api/v1/wallet/settle', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + usdtAmount: 100, + settleCurrency: 'BNB' + }) +}); +``` diff --git a/backend/services/wallet-service/docs/ARCHITECTURE.md b/backend/services/wallet-service/docs/ARCHITECTURE.md new file mode 100644 index 00000000..bf0c2578 --- /dev/null +++ b/backend/services/wallet-service/docs/ARCHITECTURE.md @@ -0,0 +1,383 @@ +# Wallet Service ๆžถๆž„่ฎพ่ฎกๆ–‡ๆกฃ + +## ๆฆ‚่ฟฐ + +Wallet Service ๆ˜ฏ RWA (Real World Assets) ๆฆด่Žฒ่ฎค็งๅนณๅฐ็š„ๆ ธๅฟƒ้’ฑๅŒ…ไธŽ่ดฆๆœฌๆœๅŠก๏ผŒ่ดŸ่ดฃ็ฎก็†็”จๆˆท่ต„ไบงใ€ๅค„็†ๅ……ๅ€ผ/ๆ็Žฐใ€่ฎฐๅฝ•ไบคๆ˜“ๆตๆฐดใ€็ฎก็†ๅฅ–ๅŠฑ็ป“็ฎ—็ญ‰ๅŠŸ่ƒฝใ€‚ + +## ๆŠ€ๆœฏๆ ˆ + +| ็ป„ไปถ | ๆŠ€ๆœฏ้€‰ๅž‹ | ็‰ˆๆœฌ | +|------|---------|------| +| ่ฟ่กŒๆ—ถ | Node.js | 20.x | +| ๆก†ๆžถ | NestJS | 10.x | +| ่ฏญ่จ€ | TypeScript | 5.x | +| ORM | Prisma | 5.x | +| ๆ•ฐๆฎๅบ“ | PostgreSQL | 15.x | +| ่ฎค่ฏ | JWT (passport-jwt) | - | +| ็ฒพๅบฆ่ฎก็ฎ— | Decimal.js | 10.x | +| APIๆ–‡ๆกฃ | Swagger | 7.x | + +## ๆžถๆž„ๆจกๅผ + +ๆœฌๆœๅŠก้‡‡็”จ **้ข†ๅŸŸ้ฉฑๅŠจ่ฎพ่ฎก (DDD)** + **CQRS** ๆžถๆž„ๆจกๅผ๏ผš + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Presentation Layer โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ WalletController โ”‚ โ”‚ LedgerController โ”‚ โ”‚ DepositController โ”‚ โ”‚ +โ”‚ โ”‚ /wallet/* โ”‚ โ”‚ /wallet/ledger โ”‚ โ”‚ /wallet/deposit โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ Application Layer (CQRS) โ”‚ โ”‚ +โ”‚ โ–ผ โ–ผ โ–ผ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ WalletApplicationService โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ Commands โ”‚ โ”‚ Queries โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - HandleDeposit โ”‚ โ”‚ - GetMyWallet โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - DeductForPlanting โ”‚ โ”‚ - GetMyLedger โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - AddRewards โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - ClaimRewards โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ - SettleRewards โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Domain Layer โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Aggregates โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ WalletAccount โ”‚ โ”‚ DepositOrder โ”‚ โ”‚ SettlementOrder โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ (่šๅˆๆ น) โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ LedgerEntry โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Value Objects โ”‚ โ”‚ +โ”‚ โ”‚ Money โ”‚ Balance โ”‚ Hashpower โ”‚ UserId โ”‚ WalletId โ”‚ Enums โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Domain Events โ”‚ โ”‚ +โ”‚ โ”‚ DepositCompleted โ”‚ BalanceDeducted โ”‚ RewardAdded โ”‚ etc. โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Infrastructure Layer โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Repository Implementations โ”‚ โ”‚ +โ”‚ โ”‚ WalletAccountRepository โ”‚ LedgerEntryRepository โ”‚ etc. โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ Prisma Service โ”‚ โ”‚ +โ”‚ โ”‚ (Database Connection Pool) โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ PostgreSQL โ”‚ + โ”‚ Database โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## ็›ฎๅฝ•็ป“ๆž„ + +``` +src/ +โ”œโ”€โ”€ api/ # ่กจ็คบๅฑ‚ (Presentation Layer) +โ”‚ โ”œโ”€โ”€ controllers/ # HTTP ๆŽงๅˆถๅ™จ +โ”‚ โ”‚ โ”œโ”€โ”€ wallet.controller.ts # ้’ฑๅŒ…็›ธๅ…ณๆŽฅๅฃ +โ”‚ โ”‚ โ”œโ”€โ”€ ledger.controller.ts # ๆตๆฐดๆŸฅ่ฏขๆŽฅๅฃ +โ”‚ โ”‚ โ”œโ”€โ”€ deposit.controller.ts # ๅ……ๅ€ผๅ…ฅ่ดฆๆŽฅๅฃ (ๅ†…้ƒจ) +โ”‚ โ”‚ โ””โ”€โ”€ health.controller.ts # ๅฅๅบทๆฃ€ๆŸฅ +โ”‚ โ”œโ”€โ”€ dto/ +โ”‚ โ”‚ โ”œโ”€โ”€ request/ # ่ฏทๆฑ‚ DTO +โ”‚ โ”‚ โ””โ”€โ”€ response/ # ๅ“ๅบ” DTO +โ”‚ โ””โ”€โ”€ api.module.ts +โ”‚ +โ”œโ”€โ”€ application/ # ๅบ”็”จๅฑ‚ (Application Layer) +โ”‚ โ”œโ”€โ”€ commands/ # ๅ‘ฝไปค (ๅ†™ๆ“ไฝœ) +โ”‚ โ”‚ โ”œโ”€โ”€ handle-deposit.command.ts +โ”‚ โ”‚ โ”œโ”€โ”€ deduct-for-planting.command.ts +โ”‚ โ”‚ โ”œโ”€โ”€ add-rewards.command.ts +โ”‚ โ”‚ โ”œโ”€โ”€ claim-rewards.command.ts +โ”‚ โ”‚ โ””โ”€โ”€ settle-rewards.command.ts +โ”‚ โ”œโ”€โ”€ queries/ # ๆŸฅ่ฏข (่ฏปๆ“ไฝœ) +โ”‚ โ”‚ โ”œโ”€โ”€ get-my-wallet.query.ts +โ”‚ โ”‚ โ””โ”€โ”€ get-my-ledger.query.ts +โ”‚ โ””โ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ wallet-application.service.ts +โ”‚ +โ”œโ”€โ”€ domain/ # ้ข†ๅŸŸๅฑ‚ (Domain Layer) +โ”‚ โ”œโ”€โ”€ aggregates/ # ่šๅˆ +โ”‚ โ”‚ โ”œโ”€โ”€ wallet-account.aggregate.ts +โ”‚ โ”‚ โ”œโ”€โ”€ ledger-entry.aggregate.ts +โ”‚ โ”‚ โ”œโ”€โ”€ deposit-order.aggregate.ts +โ”‚ โ”‚ โ””โ”€โ”€ settlement-order.aggregate.ts +โ”‚ โ”œโ”€โ”€ value-objects/ # ๅ€ผๅฏน่ฑก +โ”‚ โ”‚ โ”œโ”€โ”€ money.vo.ts +โ”‚ โ”‚ โ”œโ”€โ”€ balance.vo.ts +โ”‚ โ”‚ โ”œโ”€โ”€ hashpower.vo.ts +โ”‚ โ”‚ โ”œโ”€โ”€ user-id.vo.ts +โ”‚ โ”‚ โ”œโ”€โ”€ wallet-id.vo.ts +โ”‚ โ”‚ โ””โ”€โ”€ [ๅ„็งๆžšไธพ].enum.ts +โ”‚ โ”œโ”€โ”€ events/ # ้ข†ๅŸŸไบ‹ไปถ +โ”‚ โ”‚ โ”œโ”€โ”€ deposit-completed.event.ts +โ”‚ โ”‚ โ”œโ”€โ”€ balance-deducted.event.ts +โ”‚ โ”‚ โ””โ”€โ”€ [ๅ…ถไป–ไบ‹ไปถ].ts +โ”‚ โ””โ”€โ”€ repositories/ # ไป“ๅ‚จๆŽฅๅฃ +โ”‚ โ”œโ”€โ”€ wallet-account.repository.interface.ts +โ”‚ โ””โ”€โ”€ [ๅ…ถไป–ๆŽฅๅฃ].ts +โ”‚ +โ”œโ”€โ”€ infrastructure/ # ๅŸบ็ก€่ฎพๆ–ฝๅฑ‚ +โ”‚ โ”œโ”€โ”€ persistence/ +โ”‚ โ”‚ โ”œโ”€โ”€ prisma/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ prisma.service.ts +โ”‚ โ”‚ โ””โ”€โ”€ repositories/ # ไป“ๅ‚จๅฎž็Žฐ +โ”‚ โ”‚ โ”œโ”€โ”€ wallet-account.repository.impl.ts +โ”‚ โ”‚ โ””โ”€โ”€ [ๅ…ถไป–ๅฎž็Žฐ].ts +โ”‚ โ””โ”€โ”€ infrastructure.module.ts +โ”‚ +โ”œโ”€โ”€ shared/ # ๅ…ฑไบซๆจกๅ— +โ”‚ โ”œโ”€โ”€ decorators/ # ่ฃ…้ฅฐๅ™จ +โ”‚ โ”œโ”€โ”€ filters/ # ๅผ‚ๅธธ่ฟ‡ๆปคๅ™จ +โ”‚ โ”œโ”€โ”€ guards/ # ๅฎˆๅซ +โ”‚ โ”œโ”€โ”€ interceptors/ # ๆ‹ฆๆˆชๅ™จ +โ”‚ โ”œโ”€โ”€ strategies/ # ่ฎค่ฏ็ญ–็•ฅ +โ”‚ โ””โ”€โ”€ exceptions/ # ่‡ชๅฎšไน‰ๅผ‚ๅธธ +โ”‚ +โ”œโ”€โ”€ app.module.ts # ๆ นๆจกๅ— +โ””โ”€โ”€ main.ts # ๅ…ฅๅฃๆ–‡ไปถ +``` + +## ๆ ธๅฟƒ้ข†ๅŸŸๆจกๅž‹ + +### 1. WalletAccount (้’ฑๅŒ…่ดฆๆˆท่šๅˆ) + +้’ฑๅŒ…่ดฆๆˆทๆ˜ฏๆ ธๅฟƒ่šๅˆๆ น๏ผŒ็ฎก็†็”จๆˆท็š„ๆ‰€ๆœ‰่ต„ไบง็Šถๆ€๏ผš + +```typescript +WalletAccount { + // ๆ ‡่ฏ† + walletId: WalletId + userId: UserId + + // ๅคšๅธ็งไฝ™้ข + balances: { + usdt: Balance { available, frozen } + dst: Balance { available, frozen } + bnb: Balance { available, frozen } + og: Balance { available, frozen } + rwad: Balance { available, frozen } + } + + // ็ฎ—ๅŠ› + hashpower: Hashpower + + // ๅฅ–ๅŠฑ็Šถๆ€ๆœบ + rewards: { + pending -> ๅพ…้ข†ๅ– (24ๅฐๆ—ถๅ†…ๅฟ…้กป้ข†ๅ–) + settleable -> ๅฏ็ป“็ฎ— (ๅฏๅ…‘ๆขไธบๅ…ถไป–ๅธ็ง) + settled -> ๅทฒ็ป“็ฎ—็ดฏ่ฎก + expired -> ๅทฒ่ฟ‡ๆœŸ็ดฏ่ฎก + } + + // ็Šถๆ€ + status: ACTIVE | FROZEN +} +``` + +### 2. ๅฅ–ๅŠฑ็Šถๆ€ๆœบ + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ addReward() โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ–ผ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ PENDING โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ (ๅพ…้ข†ๅ–, ๆœ‰่ฟ‡ๆœŸ) โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ + โ”‚ โ”‚ โ”‚ + expire() โ”‚ claim() โ”‚ + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ EXPIRED โ”‚ โ”‚ SETTLEABLE โ”‚ โ”‚ +โ”‚ (ๅทฒ่ฟ‡ๆœŸ็ดฏ่ฎก) โ”‚ โ”‚ (ๅฏ็ป“็ฎ—) โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ + โ”‚ โ”‚ + settle() โ”‚ + โ”‚ โ”‚ + โ–ผ โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ + โ”‚ SETTLED โ”‚ โ”‚ + โ”‚ (ๅทฒ็ป“็ฎ—็ดฏ่ฎก) โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +``` + +### 3. LedgerEntry (่ดฆๆœฌๆตๆฐด) + +้‡‡็”จ **Append-Only** ๆจกๅผ๏ผŒไธๅฏไฟฎๆ”น๏ผŒ็กฎไฟๅฎก่ฎก่ฟฝๆบฏ๏ผš + +```typescript +LedgerEntry { + entryType: LedgerEntryType // ๆตๆฐด็ฑปๅž‹ + amount: Money // ้‡‘้ข (ๆญฃๅ…ฅ่ดฆ/่ดŸๆ”ฏๅ‡บ) + assetType: AssetType // ่ต„ไบง็ฑปๅž‹ + balanceAfter: Money? // ๆ“ไฝœๅŽไฝ™้ขๅฟซ็…ง + refOrderId?: string // ๅ…ณ่”่ฎขๅ•ๅท + refTxHash?: string // ๅ…ณ่”ไบคๆ˜“ๅ“ˆๅธŒ + memo?: string // ๅค‡ๆณจ +} +``` + +### 4. ๆตๆฐด็ฑปๅž‹ๆžšไธพ + +```typescript +enum LedgerEntryType { + // ๅ……ๅ€ผ็›ธๅ…ณ + DEPOSIT_KAVA = 'DEPOSIT_KAVA' // KAVA้“พๅ……ๅ€ผ + DEPOSIT_BSC = 'DEPOSIT_BSC' // BSC้“พๅ……ๅ€ผ + + // ่ฎค็ง็›ธๅ…ณ + PLANT_PAYMENT = 'PLANT_PAYMENT' // ่ฎค็งๆ”ฏไป˜ + + // ๅฅ–ๅŠฑ็›ธๅ…ณ + REWARD_PENDING = 'REWARD_PENDING' // ๅฅ–ๅŠฑๅพ…้ข†ๅ– + REWARD_TO_SETTLEABLE = 'REWARD_TO_SETTLEABLE' // ๅฅ–ๅŠฑ่ฝฌๅฏ็ป“็ฎ— + REWARD_SETTLED = 'REWARD_SETTLED' // ๅฅ–ๅŠฑๅทฒ็ป“็ฎ— + REWARD_EXPIRED = 'REWARD_EXPIRED' // ๅฅ–ๅŠฑๅทฒ่ฟ‡ๆœŸ + + // ๅ…ถไป– + WITHDRAWAL = 'WITHDRAWAL' // ๆ็Žฐ + ADMIN_ADJUST = 'ADMIN_ADJUST' // ็ฎก็†ๅ‘˜่ฐƒๆ•ด +} +``` + +## ๆ•ฐๆฎๅบ“่ฎพ่ฎก + +### ER ๅ›พ + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ wallet_accounts โ”‚ โ”‚ wallet_ledger_ โ”‚ +โ”‚ โ”‚ โ”‚ entries โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ wallet_id (PK) โ”‚โ”€โ”€โ”€โ” โ”‚ entry_id (PK) โ”‚ +โ”‚ user_id (UK) โ”‚ โ”‚ โ”‚ user_id (FK) โ”‚ +โ”‚ usdt_available โ”‚ โ””โ”€โ”€โ–ถโ”‚ entry_type โ”‚ +โ”‚ usdt_frozen โ”‚ โ”‚ amount โ”‚ +โ”‚ dst_available โ”‚ โ”‚ asset_type โ”‚ +โ”‚ dst_frozen โ”‚ โ”‚ balance_after โ”‚ +โ”‚ bnb_available โ”‚ โ”‚ ref_order_id โ”‚ +โ”‚ bnb_frozen โ”‚ โ”‚ ref_tx_hash โ”‚ +โ”‚ og_available โ”‚ โ”‚ memo โ”‚ +โ”‚ og_frozen โ”‚ โ”‚ payload_json โ”‚ +โ”‚ rwad_available โ”‚ โ”‚ created_at โ”‚ +โ”‚ rwad_frozen โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”‚ hashpower โ”‚ +โ”‚ pending_usdt โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ pending_hashpower โ”‚ โ”‚ deposit_orders โ”‚ +โ”‚ pending_expire_at โ”‚ โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ settleable_usdt โ”‚ โ”‚ order_id (PK) โ”‚ +โ”‚ settleable_hashpowerโ”‚ โ”‚ user_id (FK) โ”‚ +โ”‚ settled_total_usdt โ”‚ โ”‚ chain_type โ”‚ +โ”‚ settled_total_hp โ”‚ โ”‚ amount โ”‚ +โ”‚ expired_total_usdt โ”‚ โ”‚ tx_hash (UK) โ”‚ +โ”‚ expired_total_hp โ”‚ โ”‚ status โ”‚ +โ”‚ status โ”‚ โ”‚ confirmed_at โ”‚ +โ”‚ created_at โ”‚ โ”‚ created_at โ”‚ +โ”‚ updated_at โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ settlement_orders โ”‚ + โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค + โ”‚ order_id (PK) โ”‚ + โ”‚ user_id (FK) โ”‚ + โ”‚ usdt_amount โ”‚ + โ”‚ settle_currency โ”‚ + โ”‚ swap_tx_hash โ”‚ + โ”‚ received_amount โ”‚ + โ”‚ status โ”‚ + โ”‚ settled_at โ”‚ + โ”‚ created_at โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ็ฒพๅบฆๅค„็† + +ๆ‰€ๆœ‰้‡‘้ขๅญ—ๆฎตไฝฟ็”จ `Decimal(20, 8)` ๅญ˜ๅ‚จ๏ผš +- 20ไฝๆ€ป็ฒพๅบฆ๏ผŒ8ไฝๅฐๆ•ฐ +- ๆ”ฏๆŒๆœ€ๅคง 999,999,999,999.99999999 +- ไฝฟ็”จ `decimal.js` ๅบ“่ฟ›่กŒ่ฎก็ฎ—๏ผŒ้ฟๅ…ๆตฎ็‚นๆ•ฐ็ฒพๅบฆ้—ฎ้ข˜ + +## ๅฎ‰ๅ…จ่ฎพ่ฎก + +### ่ฎค่ฏไธŽๆŽˆๆƒ + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ JWT Token โ”‚ + โ”‚ (Bearer) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ JwtAuthGuard โ”‚ +โ”‚ - ้ชŒ่ฏ Token ็ญพๅ โ”‚ +โ”‚ - ๆฃ€ๆŸฅ Token ่ฟ‡ๆœŸ โ”‚ +โ”‚ - ๆๅ– userId ๅ’Œ seq โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ @Public() ่ฃ…้ฅฐๅ™จ โ”‚ +โ”‚ - ๆ ‡่ฎฐไธบๅ…ฌๅผ€ๆŽฅๅฃ๏ผŒ่ทณ่ฟ‡่ฎค่ฏ โ”‚ +โ”‚ - ็”จไบŽๅ†…้ƒจๆœๅŠก่ฐƒ็”จ (ๅฆ‚ /deposit) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### ๅ†…้ƒจๆœๅŠก่ฐƒ็”จ + +ๅ……ๅ€ผๅ…ฅ่ดฆๆŽฅๅฃ (`POST /wallet/deposit`) ไฝฟ็”จ `@Public()` ่ฃ…้ฅฐๅ™จ๏ผŒ่ทณ่ฟ‡ JWT ่ฎค่ฏ๏ผŒไป…ไพ›ๅ†…้ƒจ้“พ็›‘ๆŽงๆœๅŠก่ฐƒ็”จใ€‚็”Ÿไบง็Žฏๅขƒๅบ”้€š่ฟ‡็ฝ‘็ปœ้š”็ฆปไฟๆŠคใ€‚ + +## ๆ‰ฉๅฑ•็‚น + +### 1. ้ข†ๅŸŸไบ‹ไปถ + +ๆ‰€ๆœ‰่šๅˆๆ“ไฝœ้ƒฝไผšไบง็”Ÿ้ข†ๅŸŸไบ‹ไปถ๏ผŒๅฏ็”จไบŽ๏ผš +- ๅผ‚ๆญฅ้€š็Ÿฅ +- ไบ‹ไปถๆบฏๆบ +- ่ทจๆœๅŠก้€šไฟก + +```typescript +wallet.deposit(amount, chainType, txHash); +// ไบง็”Ÿ DepositCompletedEvent + +wallet.deduct(amount, reason); +// ไบง็”Ÿ BalanceDeductedEvent +``` + +### 2. ไป“ๅ‚จๆŽฅๅฃ + +ๅŸบ็ก€่ฎพๆ–ฝๅฑ‚ๅฎž็Žฐๅฏๆ›ฟๆข๏ผš +- ๅฝ“ๅ‰๏ผšPrisma + PostgreSQL +- ๅฏๆ‰ฉๅฑ•๏ผšRedis ็ผ“ๅญ˜ใ€MongoDB ็ญ‰ + +### 3. ๆถˆๆฏ้˜Ÿๅˆ—้›†ๆˆ + +้ข„็•™ไบ‹ไปถๅ‘ๅธƒๆŽฅๅฃ๏ผŒๅฏ้›†ๆˆ๏ผš +- RabbitMQ +- Kafka +- Redis Pub/Sub diff --git a/backend/services/wallet-service/docs/DEPLOYMENT.md b/backend/services/wallet-service/docs/DEPLOYMENT.md new file mode 100644 index 00000000..3382192d --- /dev/null +++ b/backend/services/wallet-service/docs/DEPLOYMENT.md @@ -0,0 +1,787 @@ +# Wallet Service ้ƒจ็ฝฒๆ–‡ๆกฃ + +## ๆฆ‚่ฟฐ + +ๆœฌๆ–‡ๆกฃๆ่ฟฐ Wallet Service ็š„้ƒจ็ฝฒๆžถๆž„ใ€้…็ฝฎๆ–นๅผใ€Docker ๅฎนๅ™จๅŒ–ไปฅๅŠ็”Ÿไบง็Žฏๅขƒ้ƒจ็ฝฒๆต็จ‹ใ€‚ + +--- + +## ้ƒจ็ฝฒๆžถๆž„ + +``` + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ Load Balancer โ”‚ + โ”‚ (Nginx/ALB) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ โ”‚ โ”‚ + โ–ผ โ–ผ โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Wallet Service โ”‚ โ”‚ Wallet Service โ”‚ โ”‚ Wallet Service โ”‚ +โ”‚ Instance 1 โ”‚ โ”‚ Instance 2 โ”‚ โ”‚ Instance N โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ PostgreSQL โ”‚ + โ”‚ (Primary) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ”‚ PostgreSQL โ”‚ + โ”‚ (Replica) โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## ็Žฏๅขƒ่ฆๆฑ‚ + +### ็ณป็ปŸ่ฆๆฑ‚ + +| ็ป„ไปถ | ๆœ€ไฝŽ่ฆๆฑ‚ | ๆŽจ่้…็ฝฎ | +|-----|---------|---------| +| CPU | 2 ๆ ธ | 4+ ๆ ธ | +| ๅ†…ๅญ˜ | 2 GB | 4+ GB | +| ็ฃ็›˜ | 20 GB SSD | 50+ GB SSD | +| Node.js | 20.x | 20.x LTS | +| PostgreSQL | 15.x | 15.x | + +### ไพ่ต–ๆœๅŠก + +- PostgreSQL 15.x ๆ•ฐๆฎๅบ“ +- Docker (ๅฏ้€‰๏ผŒ็”จไบŽๅฎนๅ™จๅŒ–้ƒจ็ฝฒ) +- Kubernetes (ๅฏ้€‰๏ผŒ็”จไบŽ็ผ–ๆŽ’) + +--- + +## ็Žฏๅขƒๅ˜้‡้…็ฝฎ + +### ๅฟ…้œ€ๅ˜้‡ + +| ๅ˜้‡ | ๆ่ฟฐ | ็คบไพ‹ | +|-----|------|-----| +| `DATABASE_URL` | PostgreSQL ่ฟžๆŽฅๅญ—็ฌฆไธฒ | `postgresql://user:pass@host:5432/db` | +| `JWT_SECRET` | JWT ็ญพๅๅฏ†้’ฅ | `your-secret-key-here` | +| `NODE_ENV` | ็Žฏๅขƒๆ ‡่ฏ† | `production` | +| `PORT` | ๆœๅŠก็ซฏๅฃ | `3000` | + +### ๅฏ้€‰ๅ˜้‡ + +| ๅ˜้‡ | ๆ่ฟฐ | ้ป˜่ฎคๅ€ผ | +|-----|------|-------| +| `JWT_EXPIRES_IN` | JWT ่ฟ‡ๆœŸๆ—ถ้—ด | `24h` | +| `LOG_LEVEL` | ๆ—ฅๅฟ—็บงๅˆซ | `info` | +| `CORS_ORIGIN` | CORS ๅ…่ฎธๆบ | `*` | + +### ็Žฏๅขƒ้…็ฝฎๆ–‡ไปถ + +```bash +# .env.production +DATABASE_URL="postgresql://wallet:strong_password@db.example.com:5432/wallet_prod?schema=public&connection_limit=10" +JWT_SECRET="your-production-secret-key-min-32-chars" +NODE_ENV=production +PORT=3000 +LOG_LEVEL=info +``` + +--- + +## Docker ้ƒจ็ฝฒ + +### Dockerfile + +```dockerfile +# Dockerfile +# ๆž„ๅปบ้˜ถๆฎต +FROM node:20-alpine AS builder + +WORKDIR /app + +# ๅคๅˆถไพ่ต–ๆ–‡ไปถ +COPY package*.json ./ +COPY prisma ./prisma/ + +# ๅฎ‰่ฃ…ไพ่ต– +RUN npm ci --only=production=false + +# ๅคๅˆถๆบไปฃ็  +COPY . . + +# ็”Ÿๆˆ Prisma Client +RUN npx prisma generate + +# ๆž„ๅปบๅบ”็”จ +RUN npm run build + +# ๆธ…็†ๅผ€ๅ‘ไพ่ต– +RUN npm prune --production + +# ็”Ÿไบง้˜ถๆฎต +FROM node:20-alpine AS production + +WORKDIR /app + +# ๅฎ‰่ฃ… dumb-init ็”จไบŽไฟกๅทๅค„็† +RUN apk add --no-cache dumb-init + +# ๅˆ›ๅปบ้ž root ็”จๆˆท +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nestjs -u 1001 + +# ๅคๅˆถๆž„ๅปบไบง็‰ฉ +COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist +COPY --from=builder --chown=nestjs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nestjs:nodejs /app/prisma ./prisma +COPY --from=builder --chown=nestjs:nodejs /app/package.json ./ + +# ๅˆ‡ๆขๅˆฐ้ž root ็”จๆˆท +USER nestjs + +# ๆšด้œฒ็ซฏๅฃ +EXPOSE 3000 + +# ๅฅๅบทๆฃ€ๆŸฅ +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/v1/health || exit 1 + +# ๅฏๅŠจๅ‘ฝไปค +ENTRYPOINT ["dumb-init", "--"] +CMD ["sh", "-c", "npx prisma migrate deploy && node dist/main"] +``` + +### .dockerignore + +``` +# .dockerignore +node_modules +dist +coverage +.git +.gitignore +*.md +*.log +.env* +!.env.example +test +.vscode +.idea +``` + +### ๆž„ๅปบๅ’Œ่ฟ่กŒ + +```bash +# ๆž„ๅปบ้•œๅƒ +docker build -t wallet-service:latest . + +# ่ฟ่กŒๅฎนๅ™จ +docker run -d \ + --name wallet-service \ + -p 3000:3000 \ + -e DATABASE_URL="postgresql://wallet:password@host:5432/wallet" \ + -e JWT_SECRET="your-secret-key" \ + -e NODE_ENV=production \ + wallet-service:latest + +# ๆŸฅ็œ‹ๆ—ฅๅฟ— +docker logs -f wallet-service +``` + +--- + +## Docker Compose ้ƒจ็ฝฒ + +### docker-compose.yml + +```yaml +# docker-compose.yml +version: '3.8' + +services: + wallet-service: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://wallet:wallet123@postgres:5432/wallet?schema=public + JWT_SECRET: ${JWT_SECRET:-development-secret-change-in-production} + NODE_ENV: ${NODE_ENV:-production} + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + postgres: + image: postgres:15-alpine + environment: + POSTGRES_USER: wallet + POSTGRES_PASSWORD: wallet123 + POSTGRES_DB: wallet + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U wallet -d wallet"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres_data: +``` + +### docker-compose.prod.yml + +```yaml +# docker-compose.prod.yml +version: '3.8' + +services: + wallet-service: + image: wallet-service:${VERSION:-latest} + ports: + - "3000:3000" + environment: + DATABASE_URL: ${DATABASE_URL} + JWT_SECRET: ${JWT_SECRET} + NODE_ENV: production + LOG_LEVEL: info + deploy: + replicas: 3 + update_config: + parallelism: 1 + delay: 10s + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/api/v1/health"] + interval: 30s + timeout: 10s + retries: 3 +``` + +### ่ฟ่กŒๅ‘ฝไปค + +```bash +# ๅผ€ๅ‘็Žฏๅขƒ +docker-compose up -d + +# ็”Ÿไบง็Žฏๅขƒ +docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d + +# ๆ‰ฉๅฑ•ๅ‰ฏๆœฌ +docker-compose -f docker-compose.prod.yml up -d --scale wallet-service=3 +``` + +--- + +## Kubernetes ้ƒจ็ฝฒ + +### Deployment + +```yaml +# k8s/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: wallet-service + labels: + app: wallet-service +spec: + replicas: 3 + selector: + matchLabels: + app: wallet-service + template: + metadata: + labels: + app: wallet-service + spec: + containers: + - name: wallet-service + image: wallet-service:latest + ports: + - containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: wallet-secrets + key: database-url + - name: JWT_SECRET + valueFrom: + secretKeyRef: + name: wallet-secrets + key: jwt-secret + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + livenessProbe: + httpGet: + path: /api/v1/health + port: 3000 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /api/v1/health + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 5 +``` + +### Service + +```yaml +# k8s/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: wallet-service +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 3000 + selector: + app: wallet-service +``` + +### Secret + +```yaml +# k8s/secret.yaml +apiVersion: v1 +kind: Secret +metadata: + name: wallet-secrets +type: Opaque +stringData: + database-url: "postgresql://wallet:password@postgres:5432/wallet" + jwt-secret: "your-production-secret-key" +``` + +### Ingress + +```yaml +# k8s/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: wallet-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: wallet.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: wallet-service + port: + number: 80 +``` + +### ้ƒจ็ฝฒๅ‘ฝไปค + +```bash +# ๅˆ›ๅปบ Secret +kubectl apply -f k8s/secret.yaml + +# ้ƒจ็ฝฒๆœๅŠก +kubectl apply -f k8s/deployment.yaml +kubectl apply -f k8s/service.yaml +kubectl apply -f k8s/ingress.yaml + +# ๆŸฅ็œ‹็Šถๆ€ +kubectl get pods -l app=wallet-service +kubectl get svc wallet-service + +# ๆปšๅŠจๆ›ดๆ–ฐ +kubectl set image deployment/wallet-service wallet-service=wallet-service:v2.0.0 + +# ๅ›žๆปš +kubectl rollout undo deployment/wallet-service +``` + +--- + +## ๆ•ฐๆฎๅบ“่ฟ็งป + +### Prisma ่ฟ็งป + +```bash +# ๅผ€ๅ‘็Žฏๅขƒ - ๅˆ›ๅปบ่ฟ็งป +npx prisma migrate dev --name add_new_field + +# ็”Ÿไบง็Žฏๅขƒ - ๅบ”็”จ่ฟ็งป +npx prisma migrate deploy + +# ๆŸฅ็œ‹่ฟ็งป็Šถๆ€ +npx prisma migrate status +``` + +### ่ฟ็งป่„šๆœฌ + +```bash +#!/bin/bash +# scripts/migrate.sh + +set -e + +echo "Running database migrations..." + +# ็ญ‰ๅพ…ๆ•ฐๆฎๅบ“ๅฐฑ็ปช +until pg_isready -h $DB_HOST -p $DB_PORT -U $DB_USER; do + echo "Waiting for database..." + sleep 2 +done + +# ่ฟ่กŒ่ฟ็งป +npx prisma migrate deploy + +echo "Migrations completed successfully!" +``` + +--- + +## CI/CD ๆตๆฐด็บฟ + +### GitHub Actions + +```yaml +# .github/workflows/deploy.yml +name: Deploy + +on: + push: + branches: [main] + release: + types: [published] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: wallet + POSTGRES_PASSWORD: wallet123 + POSTGRES_DB: wallet_test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma Client + run: npx prisma generate + + - name: Run migrations + run: npx prisma db push + env: + DATABASE_URL: postgresql://wallet:wallet123@localhost:5432/wallet_test + + - name: Run tests + run: npm test + env: + DATABASE_URL: postgresql://wallet:wallet123@localhost:5432/wallet_test + JWT_SECRET: test-secret + + - name: Run E2E tests + run: npm run test:e2e + env: + DATABASE_URL: postgresql://wallet:wallet123@localhost:5432/wallet_test + JWT_SECRET: test-secret + + build: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + deploy: + needs: build + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + steps: + - name: Deploy to production + run: | + # ่งฆๅ‘้ƒจ็ฝฒ (ไพ‹ๅฆ‚ ArgoCD, Kubernetes, ๆˆ– SSH) + echo "Deploying to production..." +``` + +--- + +## ็›‘ๆŽงๅ’Œๆ—ฅๅฟ— + +### ๅฅๅบทๆฃ€ๆŸฅ็ซฏ็‚น + +```bash +# ๅฅๅบทๆฃ€ๆŸฅ +curl http://localhost:3000/api/v1/health + +# ๅ“ๅบ”็คบไพ‹ +{ + "success": true, + "data": { + "status": "ok", + "service": "wallet-service", + "timestamp": "2024-01-01T00:00:00.000Z" + } +} +``` + +### ๆ—ฅๅฟ—้…็ฝฎ + +```typescript +// src/main.ts +import { Logger } from '@nestjs/common'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule, { + logger: process.env.NODE_ENV === 'production' + ? ['error', 'warn', 'log'] + : ['error', 'warn', 'log', 'debug', 'verbose'], + }); + // ... +} +``` + +### ๆ—ฅๅฟ—ๆ ผๅผ (็”Ÿไบง็Žฏๅขƒ) + +```json +{ + "timestamp": "2024-01-01T00:00:00.000Z", + "level": "info", + "context": "WalletController", + "message": "Deposit processed", + "userId": "12345", + "amount": 100, + "requestId": "abc-123" +} +``` + +--- + +## ๅฎ‰ๅ…จ้…็ฝฎ + +### Nginx ๅๅ‘ไปฃ็† + +```nginx +# nginx.conf +upstream wallet_service { + server wallet-service-1:3000; + server wallet-service-2:3000; + server wallet-service-3:3000; +} + +server { + listen 80; + server_name wallet.example.com; + + # ้‡ๅฎšๅ‘ๅˆฐ HTTPS + return 301 https://$server_name$request_uri; +} + +server { + listen 443 ssl http2; + server_name wallet.example.com; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + # ๅฎ‰ๅ…จๅคด + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Strict-Transport-Security "max-age=31536000" always; + + # ้€Ÿ็އ้™ๅˆถ + limit_req zone=api burst=20 nodelay; + + location /api/ { + proxy_pass http://wallet_service; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } +} +``` + +### ็Žฏๅขƒๅ˜้‡ๅฎ‰ๅ…จ + +```bash +# ไธ่ฆๅœจ็‰ˆๆœฌๆŽงๅˆถไธญๆไบคๆ•ๆ„Ÿไฟกๆฏ +# ไฝฟ็”จ็Žฏๅขƒๅ˜้‡ๆˆ–ๅฏ†้’ฅ็ฎก็†ๆœๅŠก + +# AWS Secrets Manager +aws secretsmanager get-secret-value --secret-id wallet/production + +# HashiCorp Vault +vault kv get secret/wallet/production +``` + +--- + +## ๆ•…้šœๆŽ’้™ค + +### ๅธธ่ง้—ฎ้ข˜ + +#### 1. ๆ•ฐๆฎๅบ“่ฟžๆŽฅๅคฑ่ดฅ + +```bash +# ๆฃ€ๆŸฅ่ฟžๆŽฅ +psql $DATABASE_URL -c "SELECT 1" + +# ๆฃ€ๆŸฅ็ฝ‘็ปœ +nc -zv db.example.com 5432 +``` + +#### 2. ๅฎนๅ™จๆ— ๆณ•ๅฏๅŠจ + +```bash +# ๆŸฅ็œ‹ๆ—ฅๅฟ— +docker logs wallet-service + +# ่ฟ›ๅ…ฅๅฎนๅ™จ่ฐƒ่ฏ• +docker exec -it wallet-service sh +``` + +#### 3. ่ฟ็งปๅคฑ่ดฅ + +```bash +# ๆŸฅ็œ‹่ฟ็งป็Šถๆ€ +npx prisma migrate status + +# ้‡็ฝฎๆ•ฐๆฎๅบ“ (ๅผ€ๅ‘็Žฏๅขƒ) +npx prisma migrate reset +``` + +### ๅ›žๆปšๆต็จ‹ + +```bash +# Docker ๅ›žๆปš +docker stop wallet-service +docker run -d --name wallet-service wallet-service:previous-tag + +# Kubernetes ๅ›žๆปš +kubectl rollout undo deployment/wallet-service + +# ๆ•ฐๆฎๅบ“ๅ›žๆปš (ๆ‰‹ๅŠจ) +# ้œ€่ฆๆๅ‰ๅค‡ไปฝ๏ผŒPrisma ไธๆ”ฏๆŒ่‡ชๅŠจๅ›žๆปš +pg_restore -d wallet wallet_backup.dump +``` + +--- + +## ้ƒจ็ฝฒๆฃ€ๆŸฅๆธ…ๅ• + +### ้ƒจ็ฝฒๅ‰ + +- [ ] ๆ‰€ๆœ‰ๆต‹่ฏ•้€š่ฟ‡ +- [ ] ไปฃ็ ๅฎกๆŸฅๅฎŒๆˆ +- [ ] ็Žฏๅขƒๅ˜้‡้…็ฝฎๆญฃ็กฎ +- [ ] ๆ•ฐๆฎๅบ“ๅค‡ไปฝๅฎŒๆˆ +- [ ] ่ฟ็งป่„šๆœฌๆต‹่ฏ•้€š่ฟ‡ + +### ้ƒจ็ฝฒไธญ + +- [ ] ็›‘ๆŽงๆŒ‡ๆ ‡ๆญฃๅธธ +- [ ] ๅฅๅบทๆฃ€ๆŸฅ้€š่ฟ‡ +- [ ] ๆ—ฅๅฟ—ๆ— ้”™่ฏฏ +- [ ] API ๅ“ๅบ”ๆญฃๅธธ + +### ้ƒจ็ฝฒๅŽ + +- [ ] ๅŠŸ่ƒฝ้ชŒ่ฏ้€š่ฟ‡ +- [ ] ๆ€ง่ƒฝๆต‹่ฏ•้€š่ฟ‡ +- [ ] ๅฎ‰ๅ…จๆ‰ซๆ้€š่ฟ‡ +- [ ] ๆ–‡ๆกฃๆ›ดๆ–ฐ + +--- + +## ่”็ณปๆ–นๅผ + +ๅฆ‚้‡้ƒจ็ฝฒ้—ฎ้ข˜๏ผŒ่ฏท่”็ณป๏ผš + +- ่ฟ็ปดๅ›ข้˜Ÿ: devops@example.com +- ๅผ€ๅ‘ๅ›ข้˜Ÿ: dev@example.com +- ็ดงๆ€ฅ่”็ณป: oncall@example.com + diff --git a/backend/services/wallet-service/docs/DEVELOPMENT.md b/backend/services/wallet-service/docs/DEVELOPMENT.md new file mode 100644 index 00000000..8e0f9336 --- /dev/null +++ b/backend/services/wallet-service/docs/DEVELOPMENT.md @@ -0,0 +1,478 @@ +# Wallet Service ๅผ€ๅ‘ๆŒ‡ๅ— + +## ็Žฏๅขƒ่ฆๆฑ‚ + +- Node.js 20.x +- npm 10.x +- PostgreSQL 15.x +- Docker (็”จไบŽๆœฌๅœฐๆ•ฐๆฎๅบ“) +- WSL2 (Windows ๅผ€ๅ‘่€…) + +## ๅฟซ้€Ÿๅผ€ๅง‹ + +### 1. ๅ…‹้š†้กน็›ฎ + +```bash +git clone +cd wallet-service +``` + +### 2. ๅฎ‰่ฃ…ไพ่ต– + +```bash +npm install +``` + +### 3. ้…็ฝฎ็Žฏๅขƒๅ˜้‡ + +ๅˆ›ๅปบ `.env.development` ๆ–‡ไปถ๏ผš + +```bash +# ๆ•ฐๆฎๅบ“่ฟžๆŽฅ +DATABASE_URL="postgresql://wallet:wallet123@localhost:5432/wallet_dev?schema=public" + +# JWT ้…็ฝฎ +JWT_SECRET="your-development-jwt-secret" + +# ๅบ”็”จ้…็ฝฎ +NODE_ENV=development +PORT=3000 +``` + +### 4. ๅฏๅŠจๆ•ฐๆฎๅบ“ + +ไฝฟ็”จ Docker ๅฏๅŠจ PostgreSQL๏ผš + +```bash +docker run -d \ + --name wallet-postgres-dev \ + -e POSTGRES_USER=wallet \ + -e POSTGRES_PASSWORD=wallet123 \ + -e POSTGRES_DB=wallet_dev \ + -p 5432:5432 \ + postgres:15-alpine +``` + +### 5. ๅˆๅง‹ๅŒ–ๆ•ฐๆฎๅบ“ + +```bash +# ็”Ÿๆˆ Prisma Client +npx prisma generate + +# ๆŽจ้€ๆ•ฐๆฎๅบ“็ป“ๆž„ +npx prisma db push + +# (ๅฏ้€‰) ๆ‰“ๅผ€ Prisma Studio ๆŸฅ็œ‹ๆ•ฐๆฎ +npx prisma studio +``` + +### 6. ๅฏๅŠจๅผ€ๅ‘ๆœๅŠกๅ™จ + +```bash +npm run start:dev +``` + +ๆœๅŠกๅฐ†ๅœจ `http://localhost:3000` ๅฏๅŠจใ€‚ + +Swagger ๆ–‡ๆกฃ: `http://localhost:3000/api-docs` + +--- + +## ้กน็›ฎ่„šๆœฌ + +| ๅ‘ฝไปค | ๆ่ฟฐ | +|-----|------| +| `npm run start` | ๅฏๅŠจ็”Ÿไบงๆจกๅผ | +| `npm run start:dev` | ๅฏๅŠจๅผ€ๅ‘ๆจกๅผ (็ƒญ้‡่ฝฝ) | +| `npm run start:debug` | ๅฏๅŠจ่ฐƒ่ฏ•ๆจกๅผ | +| `npm run build` | ๆž„ๅปบ้กน็›ฎ | +| `npm test` | ่ฟ่กŒๅ•ๅ…ƒๆต‹่ฏ• | +| `npm run test:watch` | ็›‘ๅฌๆจกๅผ่ฟ่กŒๆต‹่ฏ• | +| `npm run test:cov` | ่ฟ่กŒๆต‹่ฏ•ๅนถ็”Ÿๆˆ่ฆ†็›–็އๆŠฅๅ‘Š | +| `npm run test:e2e` | ่ฟ่กŒ E2E ๆต‹่ฏ• | +| `npm run lint` | ไปฃ็ ๆฃ€ๆŸฅ | +| `npm run format` | ไปฃ็ ๆ ผๅผๅŒ– | +| `npm run prisma:generate` | ็”Ÿๆˆ Prisma Client | +| `npm run prisma:migrate` | ่ฟ่กŒๆ•ฐๆฎๅบ“่ฟ็งป | +| `npm run prisma:studio` | ๅฏๅŠจ Prisma Studio | + +--- + +## ไปฃ็ ็ป“ๆž„ + +### ๆทปๅŠ ๆ–ฐๅŠŸ่ƒฝ็š„ๆ ‡ๅ‡†ๆต็จ‹ + +#### 1. ๅฎšไน‰ๅ€ผๅฏน่ฑก (ๅฆ‚้œ€่ฆ) + +```typescript +// src/domain/value-objects/new-value.vo.ts +export class NewValue { + private readonly _value: number; + + private constructor(value: number) { + this._value = value; + } + + static create(value: number): NewValue { + // ้ชŒ่ฏ้€ป่พ‘ + if (value < 0) { + throw new DomainError('Value cannot be negative'); + } + return new NewValue(value); + } + + get value(): number { + return this._value; + } +} +``` + +#### 2. ๅฎšไน‰้ข†ๅŸŸไบ‹ไปถ (ๅฆ‚้œ€่ฆ) + +```typescript +// src/domain/events/new-action.event.ts +export class NewActionEvent extends DomainEvent { + constructor(public readonly payload: { + userId: string; + amount: string; + }) { + super('NewActionEvent'); + } +} +``` + +#### 3. ๅœจ่šๅˆไธญๆทปๅŠ ไธšๅŠกๆ–นๆณ• + +```typescript +// src/domain/aggregates/wallet-account.aggregate.ts +newAction(amount: Money): void { + this.ensureActive(); + // ไธšๅŠก้€ป่พ‘ + this._updatedAt = new Date(); + this.addDomainEvent(new NewActionEvent({ + userId: this._userId.toString(), + amount: amount.value.toString(), + })); +} +``` + +#### 4. ๅฎšไน‰ๅ‘ฝไปค/ๆŸฅ่ฏข + +```typescript +// src/application/commands/new-action.command.ts +export class NewActionCommand { + constructor( + public readonly userId: string, + public readonly amount: number, + ) {} +} +``` + +#### 5. ๅœจๅบ”็”จๆœๅŠกไธญๅฎž็Žฐ + +```typescript +// src/application/services/wallet-application.service.ts +async newAction(command: NewActionCommand): Promise { + const wallet = await this.walletRepo.findByUserId(BigInt(command.userId)); + if (!wallet) { + throw new WalletNotFoundError(`userId: ${command.userId}`); + } + wallet.newAction(Money.USDT(command.amount)); + await this.walletRepo.save(wallet); + // ่ฎฐๅฝ•ๆตๆฐด็ญ‰... +} +``` + +#### 6. ๆทปๅŠ  DTO + +```typescript +// src/api/dto/request/new-action.dto.ts +export class NewActionDTO { + @ApiProperty({ description: '้‡‘้ข' }) + @IsNumber() + @Min(0) + amount: number; +} +``` + +#### 7. ๆทปๅŠ ๆŽงๅˆถๅ™จ็ซฏ็‚น + +```typescript +// src/api/controllers/wallet.controller.ts +@Post('new-action') +@ApiOperation({ summary: 'ๆ–ฐๆ“ไฝœ' }) +async newAction( + @CurrentUser() user: CurrentUserPayload, + @Body() dto: NewActionDTO, +): Promise<{ message: string }> { + await this.walletService.newAction( + new NewActionCommand(user.userId, dto.amount) + ); + return { message: 'Success' }; +} +``` + +--- + +## ๅ€ผๅฏน่ฑก่ง„่Œƒ + +### Money (้‡‘้ข) + +```typescript +// ๅˆ›ๅปบ +const usdt = Money.USDT(100); // 100 USDT +const bnb = Money.BNB(0.5); // 0.5 BNB +const custom = Money.create(50, 'DST'); // 50 DST + +// ่ฟ็ฎ— +const sum = usdt.add(Money.USDT(50)); // 150 USDT +const diff = usdt.subtract(Money.USDT(30)); // 70 USDT + +// ๆฏ”่พƒ +usdt.equals(Money.USDT(100)); // true +usdt.lessThan(Money.USDT(200)); // true +usdt.isZero(); // false + +// ่Žทๅ–ๅ€ผ +usdt.value; // 100 (number) +usdt.currency; // 'USDT' +``` + +### Balance (ไฝ™้ข) + +```typescript +// ๅˆ›ๅปบ +const balance = Balance.create( + Money.USDT(1000), // available + Money.USDT(100) // frozen +); + +// ๆ“ไฝœ +const afterDeposit = balance.add(Money.USDT(200)); // available + 200 +const afterDeduct = balance.deduct(Money.USDT(50)); // available - 50 +const afterFreeze = balance.freeze(Money.USDT(100)); // available -> frozen +const afterUnfreeze = balance.unfreeze(Money.USDT(50)); // frozen -> available +``` + +### Hashpower (็ฎ—ๅŠ›) + +```typescript +const hp = Hashpower.create(500); +const sum = hp.add(Hashpower.create(100)); // 600 +const value = hp.value; // 500 +``` + +--- + +## ไป“ๅ‚จๆจกๅผ + +### ๆŽฅๅฃๅฎšไน‰ + +```typescript +// src/domain/repositories/wallet-account.repository.interface.ts +export interface IWalletAccountRepository { + findByUserId(userId: bigint): Promise; + getOrCreate(userId: bigint): Promise; + save(wallet: WalletAccount): Promise; +} +``` + +### ๅฎž็Žฐ + +```typescript +// src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts +@Injectable() +export class WalletAccountRepositoryImpl implements IWalletAccountRepository { + constructor(private readonly prisma: PrismaService) {} + + async findByUserId(userId: bigint): Promise { + const record = await this.prisma.walletAccount.findUnique({ + where: { userId }, + }); + if (!record) return null; + return WalletAccount.reconstruct(/* ... */); + } +} +``` + +### ไพ่ต–ๆณจๅ…ฅ + +```typescript +// src/infrastructure/infrastructure.module.ts +@Module({ + providers: [ + { + provide: WALLET_ACCOUNT_REPOSITORY, + useClass: WalletAccountRepositoryImpl, + }, + ], + exports: [WALLET_ACCOUNT_REPOSITORY], +}) +export class InfrastructureModule {} +``` + +--- + +## ๅผ‚ๅธธๅค„็† + +### ้ข†ๅŸŸๅผ‚ๅธธ + +```typescript +// src/shared/exceptions/domain.exception.ts +export class DomainError extends Error { + constructor(message: string) { + super(message); + this.name = 'DomainError'; + } +} + +export class InsufficientBalanceError extends DomainError { + public readonly code = 'INSUFFICIENT_BALANCE'; + constructor(assetType: string, required: string, available: string) { + super(`Insufficient ${assetType} balance: required ${required}, available ${available}`); + } +} +``` + +### ๅผ‚ๅธธ่ฟ‡ๆปคๅ™จ + +```typescript +// src/shared/filters/domain-exception.filter.ts +@Catch(DomainError) +export class DomainExceptionFilter implements ExceptionFilter { + catch(exception: DomainError, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = this.getHttpStatus(exception); + + response.status(status).json({ + success: false, + code: (exception as any).code || 'DOMAIN_ERROR', + message: exception.message, + timestamp: new Date().toISOString(), + }); + } +} +``` + +--- + +## ่ฐƒ่ฏ•ๆŠ€ๅทง + +### VS Code ่ฐƒ่ฏ•้…็ฝฎ + +`.vscode/launch.json`: + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug NestJS", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "start:debug"], + "console": "integratedTerminal", + "restart": true + } + ] +} +``` + +### ๆ—ฅๅฟ—่ฐƒ่ฏ• + +```typescript +// ๅœจไปฃ็ ไธญๆทปๅŠ  +console.log('DEBUG:', JSON.stringify(data, null, 2)); + +// ไฝฟ็”จ NestJS Logger +import { Logger } from '@nestjs/common'; +const logger = new Logger('WalletService'); +logger.log('Processing deposit...'); +logger.debug('Wallet state:', wallet); +``` + +### ๆ•ฐๆฎๅบ“่ฐƒ่ฏ• + +```bash +# ๆŸฅ็œ‹ๆ•ฐๆฎๅบ“ +npx prisma studio + +# ๆŸฅ็œ‹ SQL ๆ—ฅๅฟ— (ๅœจ prisma.service.ts ไธญ) +this.$on('query', (e) => { + console.log('Query:', e.query); + console.log('Params:', e.params); +}); +``` + +--- + +## Git ๅทฅไฝœๆต + +### ๅˆ†ๆ”ฏๅ‘ฝๅ + +- `feature/xxx` - ๆ–ฐๅŠŸ่ƒฝ +- `fix/xxx` - Bug ไฟฎๅค +- `refactor/xxx` - ้‡ๆž„ +- `docs/xxx` - ๆ–‡ๆกฃ + +### ๆไบคไฟกๆฏๆ ผๅผ + +``` +(): + + + +