@k2works/claude-code-booster 3.6.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +42 -42
- package/bin/claude-code-booster +90 -90
- package/lib/assets/.claude/README.md +258 -239
- package/lib/assets/.claude/agent-memory/xp-programmer/MEMORY.md +6 -0
- package/lib/assets/.claude/agent-memory/xp-programmer/project_cargo_tracker.md +11 -0
- package/lib/assets/.claude/agent-memory/xp-programmer/project_ddd_patterns.md +27 -0
- package/lib/assets/.claude/agent-memory/xp-programmer/project_us07_route_assignment.md +19 -0
- package/lib/assets/.claude/scripts/generate-inception-deck.mjs +911 -911
- package/lib/assets/.claude/settings.json +11 -11
- package/lib/assets/.claude/skills/ai-agent-guidelines/SKILL.md +111 -111
- package/lib/assets/.claude/skills/analyzing-architecture/SKILL.md +83 -83
- package/lib/assets/.claude/skills/analyzing-business/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-data-model/SKILL.md +77 -77
- package/lib/assets/.claude/skills/analyzing-domain-model/SKILL.md +117 -117
- package/lib/assets/.claude/skills/analyzing-inception-deck/SKILL.md +84 -84
- package/lib/assets/.claude/skills/analyzing-non-functional/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-operation/SKILL.md +95 -95
- package/lib/assets/.claude/skills/analyzing-requirements/SKILL.md +91 -91
- package/lib/assets/.claude/skills/analyzing-tech-stack/SKILL.md +101 -101
- package/lib/assets/.claude/skills/analyzing-test-strategy/SKILL.md +89 -89
- package/lib/assets/.claude/skills/analyzing-ui-design/SKILL.md +80 -80
- package/lib/assets/.claude/skills/analyzing-usecases/SKILL.md +72 -72
- package/lib/assets/.claude/skills/creating-adr/SKILL.md +113 -113
- package/lib/assets/.claude/skills/developing-backend/SKILL.md +100 -100
- package/lib/assets/.claude/skills/developing-frontend/SKILL.md +93 -93
- package/lib/assets/.claude/skills/developing-release/SKILL.md +120 -120
- package/lib/assets/.claude/skills/generating-bmc/SKILL.md +97 -0
- package/lib/assets/.claude/skills/generating-slides/SKILL.md +94 -94
- package/lib/assets/.claude/skills/git-commit/SKILL.md +81 -81
- package/lib/assets/.claude/skills/killing-processes/SKILL.md +44 -44
- package/lib/assets/.claude/skills/operating-backup/SKILL.md +59 -59
- package/lib/assets/.claude/skills/operating-cicd/SKILL.md +54 -54
- package/lib/assets/.claude/skills/operating-deploy/SKILL.md +67 -67
- package/lib/assets/.claude/skills/operating-docs/SKILL.md +219 -219
- package/lib/assets/.claude/skills/operating-provision/SKILL.md +77 -77
- package/lib/assets/.claude/skills/operating-setup/SKILL.md +63 -63
- package/lib/assets/.claude/skills/orchestrating-analysis/SKILL.md +104 -104
- package/lib/assets/.claude/skills/orchestrating-development/SKILL.md +162 -162
- package/lib/assets/.claude/skills/orchestrating-operation/SKILL.md +158 -158
- package/lib/assets/.claude/skills/orchestrating-project/SKILL.md +144 -144
- package/lib/assets/.claude/skills/planning-releases/SKILL.md +119 -119
- package/lib/assets/.claude/skills/syncing-github-project/SKILL.md +151 -151
- package/lib/assets/.claude/skills/tracking-progress/SKILL.md +91 -91
- package/lib/assets/.claude/skills/validating-iteration-plan/SKILL.md +215 -215
- package/lib/assets/.devcontainer/devcontainer.json +34 -34
- package/lib/assets/.env.example +17 -17
- package/lib/assets/.gitattributes +4 -4
- package/lib/assets/.github/workflows/docker-publish.yml +77 -77
- package/lib/assets/.github/workflows/mkdocs.yml +39 -39
- package/lib/assets/AGENTS.md +94 -94
- package/lib/assets/CLAUDE.md +1 -0
- package/lib/assets/README.md +254 -254
- package/lib/assets/docker-compose.yml +33 -33
- package/lib/assets/docs/adr/index.md +10 -10
- package/lib/assets/docs/article/functional-desgin-ppp/all/01-immutability-and-data-transformation.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/all/02-function-composition.md +519 -519
- package/lib/assets/docs/article/functional-desgin-ppp/all/03-polymorphism.md +537 -537
- package/lib/assets/docs/article/functional-desgin-ppp/all/04-data-validation.md +300 -300
- package/lib/assets/docs/article/functional-desgin-ppp/all/05-property-based-testing.md +320 -320
- package/lib/assets/docs/article/functional-desgin-ppp/all/06-tdd-and-functional.md +498 -498
- package/lib/assets/docs/article/functional-desgin-ppp/all/07-composite-pattern.md +298 -298
- package/lib/assets/docs/article/functional-desgin-ppp/all/08-decorator-pattern.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/all/09-adapter-pattern.md +336 -336
- package/lib/assets/docs/article/functional-desgin-ppp/all/10-strategy-pattern.md +303 -303
- package/lib/assets/docs/article/functional-desgin-ppp/all/11-command-pattern.md +286 -286
- package/lib/assets/docs/article/functional-desgin-ppp/all/12-visitor-pattern.md +322 -322
- package/lib/assets/docs/article/functional-desgin-ppp/all/13-abstract-factory-pattern.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/all/14-abstract-server-pattern.md +365 -365
- package/lib/assets/docs/article/functional-desgin-ppp/all/15-gossiping-bus-drivers.md +156 -156
- package/lib/assets/docs/article/functional-desgin-ppp/all/16-payroll-system.md +178 -178
- package/lib/assets/docs/article/functional-desgin-ppp/all/17-video-rental-system.md +312 -312
- package/lib/assets/docs/article/functional-desgin-ppp/all/18-concurrency-system.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/all/19-wa-tor-simulation.md +286 -286
- package/lib/assets/docs/article/functional-desgin-ppp/all/20-pattern-interactions.md +274 -274
- package/lib/assets/docs/article/functional-desgin-ppp/all/21-best-practices.md +294 -294
- package/lib/assets/docs/article/functional-desgin-ppp/all/22-oo-to-fp-migration.md +337 -337
- package/lib/assets/docs/article/functional-desgin-ppp/all/index.md +388 -388
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/01-immutability-and-data-transformation.md +273 -273
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/02-function-composition.md +380 -380
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/03-polymorphism.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/04-clojure-spec.md +350 -350
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/05-property-based-testing.md +352 -352
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/06-tdd-in-functional.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/07-composite-pattern.md +529 -529
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/08-decorator-pattern.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/09-adapter-pattern.md +399 -399
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/10-strategy-pattern.md +485 -485
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/11-command-pattern.md +566 -566
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/12-visitor-pattern.md +567 -567
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/13-abstract-factory-pattern.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/14-abstract-server-pattern.md +462 -462
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/15-gossiping-bus-drivers.md +325 -325
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/16-payroll-system.md +401 -401
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/17-video-rental-system.md +450 -450
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/18-concurrency-system.md +475 -475
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/19-wator-simulation.md +739 -739
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/20-pattern-interactions.md +567 -567
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/21-best-practices.md +518 -518
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/22-oo-to-fp-migration.md +532 -532
- package/lib/assets/docs/article/functional-desgin-ppp/clojure/index.md +241 -241
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/01-immutability-and-data-transformation.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/02-function-composition.md +374 -374
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/03-polymorphism.md +375 -375
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/04-data-validation.md +195 -195
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/05-property-based-testing.md +268 -268
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/06-tdd-and-fp.md +294 -294
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/07-effects-and-pure-functions.md +164 -164
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/08-error-handling-strategies.md +168 -168
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/09-io-and-external-systems.md +254 -254
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/10-concurrency-patterns.md +269 -269
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/11-command-pattern.md +148 -148
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/12-visitor-pattern.md +176 -176
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/13-abstract-factory-pattern.md +604 -604
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/14-abstract-server-pattern.md +729 -729
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/15-gossiping-bus-drivers.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/16-payroll-system.md +420 -420
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/17-video-rental-system.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/18-concurrency-system.md +466 -466
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/19-wator-simulation.md +523 -523
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/20-pattern-interactions.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/21-best-practices.md +340 -340
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/22-oo-to-fp-migration.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/elixir/index.md +248 -248
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/01-immutability-and-data-transformation.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/02-function-composition.md +452 -452
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/03-polymorphism.md +495 -495
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/04-data-validation.md +416 -416
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/05-property-based-testing.md +382 -382
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/06-tdd-functional.md +687 -687
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/07-composite-pattern.md +442 -442
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/08-decorator-pattern.md +479 -479
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/09-adapter-pattern.md +479 -479
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/10-strategy-pattern.md +427 -427
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/11-command-pattern.md +428 -428
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/12-visitor-pattern.md +339 -339
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/13-abstract-factory-pattern.md +309 -309
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/14-abstract-server-pattern.md +596 -596
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/15-gossiping-bus-drivers.md +355 -355
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/16-payroll-system.md +350 -350
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/17-video-rental-system.md +414 -414
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/18-concurrency-system.md +367 -367
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/19-wator-simulation.md +403 -403
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/20-pattern-interactions.md +291 -291
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/21-best-practices.md +324 -324
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/22-oo-to-fp-migration.md +332 -332
- package/lib/assets/docs/article/functional-desgin-ppp/fsharp/index.md +274 -274
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/01-immutability-and-data-transformation.md +298 -298
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/02-function-composition.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/03-polymorphism.md +362 -362
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/04-data-validation.md +257 -257
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/05-property-based-testing.md +254 -254
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/06-tdd-functional.md +283 -283
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/07-composite-pattern.md +395 -395
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/08-decorator-pattern.md +319 -319
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/09-adapter-pattern.md +382 -382
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/10-strategy-pattern.md +287 -287
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/11-command-pattern.md +303 -303
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/12-visitor-pattern.md +326 -326
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/13-abstract-factory-pattern.md +332 -332
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/14-abstract-server-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/15-gossiping-bus-drivers.md +177 -177
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/16-payroll-system.md +219 -219
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/17-video-rental-system.md +244 -244
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/18-concurrency-system.md +363 -363
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/19-wator-simulation.md +438 -438
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/20-pattern-interactions.md +325 -325
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/21-best-practices.md +403 -403
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/22-oo-to-fp-migration.md +469 -469
- package/lib/assets/docs/article/functional-desgin-ppp/haskell/index.md +174 -174
- package/lib/assets/docs/article/functional-desgin-ppp/index.md +90 -90
- package/lib/assets/docs/article/functional-desgin-ppp/rust/01-immutability-and-data-transformation.md +450 -450
- package/lib/assets/docs/article/functional-desgin-ppp/rust/02-function-composition.md +463 -463
- package/lib/assets/docs/article/functional-desgin-ppp/rust/03-polymorphism.md +425 -425
- package/lib/assets/docs/article/functional-desgin-ppp/rust/04-data-validation.md +273 -273
- package/lib/assets/docs/article/functional-desgin-ppp/rust/05-property-based-testing.md +247 -247
- package/lib/assets/docs/article/functional-desgin-ppp/rust/06-tdd-and-functional.md +841 -841
- package/lib/assets/docs/article/functional-desgin-ppp/rust/07-composite-pattern.md +384 -384
- package/lib/assets/docs/article/functional-desgin-ppp/rust/08-decorator-pattern.md +383 -383
- package/lib/assets/docs/article/functional-desgin-ppp/rust/09-adapter-pattern.md +339 -339
- package/lib/assets/docs/article/functional-desgin-ppp/rust/10-strategy-pattern.md +331 -331
- package/lib/assets/docs/article/functional-desgin-ppp/rust/11-command-pattern.md +356 -356
- package/lib/assets/docs/article/functional-desgin-ppp/rust/12-visitor-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/rust/13-abstract-factory-pattern.md +361 -361
- package/lib/assets/docs/article/functional-desgin-ppp/rust/14-abstract-server-pattern.md +392 -392
- package/lib/assets/docs/article/functional-desgin-ppp/rust/15-gossiping-bus-drivers.md +300 -300
- package/lib/assets/docs/article/functional-desgin-ppp/rust/16-payroll-system.md +297 -297
- package/lib/assets/docs/article/functional-desgin-ppp/rust/17-video-rental-system.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/rust/18-concurrency-system.md +315 -315
- package/lib/assets/docs/article/functional-desgin-ppp/rust/19-wator-simulation.md +311 -311
- package/lib/assets/docs/article/functional-desgin-ppp/rust/20-pattern-interactions.md +304 -304
- package/lib/assets/docs/article/functional-desgin-ppp/rust/21-best-practices.md +336 -336
- package/lib/assets/docs/article/functional-desgin-ppp/rust/22-oo-to-fp-migration.md +349 -349
- package/lib/assets/docs/article/functional-desgin-ppp/rust/index.md +243 -243
- package/lib/assets/docs/article/functional-desgin-ppp/scala/01-immutability-and-data-transformation.md +328 -328
- package/lib/assets/docs/article/functional-desgin-ppp/scala/02-function-composition.md +348 -348
- package/lib/assets/docs/article/functional-desgin-ppp/scala/03-polymorphism.md +357 -357
- package/lib/assets/docs/article/functional-desgin-ppp/scala/04-data-validation.md +364 -364
- package/lib/assets/docs/article/functional-desgin-ppp/scala/05-property-based-testing.md +515 -515
- package/lib/assets/docs/article/functional-desgin-ppp/scala/06-tdd-functional.md +557 -557
- package/lib/assets/docs/article/functional-desgin-ppp/scala/07-composite-pattern.md +363 -363
- package/lib/assets/docs/article/functional-desgin-ppp/scala/08-decorator-pattern.md +327 -327
- package/lib/assets/docs/article/functional-desgin-ppp/scala/09-adapter-pattern.md +517 -517
- package/lib/assets/docs/article/functional-desgin-ppp/scala/10-strategy-pattern.md +441 -441
- package/lib/assets/docs/article/functional-desgin-ppp/scala/11-command-pattern.md +407 -407
- package/lib/assets/docs/article/functional-desgin-ppp/scala/12-visitor-pattern.md +379 -379
- package/lib/assets/docs/article/functional-desgin-ppp/scala/13-abstract-factory-pattern.md +398 -398
- package/lib/assets/docs/article/functional-desgin-ppp/scala/14-abstract-server-pattern.md +476 -476
- package/lib/assets/docs/article/functional-desgin-ppp/scala/15-gossiping-bus-drivers.md +391 -391
- package/lib/assets/docs/article/functional-desgin-ppp/scala/16-payroll-system.md +342 -342
- package/lib/assets/docs/article/functional-desgin-ppp/scala/17-video-rental-system.md +324 -324
- package/lib/assets/docs/article/functional-desgin-ppp/scala/18-concurrency-system.md +730 -730
- package/lib/assets/docs/article/functional-desgin-ppp/scala/19-wator-simulation.md +624 -624
- package/lib/assets/docs/article/functional-desgin-ppp/scala/20-pattern-interactions.md +512 -512
- package/lib/assets/docs/article/functional-desgin-ppp/scala/21-best-practices.md +433 -433
- package/lib/assets/docs/article/functional-desgin-ppp/scala/22-oo-to-fp-migration.md +688 -688
- package/lib/assets/docs/article/functional-desgin-ppp/scala/index.md +243 -243
- package/lib/assets/docs/article/getting-start-tdd/clojure/01-todo-list-and-first-test.md +166 -166
- package/lib/assets/docs/article/getting-start-tdd/clojure/02-fake-it-and-triangulation.md +162 -162
- package/lib/assets/docs/article/getting-start-tdd/clojure/03-obvious-implementation-and-refactoring.md +135 -135
- package/lib/assets/docs/article/getting-start-tdd/clojure/04-version-control-and-conventional-commits.md +88 -88
- package/lib/assets/docs/article/getting-start-tdd/clojure/05-package-management-and-static-analysis.md +299 -299
- package/lib/assets/docs/article/getting-start-tdd/clojure/06-task-runner-and-ci-cd.md +241 -241
- package/lib/assets/docs/article/getting-start-tdd/clojure/07-protocols-and-records.md +131 -131
- package/lib/assets/docs/article/getting-start-tdd/clojure/08-multimethods-and-design-patterns.md +130 -130
- package/lib/assets/docs/article/getting-start-tdd/clojure/09-namespaces-and-module-design.md +127 -127
- package/lib/assets/docs/article/getting-start-tdd/clojure/10-higher-order-functions-and-composition.md +114 -114
- package/lib/assets/docs/article/getting-start-tdd/clojure/11-persistent-data-and-pipeline.md +138 -138
- package/lib/assets/docs/article/getting-start-tdd/clojure/12-error-handling-and-spec.md +161 -161
- package/lib/assets/docs/article/getting-start-tdd/clojure/index.md +65 -65
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter01.md +232 -232
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter02.md +244 -244
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter03.md +202 -202
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter04.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter05.md +256 -256
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter06.md +195 -195
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter07.md +214 -214
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter08.md +249 -249
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter09.md +174 -174
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter10.md +166 -166
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter11.md +192 -192
- package/lib/assets/docs/article/getting-start-tdd/csharp/chapter12.md +211 -211
- package/lib/assets/docs/article/getting-start-tdd/csharp/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/elixir/01-todo-list-and-first-test.md +87 -87
- package/lib/assets/docs/article/getting-start-tdd/elixir/02-fake-it-and-triangulation.md +95 -95
- package/lib/assets/docs/article/getting-start-tdd/elixir/03-obvious-implementation-and-refactoring.md +109 -109
- package/lib/assets/docs/article/getting-start-tdd/elixir/04-version-control-and-conventional-commits.md +96 -96
- package/lib/assets/docs/article/getting-start-tdd/elixir/05-package-management-and-static-analysis.md +88 -88
- package/lib/assets/docs/article/getting-start-tdd/elixir/06-task-runner-and-ci-cd.md +71 -71
- package/lib/assets/docs/article/getting-start-tdd/elixir/07-structs-and-protocols.md +110 -110
- package/lib/assets/docs/article/getting-start-tdd/elixir/08-pattern-matching-and-guards.md +108 -108
- package/lib/assets/docs/article/getting-start-tdd/elixir/09-module-design-and-behaviours.md +104 -104
- package/lib/assets/docs/article/getting-start-tdd/elixir/10-higher-order-functions-and-pipeline.md +178 -178
- package/lib/assets/docs/article/getting-start-tdd/elixir/11-stream-and-lazy-evaluation.md +142 -142
- package/lib/assets/docs/article/getting-start-tdd/elixir/12-error-handling-and-with.md +145 -145
- package/lib/assets/docs/article/getting-start-tdd/elixir/index.md +35 -35
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter01.md +202 -202
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter02.md +246 -246
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter03.md +218 -218
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter04.md +179 -179
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter05.md +267 -267
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter06.md +190 -190
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter07.md +161 -161
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter08.md +175 -175
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter09.md +222 -222
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter10.md +189 -189
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter11.md +212 -212
- package/lib/assets/docs/article/getting-start-tdd/fsharp/chapter12.md +215 -215
- package/lib/assets/docs/article/getting-start-tdd/fsharp/index.md +71 -71
- package/lib/assets/docs/article/getting-start-tdd/go/01-todo-list-and-first-test.md +213 -213
- package/lib/assets/docs/article/getting-start-tdd/go/02-fake-it-and-triangulation.md +302 -302
- package/lib/assets/docs/article/getting-start-tdd/go/03-obvious-implementation-and-refactoring.md +339 -339
- package/lib/assets/docs/article/getting-start-tdd/go/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/go/05-package-management-and-static-analysis.md +272 -272
- package/lib/assets/docs/article/getting-start-tdd/go/06-task-runner-and-ci-cd.md +233 -233
- package/lib/assets/docs/article/getting-start-tdd/go/07-encapsulation-and-polymorphism.md +394 -394
- package/lib/assets/docs/article/getting-start-tdd/go/08-design-patterns.md +422 -422
- package/lib/assets/docs/article/getting-start-tdd/go/09-solid-principles-and-module-design.md +400 -400
- package/lib/assets/docs/article/getting-start-tdd/go/10-higher-order-functions-and-composition.md +226 -226
- package/lib/assets/docs/article/getting-start-tdd/go/11-immutable-data-and-pipeline.md +296 -296
- package/lib/assets/docs/article/getting-start-tdd/go/12-error-handling-and-type-safety.md +411 -411
- package/lib/assets/docs/article/getting-start-tdd/go/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/haskell/01-todo-list-and-first-test.md +279 -279
- package/lib/assets/docs/article/getting-start-tdd/haskell/02-fake-it-and-triangulation.md +337 -337
- package/lib/assets/docs/article/getting-start-tdd/haskell/03-obvious-implementation-and-refactoring.md +257 -257
- package/lib/assets/docs/article/getting-start-tdd/haskell/04-version-control-and-conventional-commits.md +182 -182
- package/lib/assets/docs/article/getting-start-tdd/haskell/05-package-management-and-static-analysis.md +313 -313
- package/lib/assets/docs/article/getting-start-tdd/haskell/06-task-runner-and-ci-cd.md +309 -309
- package/lib/assets/docs/article/getting-start-tdd/haskell/07-algebraic-data-types-and-type-classes.md +412 -412
- package/lib/assets/docs/article/getting-start-tdd/haskell/08-pattern-matching-and-guards.md +390 -390
- package/lib/assets/docs/article/getting-start-tdd/haskell/09-module-design-and-smart-constructors.md +461 -461
- package/lib/assets/docs/article/getting-start-tdd/haskell/10-higher-order-functions-and-currying.md +434 -434
- package/lib/assets/docs/article/getting-start-tdd/haskell/11-function-composition-and-point-free.md +392 -392
- package/lib/assets/docs/article/getting-start-tdd/haskell/12-monad-and-error-handling.md +631 -631
- package/lib/assets/docs/article/getting-start-tdd/haskell/index.md +49 -49
- package/lib/assets/docs/article/getting-start-tdd/index.md +93 -93
- package/lib/assets/docs/article/getting-start-tdd/integration/01-language-overview.md +375 -375
- package/lib/assets/docs/article/getting-start-tdd/integration/02-test-framework-comparison.md +349 -349
- package/lib/assets/docs/article/getting-start-tdd/integration/03-tdd-pattern-comparison.md +445 -445
- package/lib/assets/docs/article/getting-start-tdd/integration/04-type-system-comparison.md +409 -409
- package/lib/assets/docs/article/getting-start-tdd/integration/05-dev-environment-comparison.md +330 -330
- package/lib/assets/docs/article/getting-start-tdd/integration/06-learning-roadmap.md +290 -290
- package/lib/assets/docs/article/getting-start-tdd/integration/index.md +69 -69
- package/lib/assets/docs/article/getting-start-tdd/java/01-todo-list-and-first-test.md +234 -234
- package/lib/assets/docs/article/getting-start-tdd/java/02-fake-it-and-triangulation.md +261 -261
- package/lib/assets/docs/article/getting-start-tdd/java/03-obvious-implementation-and-refactoring.md +185 -185
- package/lib/assets/docs/article/getting-start-tdd/java/04-version-control-and-conventional-commits.md +115 -115
- package/lib/assets/docs/article/getting-start-tdd/java/05-package-management-and-static-analysis.md +382 -382
- package/lib/assets/docs/article/getting-start-tdd/java/06-task-runner-and-ci-cd.md +272 -272
- package/lib/assets/docs/article/getting-start-tdd/java/07-encapsulation-and-polymorphism.md +626 -626
- package/lib/assets/docs/article/getting-start-tdd/java/08-design-patterns.md +393 -393
- package/lib/assets/docs/article/getting-start-tdd/java/09-solid-principles-and-module-design.md +310 -310
- package/lib/assets/docs/article/getting-start-tdd/java/10-higher-order-functions-and-composition.md +188 -188
- package/lib/assets/docs/article/getting-start-tdd/java/11-immutable-data-and-pipeline.md +167 -167
- package/lib/assets/docs/article/getting-start-tdd/java/12-error-handling-and-type-safety.md +205 -205
- package/lib/assets/docs/article/getting-start-tdd/java/index.md +61 -61
- package/lib/assets/docs/article/getting-start-tdd/node/01-todo-list-and-first-test.md +244 -244
- package/lib/assets/docs/article/getting-start-tdd/node/02-fake-it-and-triangulation.md +262 -262
- package/lib/assets/docs/article/getting-start-tdd/node/03-obvious-implementation-and-refactoring.md +169 -169
- package/lib/assets/docs/article/getting-start-tdd/node/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/node/05-package-management-and-static-analysis.md +314 -314
- package/lib/assets/docs/article/getting-start-tdd/node/06-task-runner-and-ci-cd.md +235 -235
- package/lib/assets/docs/article/getting-start-tdd/node/07-encapsulation-and-polymorphism.md +327 -327
- package/lib/assets/docs/article/getting-start-tdd/node/08-design-patterns.md +322 -322
- package/lib/assets/docs/article/getting-start-tdd/node/09-solid-principles-and-module-design.md +285 -285
- package/lib/assets/docs/article/getting-start-tdd/node/10-higher-order-functions-and-composition.md +199 -199
- package/lib/assets/docs/article/getting-start-tdd/node/11-immutable-data-and-pipeline.md +207 -207
- package/lib/assets/docs/article/getting-start-tdd/node/12-error-handling-and-type-safety.md +295 -295
- package/lib/assets/docs/article/getting-start-tdd/node/index.md +56 -56
- package/lib/assets/docs/article/getting-start-tdd/php/01-todo-list-and-first-test.md +259 -259
- package/lib/assets/docs/article/getting-start-tdd/php/02-fake-it-and-triangulation.md +200 -200
- package/lib/assets/docs/article/getting-start-tdd/php/03-obvious-implementation-and-refactoring.md +248 -248
- package/lib/assets/docs/article/getting-start-tdd/php/04-version-control-and-conventional-commits.md +141 -141
- package/lib/assets/docs/article/getting-start-tdd/php/05-package-management-and-static-analysis.md +410 -410
- package/lib/assets/docs/article/getting-start-tdd/php/06-task-runner-and-ci-cd.md +321 -321
- package/lib/assets/docs/article/getting-start-tdd/php/07-encapsulation-and-polymorphism.md +372 -372
- package/lib/assets/docs/article/getting-start-tdd/php/08-design-patterns.md +453 -453
- package/lib/assets/docs/article/getting-start-tdd/php/09-solid-principles-and-module-design.md +460 -460
- package/lib/assets/docs/article/getting-start-tdd/php/10-higher-order-functions-and-composition.md +182 -182
- package/lib/assets/docs/article/getting-start-tdd/php/11-immutable-data-and-pipeline.md +266 -266
- package/lib/assets/docs/article/getting-start-tdd/php/12-error-handling-and-type-safety.md +308 -308
- package/lib/assets/docs/article/getting-start-tdd/php/index.md +84 -84
- package/lib/assets/docs/article/getting-start-tdd/python/01-todo-list-and-first-test.md +201 -201
- package/lib/assets/docs/article/getting-start-tdd/python/02-fake-it-and-triangulation.md +247 -247
- package/lib/assets/docs/article/getting-start-tdd/python/03-obvious-implementation-and-refactoring.md +199 -199
- package/lib/assets/docs/article/getting-start-tdd/python/04-version-control-and-conventional-commits.md +87 -87
- package/lib/assets/docs/article/getting-start-tdd/python/05-package-management-and-static-analysis.md +274 -274
- package/lib/assets/docs/article/getting-start-tdd/python/06-task-runner-and-ci-cd.md +190 -190
- package/lib/assets/docs/article/getting-start-tdd/python/07-encapsulation-and-polymorphism.md +208 -208
- package/lib/assets/docs/article/getting-start-tdd/python/08-design-patterns.md +172 -172
- package/lib/assets/docs/article/getting-start-tdd/python/09-solid-principles-and-module-design.md +130 -130
- package/lib/assets/docs/article/getting-start-tdd/python/10-higher-order-functions-and-composition.md +122 -122
- package/lib/assets/docs/article/getting-start-tdd/python/11-immutable-data-and-pipeline.md +116 -116
- package/lib/assets/docs/article/getting-start-tdd/python/12-error-handling-and-type-safety.md +126 -126
- package/lib/assets/docs/article/getting-start-tdd/python/index.md +55 -55
- package/lib/assets/docs/article/getting-start-tdd/ruby/01-todo-list-and-first-test.md +231 -231
- package/lib/assets/docs/article/getting-start-tdd/ruby/02-fake-it-and-triangulation.md +238 -238
- package/lib/assets/docs/article/getting-start-tdd/ruby/03-obvious-implementation-and-refactoring.md +228 -228
- package/lib/assets/docs/article/getting-start-tdd/ruby/04-version-control-and-conventional-commits.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/ruby/05-package-management-and-static-analysis.md +287 -287
- package/lib/assets/docs/article/getting-start-tdd/ruby/06-task-runner-and-ci-cd.md +248 -248
- package/lib/assets/docs/article/getting-start-tdd/ruby/07-encapsulation-and-polymorphism.md +279 -279
- package/lib/assets/docs/article/getting-start-tdd/ruby/08-design-patterns.md +329 -329
- package/lib/assets/docs/article/getting-start-tdd/ruby/09-solid-principles-and-module-design.md +196 -196
- package/lib/assets/docs/article/getting-start-tdd/ruby/10-higher-order-functions-and-composition.md +175 -175
- package/lib/assets/docs/article/getting-start-tdd/ruby/11-immutable-data-and-pipeline.md +237 -237
- package/lib/assets/docs/article/getting-start-tdd/ruby/12-error-handling-and-type-safety.md +398 -398
- package/lib/assets/docs/article/getting-start-tdd/ruby/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/rust/01-todo-list-and-first-test.md +211 -211
- package/lib/assets/docs/article/getting-start-tdd/rust/02-fake-it-and-triangulation.md +264 -264
- package/lib/assets/docs/article/getting-start-tdd/rust/03-obvious-implementation-and-refactoring.md +233 -233
- package/lib/assets/docs/article/getting-start-tdd/rust/04-version-control-and-conventional-commits.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/rust/05-package-management-and-static-analysis.md +212 -212
- package/lib/assets/docs/article/getting-start-tdd/rust/06-task-runner-and-ci-cd.md +164 -164
- package/lib/assets/docs/article/getting-start-tdd/rust/07-encapsulation-and-polymorphism.md +142 -142
- package/lib/assets/docs/article/getting-start-tdd/rust/08-design-patterns.md +145 -145
- package/lib/assets/docs/article/getting-start-tdd/rust/09-solid-principles-and-module-design.md +110 -110
- package/lib/assets/docs/article/getting-start-tdd/rust/10-higher-order-functions-and-composition.md +94 -94
- package/lib/assets/docs/article/getting-start-tdd/rust/11-immutable-data-and-pipeline.md +105 -105
- package/lib/assets/docs/article/getting-start-tdd/rust/12-error-handling-and-type-safety.md +112 -112
- package/lib/assets/docs/article/getting-start-tdd/rust/index.md +83 -83
- package/lib/assets/docs/article/getting-start-tdd/scala/01-todo-list-and-first-test.md +111 -111
- package/lib/assets/docs/article/getting-start-tdd/scala/02-fake-it-and-triangulation.md +107 -107
- package/lib/assets/docs/article/getting-start-tdd/scala/03-obvious-implementation-and-refactoring.md +99 -99
- package/lib/assets/docs/article/getting-start-tdd/scala/04-version-control-and-conventional-commits.md +123 -123
- package/lib/assets/docs/article/getting-start-tdd/scala/05-package-management-and-static-analysis.md +196 -196
- package/lib/assets/docs/article/getting-start-tdd/scala/06-task-runner-and-ci-cd.md +186 -186
- package/lib/assets/docs/article/getting-start-tdd/scala/07-case-classes-and-traits.md +139 -139
- package/lib/assets/docs/article/getting-start-tdd/scala/08-pattern-matching-and-sealed-traits.md +106 -106
- package/lib/assets/docs/article/getting-start-tdd/scala/09-packages-and-module-design.md +75 -75
- package/lib/assets/docs/article/getting-start-tdd/scala/10-higher-order-functions-and-composition.md +104 -104
- package/lib/assets/docs/article/getting-start-tdd/scala/11-collections-and-lazy-evaluation.md +94 -94
- package/lib/assets/docs/article/getting-start-tdd/scala/12-error-handling-and-type-safety.md +92 -92
- package/lib/assets/docs/article/getting-start-tdd/scala/index.md +65 -65
- package/lib/assets/docs/article/grokking-concurrency/all/index.md +404 -404
- package/lib/assets/docs/article/grokking-concurrency/all/part-1-ch02-sequential.md +554 -554
- package/lib/assets/docs/article/grokking-concurrency/all/part-2-ch04-05-threads.md +469 -469
- package/lib/assets/docs/article/grokking-concurrency/all/part-3-ch06-multitasking.md +520 -520
- package/lib/assets/docs/article/grokking-concurrency/all/part-4-ch07-parallel-patterns.md +420 -420
- package/lib/assets/docs/article/grokking-concurrency/all/part-5-ch08-09-synchronization.md +510 -510
- package/lib/assets/docs/article/grokking-concurrency/all/part-6-ch10-11-nonblocking-io.md +435 -435
- package/lib/assets/docs/article/grokking-concurrency/all/part-7-ch12-async.md +465 -465
- package/lib/assets/docs/article/grokking-concurrency/all/part-8-ch13-mapreduce.md +377 -377
- package/lib/assets/docs/article/grokking-concurrency/clojure/index.md +116 -116
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-1.md +108 -108
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-2.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-3.md +122 -122
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-4.md +123 -123
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-5.md +118 -118
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-6.md +89 -89
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-7.md +100 -100
- package/lib/assets/docs/article/grokking-concurrency/clojure/part-8.md +120 -120
- package/lib/assets/docs/article/grokking-concurrency/csharp/index.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-1.md +97 -97
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-2.md +123 -123
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-3.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-4.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-5.md +99 -99
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-6.md +61 -61
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-7.md +84 -84
- package/lib/assets/docs/article/grokking-concurrency/csharp/part-8.md +92 -92
- package/lib/assets/docs/article/grokking-concurrency/fsharp/index.md +65 -65
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-1.md +80 -80
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-2.md +103 -103
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-3.md +94 -94
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-4.md +110 -110
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-5.md +104 -104
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-6.md +93 -93
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-7.md +121 -121
- package/lib/assets/docs/article/grokking-concurrency/fsharp/part-8.md +107 -107
- package/lib/assets/docs/article/grokking-concurrency/haskell/index.md +248 -248
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-1.md +96 -96
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-2.md +96 -96
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-3.md +91 -91
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-4.md +106 -106
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-5.md +99 -99
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-6.md +95 -95
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-7.md +111 -111
- package/lib/assets/docs/article/grokking-concurrency/haskell/part-8.md +118 -118
- package/lib/assets/docs/article/grokking-concurrency/index.md +66 -66
- package/lib/assets/docs/article/grokking-concurrency/java/index.md +102 -102
- package/lib/assets/docs/article/grokking-concurrency/java/part-1.md +308 -308
- package/lib/assets/docs/article/grokking-concurrency/java/part-2.md +334 -334
- package/lib/assets/docs/article/grokking-concurrency/java/part-3.md +221 -221
- package/lib/assets/docs/article/grokking-concurrency/java/part-4.md +213 -213
- package/lib/assets/docs/article/grokking-concurrency/java/part-5.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/java/part-6.md +69 -69
- package/lib/assets/docs/article/grokking-concurrency/java/part-7.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/java/part-8.md +101 -101
- package/lib/assets/docs/article/grokking-concurrency/python/index.md +313 -313
- package/lib/assets/docs/article/grokking-concurrency/python/part-1.md +239 -239
- package/lib/assets/docs/article/grokking-concurrency/python/part-2.md +418 -418
- package/lib/assets/docs/article/grokking-concurrency/python/part-3.md +227 -227
- package/lib/assets/docs/article/grokking-concurrency/python/part-4.md +299 -299
- package/lib/assets/docs/article/grokking-concurrency/python/part-5.md +315 -315
- package/lib/assets/docs/article/grokking-concurrency/python/part-6.md +297 -297
- package/lib/assets/docs/article/grokking-concurrency/python/part-7.md +314 -314
- package/lib/assets/docs/article/grokking-concurrency/python/part-8.md +360 -360
- package/lib/assets/docs/article/grokking-concurrency/rust/index.md +270 -270
- package/lib/assets/docs/article/grokking-concurrency/rust/part-1.md +108 -108
- package/lib/assets/docs/article/grokking-concurrency/rust/part-2.md +120 -120
- package/lib/assets/docs/article/grokking-concurrency/rust/part-3.md +126 -126
- package/lib/assets/docs/article/grokking-concurrency/rust/part-4.md +175 -175
- package/lib/assets/docs/article/grokking-concurrency/rust/part-5.md +158 -158
- package/lib/assets/docs/article/grokking-concurrency/rust/part-6.md +94 -94
- package/lib/assets/docs/article/grokking-concurrency/rust/part-7.md +133 -133
- package/lib/assets/docs/article/grokking-concurrency/rust/part-8.md +155 -155
- package/lib/assets/docs/article/grokking-concurrency/scala/index.md +69 -69
- package/lib/assets/docs/article/grokking-concurrency/scala/part-1.md +78 -78
- package/lib/assets/docs/article/grokking-concurrency/scala/part-2.md +112 -112
- package/lib/assets/docs/article/grokking-concurrency/scala/part-3.md +93 -93
- package/lib/assets/docs/article/grokking-concurrency/scala/part-4.md +110 -110
- package/lib/assets/docs/article/grokking-concurrency/scala/part-5.md +119 -119
- package/lib/assets/docs/article/grokking-concurrency/scala/part-6.md +83 -83
- package/lib/assets/docs/article/grokking-concurrency/scala/part-7.md +131 -131
- package/lib/assets/docs/article/grokking-concurrency/scala/part-8.md +129 -129
- package/lib/assets/docs/article/grokkingfp/all/index.md +368 -368
- package/lib/assets/docs/article/grokkingfp/all/part-1-ch01-fp-introduction.md +530 -530
- package/lib/assets/docs/article/grokkingfp/all/part-1-ch02-pure-functions.md +923 -923
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch03-immutable-data.md +1128 -1128
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch04-higher-order-functions.md +1104 -1104
- package/lib/assets/docs/article/grokkingfp/all/part-2-ch05-flatmap.md +1026 -1026
- package/lib/assets/docs/article/grokkingfp/all/part-3-ch06-option.md +785 -785
- package/lib/assets/docs/article/grokkingfp/all/part-3-ch07-either-adt.md +871 -871
- package/lib/assets/docs/article/grokkingfp/all/part-4-ch08-io-monad.md +972 -972
- package/lib/assets/docs/article/grokkingfp/all/part-4-ch09-streams.md +926 -926
- package/lib/assets/docs/article/grokkingfp/all/part-5-ch10-concurrency.md +870 -870
- package/lib/assets/docs/article/grokkingfp/all/part-6-ch11-application.md +715 -715
- package/lib/assets/docs/article/grokkingfp/all/part-6-ch12-testing.md +626 -626
- package/lib/assets/docs/article/grokkingfp/all/writing-plan.md +712 -712
- package/lib/assets/docs/article/grokkingfp/clojure/index.md +276 -276
- package/lib/assets/docs/article/grokkingfp/clojure/part-1.md +667 -667
- package/lib/assets/docs/article/grokkingfp/clojure/part-2.md +643 -643
- package/lib/assets/docs/article/grokkingfp/clojure/part-3.md +620 -620
- package/lib/assets/docs/article/grokkingfp/clojure/part-4.md +697 -697
- package/lib/assets/docs/article/grokkingfp/clojure/part-5.md +751 -751
- package/lib/assets/docs/article/grokkingfp/clojure/part-6.md +721 -721
- package/lib/assets/docs/article/grokkingfp/csharp/index.md +246 -246
- package/lib/assets/docs/article/grokkingfp/csharp/part-1.md +811 -811
- package/lib/assets/docs/article/grokkingfp/csharp/part-2.md +971 -971
- package/lib/assets/docs/article/grokkingfp/csharp/part-3.md +981 -981
- package/lib/assets/docs/article/grokkingfp/csharp/part-4.md +949 -949
- package/lib/assets/docs/article/grokkingfp/csharp/part-5.md +947 -947
- package/lib/assets/docs/article/grokkingfp/csharp/part-6.md +739 -739
- package/lib/assets/docs/article/grokkingfp/elixir/index.md +203 -203
- package/lib/assets/docs/article/grokkingfp/elixir/part-1.md +712 -712
- package/lib/assets/docs/article/grokkingfp/elixir/part-2.md +838 -838
- package/lib/assets/docs/article/grokkingfp/elixir/part-3.md +985 -985
- package/lib/assets/docs/article/grokkingfp/elixir/part-4.md +974 -974
- package/lib/assets/docs/article/grokkingfp/elixir/part-5.md +1286 -1286
- package/lib/assets/docs/article/grokkingfp/elixir/part-6.md +1049 -1049
- package/lib/assets/docs/article/grokkingfp/fsharp/index.md +210 -210
- package/lib/assets/docs/article/grokkingfp/fsharp/part-1.md +714 -714
- package/lib/assets/docs/article/grokkingfp/fsharp/part-2.md +961 -961
- package/lib/assets/docs/article/grokkingfp/fsharp/part-3.md +972 -972
- package/lib/assets/docs/article/grokkingfp/fsharp/part-4.md +832 -832
- package/lib/assets/docs/article/grokkingfp/fsharp/part-5.md +911 -911
- package/lib/assets/docs/article/grokkingfp/fsharp/part-6.md +922 -922
- package/lib/assets/docs/article/grokkingfp/haskell/index.md +234 -234
- package/lib/assets/docs/article/grokkingfp/haskell/part-1.md +591 -591
- package/lib/assets/docs/article/grokkingfp/haskell/part-2.md +866 -866
- package/lib/assets/docs/article/grokkingfp/haskell/part-3.md +915 -915
- package/lib/assets/docs/article/grokkingfp/haskell/part-4.md +878 -878
- package/lib/assets/docs/article/grokkingfp/haskell/part-5.md +845 -845
- package/lib/assets/docs/article/grokkingfp/haskell/part-6.md +844 -844
- package/lib/assets/docs/article/grokkingfp/index.md +143 -143
- package/lib/assets/docs/article/grokkingfp/java/index.md +211 -211
- package/lib/assets/docs/article/grokkingfp/java/part-1.md +648 -648
- package/lib/assets/docs/article/grokkingfp/java/part-2.md +675 -675
- package/lib/assets/docs/article/grokkingfp/java/part-3.md +672 -672
- package/lib/assets/docs/article/grokkingfp/java/part-4.md +771 -771
- package/lib/assets/docs/article/grokkingfp/java/part-5.md +959 -959
- package/lib/assets/docs/article/grokkingfp/java/part-6.md +1328 -1328
- package/lib/assets/docs/article/grokkingfp/python/index.md +258 -258
- package/lib/assets/docs/article/grokkingfp/python/part-1.md +443 -443
- package/lib/assets/docs/article/grokkingfp/python/part-2.md +958 -958
- package/lib/assets/docs/article/grokkingfp/python/part-3.md +1004 -1004
- package/lib/assets/docs/article/grokkingfp/python/part-4.md +765 -765
- package/lib/assets/docs/article/grokkingfp/python/part-5.md +747 -747
- package/lib/assets/docs/article/grokkingfp/python/part-6.md +861 -861
- package/lib/assets/docs/article/grokkingfp/ruby/index.md +330 -330
- package/lib/assets/docs/article/grokkingfp/ruby/part-1.md +755 -755
- package/lib/assets/docs/article/grokkingfp/ruby/part-2.md +938 -938
- package/lib/assets/docs/article/grokkingfp/ruby/part-3.md +946 -946
- package/lib/assets/docs/article/grokkingfp/ruby/part-4.md +921 -921
- package/lib/assets/docs/article/grokkingfp/ruby/part-5.md +908 -908
- package/lib/assets/docs/article/grokkingfp/ruby/part-6.md +1412 -1412
- package/lib/assets/docs/article/grokkingfp/rust/index.md +242 -242
- package/lib/assets/docs/article/grokkingfp/rust/part-1.md +634 -634
- package/lib/assets/docs/article/grokkingfp/rust/part-2.md +1060 -1060
- package/lib/assets/docs/article/grokkingfp/rust/part-3.md +994 -994
- package/lib/assets/docs/article/grokkingfp/rust/part-4.md +573 -573
- package/lib/assets/docs/article/grokkingfp/rust/part-5.md +705 -705
- package/lib/assets/docs/article/grokkingfp/rust/part-6.md +508 -508
- package/lib/assets/docs/article/grokkingfp/scala/index.md +171 -171
- package/lib/assets/docs/article/grokkingfp/scala/part-1.md +543 -543
- package/lib/assets/docs/article/grokkingfp/scala/part-2.md +946 -946
- package/lib/assets/docs/article/grokkingfp/scala/part-3.md +919 -919
- package/lib/assets/docs/article/grokkingfp/scala/part-4.md +742 -742
- package/lib/assets/docs/article/grokkingfp/scala/part-5.md +722 -722
- package/lib/assets/docs/article/grokkingfp/scala/part-6.md +867 -867
- package/lib/assets/docs/article/grokkingfp/typescript/index.md +273 -273
- package/lib/assets/docs/article/grokkingfp/typescript/part-1.md +561 -561
- package/lib/assets/docs/article/grokkingfp/typescript/part-2.md +1129 -1129
- package/lib/assets/docs/article/grokkingfp/typescript/part-3.md +842 -842
- package/lib/assets/docs/article/grokkingfp/typescript/part-4.md +1087 -1087
- package/lib/assets/docs/article/grokkingfp/typescript/part-5.md +717 -717
- package/lib/assets/docs/article/grokkingfp/typescript/part-6.md +982 -982
- package/lib/assets/docs/article/practical-database-design/index.md +121 -121
- package/lib/assets/docs/article/practical-database-design/part1/chapter01.md +288 -288
- package/lib/assets/docs/article/practical-database-design/part1/chapter02.md +518 -518
- package/lib/assets/docs/article/practical-database-design/part1/chapter03.md +557 -557
- package/lib/assets/docs/article/practical-database-design/part2/chapter04.md +924 -924
- package/lib/assets/docs/article/practical-database-design/part2/chapter05.md +1627 -1627
- package/lib/assets/docs/article/practical-database-design/part2/chapter06.md +2716 -2716
- package/lib/assets/docs/article/practical-database-design/part2/chapter07.md +2082 -2082
- package/lib/assets/docs/article/practical-database-design/part2/chapter08.md +2105 -2105
- package/lib/assets/docs/article/practical-database-design/part2/chapter09.md +2031 -2031
- package/lib/assets/docs/article/practical-database-design/part2/chapter10.md +1387 -1387
- package/lib/assets/docs/article/practical-database-design/part2/chapter11.md +1677 -1677
- package/lib/assets/docs/article/practical-database-design/part2/chapter12.md +1417 -1417
- package/lib/assets/docs/article/practical-database-design/part2/chapter13.md +1434 -1434
- package/lib/assets/docs/article/practical-database-design/part3/chapter14.md +667 -667
- package/lib/assets/docs/article/practical-database-design/part3/chapter15.md +1625 -1625
- package/lib/assets/docs/article/practical-database-design/part3/chapter16.md +1915 -1915
- package/lib/assets/docs/article/practical-database-design/part3/chapter17.md +1708 -1708
- package/lib/assets/docs/article/practical-database-design/part3/chapter18.md +2095 -2095
- package/lib/assets/docs/article/practical-database-design/part3/chapter19.md +1123 -1123
- package/lib/assets/docs/article/practical-database-design/part3/chapter20.md +1031 -1031
- package/lib/assets/docs/article/practical-database-design/part3/chapter21.md +1382 -1382
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter14-orm.md +991 -991
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter15-orm.md +1300 -1300
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter16-orm.md +1166 -1166
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter17-orm.md +1584 -1584
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter18-orm.md +1183 -1183
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter19-orm.md +1016 -1016
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter20-orm.md +1753 -1753
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter21-orm.md +1447 -1447
- package/lib/assets/docs/article/practical-database-design/part3-orm/chapter22-orm.md +1878 -1878
- package/lib/assets/docs/article/practical-database-design/part4/chapter22.md +965 -965
- package/lib/assets/docs/article/practical-database-design/part4/chapter23.md +2069 -2069
- package/lib/assets/docs/article/practical-database-design/part4/chapter24.md +2439 -2439
- package/lib/assets/docs/article/practical-database-design/part4/chapter25.md +3661 -3661
- package/lib/assets/docs/article/practical-database-design/part4/chapter26.md +2916 -2916
- package/lib/assets/docs/article/practical-database-design/part4/chapter27.md +3105 -3105
- package/lib/assets/docs/article/practical-database-design/part4/chapter28.md +2697 -2697
- package/lib/assets/docs/article/practical-database-design/part4/chapter29.md +2544 -2544
- package/lib/assets/docs/article/practical-database-design/part4/chapter30.md +2180 -2180
- package/lib/assets/docs/article/practical-database-design/part4/chapter31.md +1192 -1192
- package/lib/assets/docs/article/practical-database-design/part4/chapter32.md +2101 -2101
- package/lib/assets/docs/article/practical-database-design/part5/chapter33.md +1032 -1032
- package/lib/assets/docs/article/practical-database-design/part5/chapter34.md +1609 -1609
- package/lib/assets/docs/article/practical-database-design/part5/chapter35.md +1453 -1453
- package/lib/assets/docs/article/practical-database-design/part5/chapter36.md +1292 -1292
- package/lib/assets/docs/article/practical-database-design/part5/chapter37.md +1470 -1470
- package/lib/assets/docs/article/practical-database-design/part5/chapter38.md +1698 -1698
- package/lib/assets/docs/article/practical-database-design/part5/chapter39.md +2334 -2334
- package/lib/assets/docs/article/practical-database-design/study/study2-1.md +1693 -1693
- package/lib/assets/docs/article/practical-database-design/study/study2-2.md +1347 -1347
- package/lib/assets/docs/article/practical-database-design/study/study2-3.md +2044 -2044
- package/lib/assets/docs/article/practical-database-design/study/study2-4.md +2229 -2229
- package/lib/assets/docs/article/practical-database-design/study/study2-5.md +2418 -2418
- package/lib/assets/docs/article/practical-database-design/study/study3-1.md +2205 -2205
- package/lib/assets/docs/article/practical-database-design/study/study3-2.md +2221 -2221
- package/lib/assets/docs/article/practical-database-design/study/study3-3.md +2253 -2253
- package/lib/assets/docs/article/practical-database-design/study/study3-4.md +2106 -2106
- package/lib/assets/docs/article/practical-database-design/study/study3-5.md +2507 -2507
- package/lib/assets/docs/article/practical-database-design/study/study4-1.md +2587 -2587
- package/lib/assets/docs/article/practical-database-design/study/study4-2.md +2075 -2075
- package/lib/assets/docs/article/practical-database-design/study/study4-3.md +1805 -1805
- package/lib/assets/docs/article/practical-database-design/study/study4-4.md +1895 -1895
- package/lib/assets/docs/article/practical-database-design/study/study4-5.md +2878 -2878
- package/lib/assets/docs/assets/css/extra.css +29 -29
- package/lib/assets/docs/assets/js/extra.js +44 -44
- package/lib/assets/docs/development/index.md +39 -39
- package/lib/assets/docs/operation/index.md +11 -11
- package/lib/assets/docs/reference/CodexCLIMCP/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/351/226/213/347/231/272/343/203/225/343/203/255/343/203/274.md +532 -532
- package/lib/assets/docs/reference/CodexCLIMCP/343/202/265/343/203/274/343/203/220/343/203/274/350/250/255/345/256/232/346/211/213/351/240/206.md +341 -341
- package/lib/assets/docs/reference/Java/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/346/247/213/347/257/211/343/202/254/343/202/244/343/203/211.md +581 -581
- package/lib/assets/docs/reference/SonarQube/343/203/255/343/203/274/343/202/253/343/203/253/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +642 -642
- package/lib/assets/docs/reference/TypeScript/343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/346/247/213/347/257/211/343/202/254/343/202/244/343/203/211.md +465 -465
- package/lib/assets/docs/reference/UI/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +450 -450
- package/lib/assets/docs/reference/images/Ansoff.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandBasicStrategy.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandCategorization.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandRecurutementStrategy.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BrandValue.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/BusinessActivitiy.svg +3 -3
- package/lib/assets/docs/reference/images/HRM.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/MarketingStructure.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/OrganizationElemnts.svg +3 -3
- package/lib/assets/docs/reference/images/PPM.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/PositioningMap.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ProductLayer.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ProductMix.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/SWOT.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/TargetMarket.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ThreeGenericStrategies.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/VRIO.drawio.svg +3 -3
- package/lib/assets/docs/reference/images/ValueChain.drawio.svg +3 -3
- package/lib/assets/docs/reference/index.md +52 -52
- package/lib/assets/docs/reference//343/202/210/343/201/204/343/202/275/343/203/225/343/203/210/343/202/246/343/202/247/343/202/242/343/201/250/343/201/257.md +250 -250
- package/lib/assets/docs/reference//343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +2216 -2216
- package/lib/assets/docs/reference//343/202/244/343/203/263/343/203/225/343/203/251/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +1878 -1878
- package/lib/assets/docs/reference//343/202/250/343/202/257/343/202/271/343/203/210/343/203/252/343/203/274/343/203/240/343/203/227/343/203/255/343/202/260/343/203/251/343/203/237/343/203/263/343/202/260.md +550 -550
- package/lib/assets/docs/reference//343/202/263/343/203/274/343/203/207/343/202/243/343/203/263/343/202/260/343/201/250/343/203/206/343/202/271/343/203/210/343/202/254/343/202/244/343/203/211.md +705 -705
- package/lib/assets/docs/reference//343/203/206/343/202/271/343/203/210/346/210/246/347/225/245/343/202/254/343/202/244/343/203/211.md +1313 -1313
- package/lib/assets/docs/reference//343/203/207/343/203/274/343/202/277/343/203/242/343/203/207/343/203/253/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +311 -311
- package/lib/assets/docs/reference//343/203/211/343/203/241/343/202/244/343/203/263/343/203/242/343/203/207/343/203/253/350/250/255/350/250/210/343/202/254/343/202/244/343/203/211.md +599 -599
- package/lib/assets/docs/reference//343/203/223/343/202/270/343/203/215/343/202/271/343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243/345/210/206/346/236/220/343/202/254/343/202/244/343/203/211.md +528 -528
- package/lib/assets/docs/reference//343/203/246/343/203/274/343/202/271/343/202/261/343/203/274/343/202/271/344/275/234/346/210/220/343/202/254/343/202/244/343/203/211.md +689 -689
- package/lib/assets/docs/reference//343/203/252/343/203/252/343/203/274/343/202/271/343/202/254/343/202/244/343/203/211.md +461 -461
- package/lib/assets/docs/reference//343/203/252/343/203/252/343/203/274/343/202/271/343/203/273/343/202/244/343/203/206/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/350/250/210/347/224/273/343/202/254/343/202/244/343/203/211.md +580 -580
- package/lib/assets/docs/reference//343/203/255/343/202/270/343/202/253/343/203/253/343/202/267/343/203/263/343/202/255/343/203/263/343/202/260.md +1367 -1367
- package/lib/assets/docs/reference//344/274/201/346/245/255/347/265/214/345/226/266/350/253/226.md +2637 -2637
- package/lib/assets/docs/reference//347/222/260/345/242/203/345/244/211/346/225/260/347/256/241/347/220/206/343/202/254/343/202/244/343/203/211.md +665 -665
- package/lib/assets/docs/reference//350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +1248 -1248
- package/lib/assets/docs/reference//350/250/200/350/252/236/345/210/245/351/226/213/347/231/272/343/202/254/343/202/244/343/203/211.md +518 -518
- package/lib/assets/docs/reference//351/201/213/345/226/266/347/256/241/347/220/206.md +1482 -1482
- package/lib/assets/docs/reference//351/201/213/347/224/250/343/202/271/343/202/257/343/203/252/343/203/227/343/203/210/344/275/234/346/210/220/343/202/254/343/202/244/343/203/211.md +421 -421
- package/lib/assets/docs/reference//351/201/213/347/224/250/350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +392 -392
- package/lib/assets/docs/reference//351/226/213/347/231/272/343/202/254/343/202/244/343/203/211.md +299 -299
- package/lib/assets/docs/reference//351/235/236/346/251/237/350/203/275/350/246/201/344/273/266/345/256/232/347/276/251/343/202/254/343/202/244/343/203/211.md +1236 -1236
- package/lib/assets/docs/review/index.md +5 -5
- package/lib/assets/docs/strategy/index.md +1 -1
- package/lib/assets/docs/template/ADR.md +30 -30
- package/lib/assets/docs/template/AWS/343/202/271/343/203/206/343/203/274/343/202/270/343/203/263/343/202/260/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +1366 -1366
- package/lib/assets/docs/template/AWS/343/203/227/343/203/255/343/203/200/343/202/257/343/202/267/343/203/247/343/203/263/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +634 -634
- package/lib/assets/docs/template/README.md +50 -50
- package/lib/assets/docs/template/index.md +23 -23
- package/lib/assets/docs/template//343/201/276/343/201/232/343/201/223/343/202/214/343/202/222/350/252/255/343/202/202/343/201/206/343/203/252/343/202/271/343/203/210.md +12 -12
- package/lib/assets/docs/template//343/202/242/343/203/227/343/203/252/343/202/261/343/203/274/343/202/267/343/203/247/343/203/263/351/226/213/347/231/272/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +547 -547
- package/lib/assets/docs/template//343/202/244/343/203/206/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263/345/256/214/344/272/206/345/240/261/345/221/212/346/233/270.md +58 -58
- package/lib/assets/docs/template//343/202/244/343/203/263/343/202/273/343/203/227/343/202/267/343/203/247/343/203/263/343/203/207/343/203/203/343/202/255.md +13 -13
- package/lib/assets/docs/template//343/203/223/343/202/270/343/203/215/343/202/271/343/202/242/343/203/274/343/202/255/343/203/206/343/202/257/343/203/201/343/203/243.md +379 -379
- package/lib/assets/docs/template//344/274/201/346/245/255/345/210/206/346/236/220.md +573 -573
- package/lib/assets/docs/template//345/256/214/345/205/250/345/275/242/345/274/217/343/201/256/343/203/246/343/203/274/343/202/271/343/202/261/343/203/274/343/202/271.md +69 -69
- package/lib/assets/docs/template//350/246/201/344/273/266/345/256/232/347/276/251.md +669 -669
- package/lib/assets/docs/template//350/250/255/350/250/210.md +173 -173
- package/lib/assets/docs/template//351/226/213/347/231/272/347/222/260/345/242/203/343/202/273/343/203/203/343/203/210/343/202/242/343/203/203/343/203/227/346/211/213/351/240/206/346/233/270.md +688 -688
- package/lib/assets/gulpfile.js +25 -25
- package/lib/assets/mkdocs.yml +136 -136
- package/lib/assets/ops/docker/mkdoc/Dockerfile +19 -19
- package/lib/assets/ops/scripts/journal.js +180 -180
- package/lib/assets/ops/scripts/mkdocs.js +82 -82
- package/lib/assets/ops/scripts/release.js +431 -431
- package/lib/assets/ops/scripts/sonar_local.js +726 -726
- package/lib/assets/ops/scripts/ssh.js +190 -190
- package/lib/assets/ops/scripts/vault.js +299 -299
- package/lib/assets/package-lock.json +1653 -1653
- package/lib/assets/package.json +40 -40
- package/lib/gulpfile.js +37 -37
- package/package.json +41 -41
|
@@ -1,2507 +1,2507 @@
|
|
|
1
|
-
# 実践データベース設計:財務会計システム 研究 5 - Axon CQRS/ES の実装
|
|
2
|
-
|
|
3
|
-
## はじめに
|
|
4
|
-
|
|
5
|
-
本研究では、REST API(第21章)、gRPC(研究 3)、GraphQL(研究 4)とは異なるアプローチとして、**CQRS(Command Query Responsibility Segregation)** と **Event Sourcing** による財務会計システムを実装します。Axon Framework を使用し、コマンド(書き込み)とクエリ(読み取り)を分離し、すべての状態変更をイベントとして記録するアーキテクチャを構築します。
|
|
6
|
-
|
|
7
|
-
研究 1 で構築したヘキサゴナルアーキテクチャの考え方を踏襲しつつ、**ドメインモデルをフレームワークから分離**し、Axon Aggregate Adapter を介して連携させます。Read Model の永続化には **MyBatis** を使用します。
|
|
8
|
-
|
|
9
|
-
財務会計システムでは、**仕訳(Journal Entry)** の作成から承認、転記までのワークフローを CQRS/ES で実装し、すべての取引履歴を完全に追跡可能な形で保存します。
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 第26章:Axon CQRS/ES アーキテクチャの基礎
|
|
14
|
-
|
|
15
|
-
### 26.1 CQRS とは
|
|
16
|
-
|
|
17
|
-
CQRS(Command Query Responsibility Segregation)は、コマンド(書き込み)とクエリ(読み取り)の責務を分離するアーキテクチャパターンです。従来の CRUD 操作を単一のモデルで処理するのではなく、書き込み用のモデルと読み取り用のモデルを分けることで、それぞれに最適化された設計が可能になります。
|
|
18
|
-
|
|
19
|
-
```plantuml
|
|
20
|
-
@startuml cqrs_architecture
|
|
21
|
-
skinparam componentStyle rectangle
|
|
22
|
-
skinparam backgroundColor #FEFEFE
|
|
23
|
-
|
|
24
|
-
title CQRS アーキテクチャ(財務会計システム)
|
|
25
|
-
|
|
26
|
-
package "Command Side (Write)" {
|
|
27
|
-
[REST Controller] --> [CommandGateway]
|
|
28
|
-
[CommandGateway] --> [Aggregate]
|
|
29
|
-
[Aggregate] --> [Event Store]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
package "Query Side (Read)" {
|
|
33
|
-
[Event Store] --> [Projection]
|
|
34
|
-
[Projection] --> [Read Model DB]
|
|
35
|
-
[REST Controller] --> [MyBatis Mapper]
|
|
36
|
-
[MyBatis Mapper] --> [Read Model DB]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@enduml
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**CQRS の利点:**
|
|
43
|
-
|
|
44
|
-
| 観点 | 説明 |
|
|
45
|
-
|------|------|
|
|
46
|
-
| **スケーラビリティ** | 読み取りと書き込みを独立してスケール可能 |
|
|
47
|
-
| **パフォーマンス** | 読み取りに最適化されたモデルで高速クエリ |
|
|
48
|
-
| **複雑性の分離** | 書き込みロジックと読み取りロジックを独立して開発 |
|
|
49
|
-
| **監査対応** | Event Sourcing と組み合わせて完全な履歴を保持 |
|
|
50
|
-
|
|
51
|
-
**財務会計システムでの CQRS の意義:**
|
|
52
|
-
|
|
53
|
-
- **監査証跡**: すべての仕訳変更履歴を完全に保持
|
|
54
|
-
- **複雑なクエリ**: 試算表、財務諸表など多様なビューを効率的に生成
|
|
55
|
-
- **データ整合性**: イベントベースで一貫性を保証
|
|
56
|
-
|
|
57
|
-
---
|
|
58
|
-
|
|
59
|
-
### 26.2 Event Sourcing とは
|
|
60
|
-
|
|
61
|
-
Event Sourcing は、アプリケーションの状態をイベントの連続として保存するパターンです。現在の状態を直接保存するのではなく、状態変更を表すイベントを記録し、必要に応じてイベントを再生して現在の状態を再構築します。
|
|
62
|
-
|
|
63
|
-
```plantuml
|
|
64
|
-
@startuml event_sourcing
|
|
65
|
-
skinparam componentStyle rectangle
|
|
66
|
-
skinparam backgroundColor #FEFEFE
|
|
67
|
-
|
|
68
|
-
[Command] --> [Aggregate] : 1. コマンド受信
|
|
69
|
-
[Aggregate] --> [Domain Model] : 2. ビジネスロジック実行
|
|
70
|
-
[Domain Model] --> [Event] : 3. イベント生成
|
|
71
|
-
[Event] --> [Event Store] : 4. イベント永続化
|
|
72
|
-
[Event Store] --> [Aggregate] : 5. イベント再生(状態復元)
|
|
73
|
-
|
|
74
|
-
@enduml
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
**Event Sourcing の利点:**
|
|
78
|
-
|
|
79
|
-
| 観点 | 説明 |
|
|
80
|
-
|------|------|
|
|
81
|
-
| **完全な履歴** | すべての状態変更が記録される |
|
|
82
|
-
| **監査証跡** | いつ、誰が、何を変更したかが明確 |
|
|
83
|
-
| **時間旅行** | 過去の任意の時点の状態を再構築可能 |
|
|
84
|
-
| **イベント駆動** | 他システムとの連携が容易 |
|
|
85
|
-
| **デバッグ** | 問題発生時にイベントを追跡可能 |
|
|
86
|
-
|
|
87
|
-
**財務会計システムでの Event Sourcing の意義:**
|
|
88
|
-
|
|
89
|
-
- **訂正仕訳の追跡**: 修正履歴を完全に保持
|
|
90
|
-
- **期末決算処理**: 任意の時点での残高を再計算可能
|
|
91
|
-
- **内部統制**: SOX 法対応の監査証跡を自動生成
|
|
92
|
-
|
|
93
|
-
---
|
|
94
|
-
|
|
95
|
-
### 26.3 ヘキサゴナルアーキテクチャとの統合
|
|
96
|
-
|
|
97
|
-
本実装では、ヘキサゴナルアーキテクチャ(Ports & Adapters)を採用し、ビジネスロジックを外部依存から分離します。
|
|
98
|
-
|
|
99
|
-
```plantuml
|
|
100
|
-
@startuml hexagonal_cqrs
|
|
101
|
-
!define RECTANGLE class
|
|
102
|
-
skinparam backgroundColor #FEFEFE
|
|
103
|
-
|
|
104
|
-
package "Hexagonal Architecture (CQRS/ES 版)" {
|
|
105
|
-
|
|
106
|
-
package "Inbound Adapters" {
|
|
107
|
-
[REST Controller]
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
package "Application Core" {
|
|
111
|
-
package "Application Layer" {
|
|
112
|
-
[Aggregate Adapter]
|
|
113
|
-
[Projection]
|
|
114
|
-
[Policy Handler]
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
package "Domain Layer" {
|
|
118
|
-
[Domain Model]
|
|
119
|
-
[Commands]
|
|
120
|
-
[Value Objects]
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
package "API Layer" {
|
|
124
|
-
[Events]
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
package "Outbound Adapters" {
|
|
129
|
-
[MyBatis Mapper]
|
|
130
|
-
[Event Store]
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
[REST Controller] --> [Aggregate Adapter]
|
|
135
|
-
[Aggregate Adapter] --> [Domain Model]
|
|
136
|
-
[Projection] --> [MyBatis Mapper]
|
|
137
|
-
|
|
138
|
-
note top of [Domain Model]
|
|
139
|
-
純粋なドメインモデル
|
|
140
|
-
Axon Framework に依存しない
|
|
141
|
-
ビジネスロジックのみ
|
|
142
|
-
end note
|
|
143
|
-
|
|
144
|
-
note left of [Aggregate Adapter]
|
|
145
|
-
Axon 用アダプター
|
|
146
|
-
@Aggregate, @CommandHandler
|
|
147
|
-
フレームワーク依存を吸収
|
|
148
|
-
end note
|
|
149
|
-
|
|
150
|
-
@enduml
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**設計原則:**
|
|
154
|
-
|
|
155
|
-
1. **ドメインモデルの純粋性**: ドメインモデルは Axon に依存しない純粋な Java コード
|
|
156
|
-
2. **Aggregate Adapter**: Axon Framework 用のアダプターを Application Layer に配置
|
|
157
|
-
3. **イベントは公開 API**: イベントは他の Context から参照される公開 API として定義
|
|
158
|
-
|
|
159
|
-
---
|
|
160
|
-
|
|
161
|
-
### 26.4 技術スタック
|
|
162
|
-
|
|
163
|
-
| カテゴリ | 技術 | バージョン |
|
|
164
|
-
|---------|------|-----------|
|
|
165
|
-
| 言語 | Java | 21 |
|
|
166
|
-
| フレームワーク | Spring Boot | 3.4.1 |
|
|
167
|
-
| CQRS/ES | Axon Framework | 4.10.3 |
|
|
168
|
-
| ORM | MyBatis | 3.0.4 |
|
|
169
|
-
| API ドキュメント | SpringDoc OpenAPI | 2.7.0 |
|
|
170
|
-
| データベース | H2 (開発) / PostgreSQL (本番) | - |
|
|
171
|
-
|
|
172
|
-
#### build.gradle.kts
|
|
173
|
-
|
|
174
|
-
<details>
|
|
175
|
-
<summary>コード例: build.gradle.kts</summary>
|
|
176
|
-
|
|
177
|
-
```kotlin
|
|
178
|
-
dependencies {
|
|
179
|
-
// Spring Boot
|
|
180
|
-
implementation("org.springframework.boot:spring-boot-starter-web")
|
|
181
|
-
implementation("org.springframework.boot:spring-boot-starter-validation")
|
|
182
|
-
|
|
183
|
-
// Axon Framework
|
|
184
|
-
implementation("org.axonframework:axon-spring-boot-starter:4.10.3") {
|
|
185
|
-
exclude(group = "org.axonframework", module = "axon-server-connector")
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// MyBatis
|
|
189
|
-
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4")
|
|
190
|
-
|
|
191
|
-
// OpenAPI
|
|
192
|
-
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
|
|
193
|
-
|
|
194
|
-
// Database
|
|
195
|
-
runtimeOnly("com.h2database:h2")
|
|
196
|
-
runtimeOnly("org.postgresql:postgresql")
|
|
197
|
-
|
|
198
|
-
// Test
|
|
199
|
-
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
200
|
-
testImplementation("org.axonframework:axon-test:4.10.3")
|
|
201
|
-
testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4")
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
</details>
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
### 26.5 ディレクトリ構造
|
|
210
|
-
|
|
211
|
-
<details>
|
|
212
|
-
<summary>コード例: ディレクトリ構成</summary>
|
|
213
|
-
|
|
214
|
-
```
|
|
215
|
-
src/main/java/com/example/accounting/
|
|
216
|
-
├── app/ # アプリケーション共通
|
|
217
|
-
│ └── RootController.java
|
|
218
|
-
├── config/ # 設定クラス
|
|
219
|
-
│ ├── AxonConfig.java
|
|
220
|
-
│ ├── MyBatisConfig.java
|
|
221
|
-
│ └── OpenApiConfig.java
|
|
222
|
-
├── journal/ # Journal Bounded Context(仕訳)
|
|
223
|
-
│ ├── api/
|
|
224
|
-
│ │ └── events/ # 公開イベント API
|
|
225
|
-
│ │ ├── JournalEntryEvent.java # sealed interface
|
|
226
|
-
│ │ ├── JournalEntryCreatedEvent.java
|
|
227
|
-
│ │ ├── JournalEntryApprovedEvent.java
|
|
228
|
-
│ │ └── JournalEntryPostedEvent.java
|
|
229
|
-
│ ├── adapter/
|
|
230
|
-
│ │ ├── inbound/rest/journals/ # Inbound Adapter (REST)
|
|
231
|
-
│ │ │ ├── JournalsController.java
|
|
232
|
-
│ │ │ └── protocol/ # Request/Response DTO
|
|
233
|
-
│ │ └── outbound/persistence/ # Outbound Adapter (MyBatis)
|
|
234
|
-
│ │ ├── entity/
|
|
235
|
-
│ │ │ └── JournalEntryEntity.java
|
|
236
|
-
│ │ └── mapper/
|
|
237
|
-
│ │ └── JournalEntryMapper.java
|
|
238
|
-
│ ├── application/
|
|
239
|
-
│ │ ├── aggregate/ # Axon Aggregate Adapter
|
|
240
|
-
│ │ │ └── JournalEntryAggregateAdapter.java
|
|
241
|
-
│ │ ├── policy/ # イベントハンドラー(Choreography)
|
|
242
|
-
│ │ │ └── AccountEventHandler.java
|
|
243
|
-
│ │ └── query/ # Projection
|
|
244
|
-
│ │ └── JournalEntryProjection.java
|
|
245
|
-
│ └── domain/
|
|
246
|
-
│ └── model/aggregate/journal/ # 純粋なドメインモデル
|
|
247
|
-
│ ├── JournalEntry.java
|
|
248
|
-
│ ├── JournalEntryCommands.java
|
|
249
|
-
│ ├── JournalEntryStatus.java
|
|
250
|
-
│ └── JournalLine.java
|
|
251
|
-
└── account/ # Account Bounded Context(勘定科目)
|
|
252
|
-
├── api/events/
|
|
253
|
-
│ ├── AccountEvent.java
|
|
254
|
-
│ ├── AccountBalanceUpdatedEvent.java
|
|
255
|
-
│ └── AccountClosedEvent.java
|
|
256
|
-
├── application/
|
|
257
|
-
│ ├── aggregate/
|
|
258
|
-
│ │ └── AccountAggregateAdapter.java
|
|
259
|
-
│ └── policy/
|
|
260
|
-
│ └── JournalEventHandler.java
|
|
261
|
-
└── domain/model/aggregate/account/
|
|
262
|
-
├── Account.java
|
|
263
|
-
└── AccountCommands.java
|
|
264
|
-
|
|
265
|
-
src/main/resources/
|
|
266
|
-
├── application.yml
|
|
267
|
-
├── schema.sql # Read Model スキーマ
|
|
268
|
-
└── mapper/
|
|
269
|
-
└── JournalEntryMapper.xml # MyBatis マッパー XML
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
</details>
|
|
273
|
-
|
|
274
|
-
---
|
|
275
|
-
|
|
276
|
-
### 26.6 Axon 設定クラス
|
|
277
|
-
|
|
278
|
-
<details>
|
|
279
|
-
<summary>コード例: AxonConfig.java</summary>
|
|
280
|
-
|
|
281
|
-
```java
|
|
282
|
-
package com.example.accounting.config;
|
|
283
|
-
|
|
284
|
-
import org.axonframework.eventsourcing.eventstore.EmbeddedEventStore;
|
|
285
|
-
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
|
|
286
|
-
import org.axonframework.eventsourcing.eventstore.EventStore;
|
|
287
|
-
import org.axonframework.eventsourcing.eventstore.jdbc.JdbcEventStorageEngine;
|
|
288
|
-
import org.axonframework.serialization.Serializer;
|
|
289
|
-
import org.axonframework.serialization.json.JacksonSerializer;
|
|
290
|
-
import org.springframework.context.annotation.Bean;
|
|
291
|
-
import org.springframework.context.annotation.Configuration;
|
|
292
|
-
|
|
293
|
-
import javax.sql.DataSource;
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Axon Framework 設定
|
|
297
|
-
*/
|
|
298
|
-
@Configuration
|
|
299
|
-
public class AxonConfig {
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* JDBC ベースの Event Storage Engine
|
|
303
|
-
*/
|
|
304
|
-
@Bean
|
|
305
|
-
public EventStorageEngine eventStorageEngine(
|
|
306
|
-
DataSource dataSource,
|
|
307
|
-
Serializer serializer) {
|
|
308
|
-
return JdbcEventStorageEngine.builder()
|
|
309
|
-
.snapshotSerializer(serializer)
|
|
310
|
-
.eventSerializer(serializer)
|
|
311
|
-
.dataSource(dataSource)
|
|
312
|
-
.build();
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Event Store
|
|
317
|
-
*/
|
|
318
|
-
@Bean
|
|
319
|
-
public EventStore eventStore(EventStorageEngine eventStorageEngine) {
|
|
320
|
-
return EmbeddedEventStore.builder()
|
|
321
|
-
.storageEngine(eventStorageEngine)
|
|
322
|
-
.build();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* JSON シリアライザー
|
|
327
|
-
*/
|
|
328
|
-
@Bean
|
|
329
|
-
public Serializer eventSerializer() {
|
|
330
|
-
return JacksonSerializer.defaultSerializer();
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
</details>
|
|
336
|
-
|
|
337
|
-
### 26.7 MyBatis 設定クラス
|
|
338
|
-
|
|
339
|
-
<details>
|
|
340
|
-
<summary>コード例: MyBatisConfig.java</summary>
|
|
341
|
-
|
|
342
|
-
```java
|
|
343
|
-
package com.example.accounting.config;
|
|
344
|
-
|
|
345
|
-
import org.apache.ibatis.session.SqlSessionFactory;
|
|
346
|
-
import org.mybatis.spring.SqlSessionFactoryBean;
|
|
347
|
-
import org.mybatis.spring.annotation.MapperScan;
|
|
348
|
-
import org.springframework.context.annotation.Bean;
|
|
349
|
-
import org.springframework.context.annotation.Configuration;
|
|
350
|
-
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
|
351
|
-
|
|
352
|
-
import javax.sql.DataSource;
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* MyBatis 設定
|
|
356
|
-
*/
|
|
357
|
-
@Configuration
|
|
358
|
-
@MapperScan(basePackages = {
|
|
359
|
-
"com.example.accounting.journal.adapter.outbound.persistence.mapper",
|
|
360
|
-
"com.example.accounting.account.adapter.outbound.persistence.mapper"
|
|
361
|
-
})
|
|
362
|
-
public class MyBatisConfig {
|
|
363
|
-
|
|
364
|
-
@Bean
|
|
365
|
-
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
|
366
|
-
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
|
|
367
|
-
factoryBean.setDataSource(dataSource);
|
|
368
|
-
factoryBean.setMapperLocations(
|
|
369
|
-
new PathMatchingResourcePatternResolver()
|
|
370
|
-
.getResources("classpath:mapper/**/*.xml")
|
|
371
|
-
);
|
|
372
|
-
factoryBean.setTypeAliasesPackage(
|
|
373
|
-
"com.example.accounting.journal.adapter.outbound.persistence.entity," +
|
|
374
|
-
"com.example.accounting.account.adapter.outbound.persistence.entity"
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
org.apache.ibatis.session.Configuration configuration =
|
|
378
|
-
new org.apache.ibatis.session.Configuration();
|
|
379
|
-
configuration.setMapUnderscoreToCamelCase(true);
|
|
380
|
-
factoryBean.setConfiguration(configuration);
|
|
381
|
-
|
|
382
|
-
return factoryBean.getObject();
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
</details>
|
|
388
|
-
|
|
389
|
-
---
|
|
390
|
-
|
|
391
|
-
## 第27章:ドメインモデルとイベント設計
|
|
392
|
-
|
|
393
|
-
### 27.1 Journal Bounded Context
|
|
394
|
-
|
|
395
|
-
仕訳の作成から転記完了までのライフサイクルを管理します。
|
|
396
|
-
|
|
397
|
-
#### 状態遷移図
|
|
398
|
-
|
|
399
|
-
```plantuml
|
|
400
|
-
@startuml journal_state
|
|
401
|
-
skinparam backgroundColor #FEFEFE
|
|
402
|
-
|
|
403
|
-
title 仕訳(Journal Entry)の状態遷移
|
|
404
|
-
|
|
405
|
-
[*] --> DRAFT : JournalEntryCreated
|
|
406
|
-
DRAFT --> PENDING_APPROVAL : JournalEntrySubmitted
|
|
407
|
-
DRAFT --> CANCELLED : JournalEntryCancelled
|
|
408
|
-
PENDING_APPROVAL --> APPROVED : JournalEntryApproved
|
|
409
|
-
PENDING_APPROVAL --> REJECTED : JournalEntryRejected
|
|
410
|
-
PENDING_APPROVAL --> CANCELLED : JournalEntryCancelled
|
|
411
|
-
APPROVED --> POSTED : JournalEntryPosted
|
|
412
|
-
REJECTED --> DRAFT : JournalEntryRevised
|
|
413
|
-
|
|
414
|
-
@enduml
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
#### イベント一覧
|
|
418
|
-
|
|
419
|
-
| イベント | 説明 |
|
|
420
|
-
|---------|------|
|
|
421
|
-
| `JournalEntryCreatedEvent` | 仕訳が作成された |
|
|
422
|
-
| `JournalEntrySubmittedEvent` | 仕訳が承認申請された |
|
|
423
|
-
| `JournalEntryApprovedEvent` | 仕訳が承認された |
|
|
424
|
-
| `JournalEntryRejectedEvent` | 仕訳が却下された |
|
|
425
|
-
| `JournalEntryRevisedEvent` | 却下された仕訳が修正された |
|
|
426
|
-
| `JournalEntryPostedEvent` | 仕訳が転記された(元帳に反映) |
|
|
427
|
-
| `JournalEntryCancelledEvent` | 仕訳がキャンセルされた |
|
|
428
|
-
|
|
429
|
-
---
|
|
430
|
-
|
|
431
|
-
### 27.2 イベント定義(API Layer)
|
|
432
|
-
|
|
433
|
-
#### sealed interface によるイベントの型安全性
|
|
434
|
-
|
|
435
|
-
<details>
|
|
436
|
-
<summary>コード例: JournalEntryEvent.java</summary>
|
|
437
|
-
|
|
438
|
-
```java
|
|
439
|
-
package com.example.accounting.journal.api.events;
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* JournalEntry イベントの sealed interface
|
|
443
|
-
* すべての JournalEntry イベントの親インターフェース
|
|
444
|
-
*/
|
|
445
|
-
public sealed interface JournalEntryEvent permits
|
|
446
|
-
JournalEntryCreatedEvent,
|
|
447
|
-
JournalEntrySubmittedEvent,
|
|
448
|
-
JournalEntryApprovedEvent,
|
|
449
|
-
JournalEntryRejectedEvent,
|
|
450
|
-
JournalEntryRevisedEvent,
|
|
451
|
-
JournalEntryPostedEvent,
|
|
452
|
-
JournalEntryCancelledEvent {
|
|
453
|
-
|
|
454
|
-
String journalEntryId();
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
</details>
|
|
459
|
-
|
|
460
|
-
**sealed interface の利点:**
|
|
461
|
-
|
|
462
|
-
- **網羅性チェック**: switch 式でコンパイル時に全ケースをチェック
|
|
463
|
-
- **型安全性**: 許可されたクラスのみが実装可能
|
|
464
|
-
- **ドキュメント**: 存在するイベントが一目でわかる
|
|
465
|
-
|
|
466
|
-
#### イベント record の実装
|
|
467
|
-
|
|
468
|
-
<details>
|
|
469
|
-
<summary>コード例: JournalEntryCreatedEvent.java</summary>
|
|
470
|
-
|
|
471
|
-
```java
|
|
472
|
-
package com.example.accounting.journal.api.events;
|
|
473
|
-
|
|
474
|
-
import java.math.BigDecimal;
|
|
475
|
-
import java.time.LocalDate;
|
|
476
|
-
import java.time.LocalDateTime;
|
|
477
|
-
import java.util.List;
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* 仕訳作成イベント
|
|
481
|
-
*/
|
|
482
|
-
public record JournalEntryCreatedEvent(
|
|
483
|
-
String journalEntryId,
|
|
484
|
-
String journalNumber,
|
|
485
|
-
LocalDate transactionDate,
|
|
486
|
-
String description,
|
|
487
|
-
List<JournalLineData> lines,
|
|
488
|
-
String createdBy,
|
|
489
|
-
LocalDateTime createdAt
|
|
490
|
-
) implements JournalEntryEvent {
|
|
491
|
-
|
|
492
|
-
/**
|
|
493
|
-
* 仕訳明細データ
|
|
494
|
-
*/
|
|
495
|
-
public record JournalLineData(
|
|
496
|
-
String accountCode,
|
|
497
|
-
String accountName,
|
|
498
|
-
BigDecimal debitAmount,
|
|
499
|
-
BigDecimal creditAmount
|
|
500
|
-
) {
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
</details>
|
|
506
|
-
|
|
507
|
-
<details>
|
|
508
|
-
<summary>コード例: JournalEntryApprovedEvent.java</summary>
|
|
509
|
-
|
|
510
|
-
```java
|
|
511
|
-
package com.example.accounting.journal.api.events;
|
|
512
|
-
|
|
513
|
-
import java.time.LocalDateTime;
|
|
514
|
-
|
|
515
|
-
/**
|
|
516
|
-
* 仕訳承認イベント
|
|
517
|
-
*/
|
|
518
|
-
public record JournalEntryApprovedEvent(
|
|
519
|
-
String journalEntryId,
|
|
520
|
-
String approvedBy,
|
|
521
|
-
String comment,
|
|
522
|
-
LocalDateTime approvedAt
|
|
523
|
-
) implements JournalEntryEvent {
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
</details>
|
|
528
|
-
|
|
529
|
-
<details>
|
|
530
|
-
<summary>コード例: JournalEntryPostedEvent.java</summary>
|
|
531
|
-
|
|
532
|
-
```java
|
|
533
|
-
package com.example.accounting.journal.api.events;
|
|
534
|
-
|
|
535
|
-
import java.time.LocalDateTime;
|
|
536
|
-
|
|
537
|
-
/**
|
|
538
|
-
* 仕訳転記イベント
|
|
539
|
-
*/
|
|
540
|
-
public record JournalEntryPostedEvent(
|
|
541
|
-
String journalEntryId,
|
|
542
|
-
String fiscalPeriod,
|
|
543
|
-
String postedBy,
|
|
544
|
-
LocalDateTime postedAt
|
|
545
|
-
) implements JournalEntryEvent {
|
|
546
|
-
}
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
</details>
|
|
550
|
-
|
|
551
|
-
<details>
|
|
552
|
-
<summary>コード例: JournalEntryCancelledEvent.java</summary>
|
|
553
|
-
|
|
554
|
-
```java
|
|
555
|
-
package com.example.accounting.journal.api.events;
|
|
556
|
-
|
|
557
|
-
import java.time.LocalDateTime;
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* 仕訳キャンセルイベント
|
|
561
|
-
*/
|
|
562
|
-
public record JournalEntryCancelledEvent(
|
|
563
|
-
String journalEntryId,
|
|
564
|
-
String cancelledBy,
|
|
565
|
-
String reason,
|
|
566
|
-
LocalDateTime cancelledAt
|
|
567
|
-
) implements JournalEntryEvent {
|
|
568
|
-
}
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
</details>
|
|
572
|
-
|
|
573
|
-
---
|
|
574
|
-
|
|
575
|
-
### 27.3 コマンド定義(Domain Layer)
|
|
576
|
-
|
|
577
|
-
<details>
|
|
578
|
-
<summary>コード例: JournalEntryCommands.java</summary>
|
|
579
|
-
|
|
580
|
-
```java
|
|
581
|
-
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
582
|
-
|
|
583
|
-
import org.axonframework.modelling.command.TargetAggregateIdentifier;
|
|
584
|
-
import java.math.BigDecimal;
|
|
585
|
-
import java.time.LocalDate;
|
|
586
|
-
import java.util.List;
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* JournalEntry 集約へのコマンド定義
|
|
590
|
-
*/
|
|
591
|
-
public final class JournalEntryCommands {
|
|
592
|
-
|
|
593
|
-
private JournalEntryCommands() {
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* 仕訳作成コマンド
|
|
598
|
-
*/
|
|
599
|
-
public record CreateJournalEntryCommand(
|
|
600
|
-
@TargetAggregateIdentifier
|
|
601
|
-
String journalEntryId,
|
|
602
|
-
LocalDate transactionDate,
|
|
603
|
-
String description,
|
|
604
|
-
List<JournalLineInput> lines,
|
|
605
|
-
String createdBy
|
|
606
|
-
) {
|
|
607
|
-
public record JournalLineInput(
|
|
608
|
-
String accountCode,
|
|
609
|
-
String accountName,
|
|
610
|
-
BigDecimal debitAmount,
|
|
611
|
-
BigDecimal creditAmount
|
|
612
|
-
) {
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* 仕訳承認申請コマンド
|
|
618
|
-
*/
|
|
619
|
-
public record SubmitJournalEntryCommand(
|
|
620
|
-
@TargetAggregateIdentifier
|
|
621
|
-
String journalEntryId,
|
|
622
|
-
String submittedBy
|
|
623
|
-
) {
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* 仕訳承認コマンド
|
|
628
|
-
*/
|
|
629
|
-
public record ApproveJournalEntryCommand(
|
|
630
|
-
@TargetAggregateIdentifier
|
|
631
|
-
String journalEntryId,
|
|
632
|
-
String approvedBy,
|
|
633
|
-
String comment
|
|
634
|
-
) {
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* 仕訳却下コマンド
|
|
639
|
-
*/
|
|
640
|
-
public record RejectJournalEntryCommand(
|
|
641
|
-
@TargetAggregateIdentifier
|
|
642
|
-
String journalEntryId,
|
|
643
|
-
String rejectedBy,
|
|
644
|
-
String reason
|
|
645
|
-
) {
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
/**
|
|
649
|
-
* 仕訳修正コマンド
|
|
650
|
-
*/
|
|
651
|
-
public record ReviseJournalEntryCommand(
|
|
652
|
-
@TargetAggregateIdentifier
|
|
653
|
-
String journalEntryId,
|
|
654
|
-
LocalDate transactionDate,
|
|
655
|
-
String description,
|
|
656
|
-
List<CreateJournalEntryCommand.JournalLineInput> lines,
|
|
657
|
-
String revisedBy
|
|
658
|
-
) {
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
/**
|
|
662
|
-
* 仕訳転記コマンド
|
|
663
|
-
*/
|
|
664
|
-
public record PostJournalEntryCommand(
|
|
665
|
-
@TargetAggregateIdentifier
|
|
666
|
-
String journalEntryId,
|
|
667
|
-
String fiscalPeriod,
|
|
668
|
-
String postedBy
|
|
669
|
-
) {
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
/**
|
|
673
|
-
* 仕訳キャンセルコマンド
|
|
674
|
-
*/
|
|
675
|
-
public record CancelJournalEntryCommand(
|
|
676
|
-
@TargetAggregateIdentifier
|
|
677
|
-
String journalEntryId,
|
|
678
|
-
String cancelledBy,
|
|
679
|
-
String reason
|
|
680
|
-
) {
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
```
|
|
684
|
-
|
|
685
|
-
</details>
|
|
686
|
-
|
|
687
|
-
**@TargetAggregateIdentifier の役割:**
|
|
688
|
-
|
|
689
|
-
- Axon がコマンドをどの集約インスタンスにルーティングするかを決定
|
|
690
|
-
- 集約の一意識別子となるフィールドに付与
|
|
691
|
-
|
|
692
|
-
---
|
|
693
|
-
|
|
694
|
-
### 27.4 ドメインモデル(純粋な Java)
|
|
695
|
-
|
|
696
|
-
<details>
|
|
697
|
-
<summary>コード例: JournalEntry.java</summary>
|
|
698
|
-
|
|
699
|
-
```java
|
|
700
|
-
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
701
|
-
|
|
702
|
-
import com.example.accounting.journal.api.events.*;
|
|
703
|
-
import com.example.accounting.journal.api.events.JournalEntryCreatedEvent.JournalLineData;
|
|
704
|
-
import java.math.BigDecimal;
|
|
705
|
-
import java.time.LocalDate;
|
|
706
|
-
import java.time.LocalDateTime;
|
|
707
|
-
import java.util.List;
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* JournalEntry ドメインモデル(Axon 非依存)
|
|
711
|
-
* 純粋なビジネスロジックのみを含む
|
|
712
|
-
*/
|
|
713
|
-
public record JournalEntry(
|
|
714
|
-
String journalEntryId,
|
|
715
|
-
String journalNumber,
|
|
716
|
-
LocalDate transactionDate,
|
|
717
|
-
String description,
|
|
718
|
-
List<JournalLine> lines,
|
|
719
|
-
JournalEntryStatus status
|
|
720
|
-
) {
|
|
721
|
-
|
|
722
|
-
// ======== ファクトリメソッド ========
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* 仕訳作成
|
|
726
|
-
*/
|
|
727
|
-
public static JournalEntryCreatedEvent create(
|
|
728
|
-
String journalEntryId,
|
|
729
|
-
String journalNumber,
|
|
730
|
-
LocalDate transactionDate,
|
|
731
|
-
String description,
|
|
732
|
-
List<JournalLineData> lines,
|
|
733
|
-
String createdBy
|
|
734
|
-
) {
|
|
735
|
-
// バリデーション: 借方・貸方の合計が一致すること
|
|
736
|
-
BigDecimal totalDebit = lines.stream()
|
|
737
|
-
.map(JournalLineData::debitAmount)
|
|
738
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
739
|
-
BigDecimal totalCredit = lines.stream()
|
|
740
|
-
.map(JournalLineData::creditAmount)
|
|
741
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
742
|
-
|
|
743
|
-
if (totalDebit.compareTo(totalCredit) != 0) {
|
|
744
|
-
throw new IllegalArgumentException(
|
|
745
|
-
"Debit and credit amounts must be equal. Debit: " + totalDebit + ", Credit: " + totalCredit
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (lines.isEmpty()) {
|
|
750
|
-
throw new IllegalArgumentException("Journal entry must have at least one line");
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
return new JournalEntryCreatedEvent(
|
|
754
|
-
journalEntryId,
|
|
755
|
-
journalNumber,
|
|
756
|
-
transactionDate,
|
|
757
|
-
description,
|
|
758
|
-
lines,
|
|
759
|
-
createdBy,
|
|
760
|
-
LocalDateTime.now()
|
|
761
|
-
);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
/**
|
|
765
|
-
* イベントからの再構築
|
|
766
|
-
*/
|
|
767
|
-
public static JournalEntry from(JournalEntryCreatedEvent event) {
|
|
768
|
-
List<JournalLine> lines = event.lines().stream()
|
|
769
|
-
.map(data -> new JournalLine(
|
|
770
|
-
data.accountCode(),
|
|
771
|
-
data.accountName(),
|
|
772
|
-
data.debitAmount(),
|
|
773
|
-
data.creditAmount()
|
|
774
|
-
))
|
|
775
|
-
.toList();
|
|
776
|
-
|
|
777
|
-
return new JournalEntry(
|
|
778
|
-
event.journalEntryId(),
|
|
779
|
-
event.journalNumber(),
|
|
780
|
-
event.transactionDate(),
|
|
781
|
-
event.description(),
|
|
782
|
-
lines,
|
|
783
|
-
JournalEntryStatus.DRAFT
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
// ======== 状態遷移メソッド ========
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* 承認申請
|
|
791
|
-
*/
|
|
792
|
-
public JournalEntrySubmittedEvent submit(String submittedBy) {
|
|
793
|
-
if (status != JournalEntryStatus.DRAFT) {
|
|
794
|
-
throw new IllegalStateException("Only draft journal entries can be submitted");
|
|
795
|
-
}
|
|
796
|
-
return new JournalEntrySubmittedEvent(journalEntryId, submittedBy, LocalDateTime.now());
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* 承認
|
|
801
|
-
*/
|
|
802
|
-
public JournalEntryApprovedEvent approve(String approvedBy, String comment) {
|
|
803
|
-
if (status != JournalEntryStatus.PENDING_APPROVAL) {
|
|
804
|
-
throw new IllegalStateException("Only pending journal entries can be approved");
|
|
805
|
-
}
|
|
806
|
-
return new JournalEntryApprovedEvent(journalEntryId, approvedBy, comment, LocalDateTime.now());
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
/**
|
|
810
|
-
* 却下
|
|
811
|
-
*/
|
|
812
|
-
public JournalEntryRejectedEvent reject(String rejectedBy, String reason) {
|
|
813
|
-
if (status != JournalEntryStatus.PENDING_APPROVAL) {
|
|
814
|
-
throw new IllegalStateException("Only pending journal entries can be rejected");
|
|
815
|
-
}
|
|
816
|
-
return new JournalEntryRejectedEvent(journalEntryId, rejectedBy, reason, LocalDateTime.now());
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
/**
|
|
820
|
-
* 修正
|
|
821
|
-
*/
|
|
822
|
-
public JournalEntryRevisedEvent revise(
|
|
823
|
-
LocalDate transactionDate,
|
|
824
|
-
String description,
|
|
825
|
-
List<JournalLineData> lines,
|
|
826
|
-
String revisedBy
|
|
827
|
-
) {
|
|
828
|
-
if (status != JournalEntryStatus.REJECTED) {
|
|
829
|
-
throw new IllegalStateException("Only rejected journal entries can be revised");
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// バリデーション
|
|
833
|
-
BigDecimal totalDebit = lines.stream()
|
|
834
|
-
.map(JournalLineData::debitAmount)
|
|
835
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
836
|
-
BigDecimal totalCredit = lines.stream()
|
|
837
|
-
.map(JournalLineData::creditAmount)
|
|
838
|
-
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
839
|
-
|
|
840
|
-
if (totalDebit.compareTo(totalCredit) != 0) {
|
|
841
|
-
throw new IllegalArgumentException("Debit and credit amounts must be equal");
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
return new JournalEntryRevisedEvent(
|
|
845
|
-
journalEntryId,
|
|
846
|
-
transactionDate,
|
|
847
|
-
description,
|
|
848
|
-
lines,
|
|
849
|
-
revisedBy,
|
|
850
|
-
LocalDateTime.now()
|
|
851
|
-
);
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* 転記
|
|
856
|
-
*/
|
|
857
|
-
public JournalEntryPostedEvent post(String fiscalPeriod, String postedBy) {
|
|
858
|
-
if (status != JournalEntryStatus.APPROVED) {
|
|
859
|
-
throw new IllegalStateException("Only approved journal entries can be posted");
|
|
860
|
-
}
|
|
861
|
-
return new JournalEntryPostedEvent(journalEntryId, fiscalPeriod, postedBy, LocalDateTime.now());
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* キャンセル
|
|
866
|
-
*/
|
|
867
|
-
public JournalEntryCancelledEvent cancel(String cancelledBy, String reason) {
|
|
868
|
-
if (status == JournalEntryStatus.POSTED) {
|
|
869
|
-
throw new IllegalStateException("Posted journal entries cannot be cancelled");
|
|
870
|
-
}
|
|
871
|
-
if (status == JournalEntryStatus.CANCELLED) {
|
|
872
|
-
throw new IllegalStateException("Journal entry is already cancelled");
|
|
873
|
-
}
|
|
874
|
-
return new JournalEntryCancelledEvent(journalEntryId, cancelledBy, reason, LocalDateTime.now());
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// ======== イベント適用メソッド ========
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* イベントを適用して新しい状態を生成
|
|
881
|
-
*/
|
|
882
|
-
public JournalEntry apply(JournalEntryEvent event) {
|
|
883
|
-
return switch (event) {
|
|
884
|
-
case JournalEntryCreatedEvent e -> from(e);
|
|
885
|
-
case JournalEntrySubmittedEvent e -> withStatus(JournalEntryStatus.PENDING_APPROVAL);
|
|
886
|
-
case JournalEntryApprovedEvent e -> withStatus(JournalEntryStatus.APPROVED);
|
|
887
|
-
case JournalEntryRejectedEvent e -> withStatus(JournalEntryStatus.REJECTED);
|
|
888
|
-
case JournalEntryRevisedEvent e -> revised(e);
|
|
889
|
-
case JournalEntryPostedEvent e -> withStatus(JournalEntryStatus.POSTED);
|
|
890
|
-
case JournalEntryCancelledEvent e -> withStatus(JournalEntryStatus.CANCELLED);
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
/**
|
|
895
|
-
* 状態更新ヘルパー
|
|
896
|
-
*/
|
|
897
|
-
private JournalEntry withStatus(JournalEntryStatus newStatus) {
|
|
898
|
-
return new JournalEntry(journalEntryId, journalNumber, transactionDate, description, lines, newStatus);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
/**
|
|
902
|
-
* 修正適用
|
|
903
|
-
*/
|
|
904
|
-
private JournalEntry revised(JournalEntryRevisedEvent event) {
|
|
905
|
-
List<JournalLine> newLines = event.lines().stream()
|
|
906
|
-
.map(data -> new JournalLine(
|
|
907
|
-
data.accountCode(),
|
|
908
|
-
data.accountName(),
|
|
909
|
-
data.debitAmount(),
|
|
910
|
-
data.creditAmount()
|
|
911
|
-
))
|
|
912
|
-
.toList();
|
|
913
|
-
|
|
914
|
-
return new JournalEntry(
|
|
915
|
-
journalEntryId,
|
|
916
|
-
journalNumber,
|
|
917
|
-
event.transactionDate(),
|
|
918
|
-
event.description(),
|
|
919
|
-
newLines,
|
|
920
|
-
JournalEntryStatus.DRAFT
|
|
921
|
-
);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
```
|
|
925
|
-
|
|
926
|
-
</details>
|
|
927
|
-
|
|
928
|
-
<details>
|
|
929
|
-
<summary>コード例: JournalEntryStatus.java</summary>
|
|
930
|
-
|
|
931
|
-
```java
|
|
932
|
-
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
* 仕訳ステータス
|
|
936
|
-
*/
|
|
937
|
-
public enum JournalEntryStatus {
|
|
938
|
-
DRAFT, // 下書き
|
|
939
|
-
PENDING_APPROVAL, // 承認待ち
|
|
940
|
-
APPROVED, // 承認済み
|
|
941
|
-
REJECTED, // 却下
|
|
942
|
-
POSTED, // 転記済み
|
|
943
|
-
CANCELLED // キャンセル
|
|
944
|
-
}
|
|
945
|
-
```
|
|
946
|
-
|
|
947
|
-
</details>
|
|
948
|
-
|
|
949
|
-
<details>
|
|
950
|
-
<summary>コード例: JournalLine.java</summary>
|
|
951
|
-
|
|
952
|
-
```java
|
|
953
|
-
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
954
|
-
|
|
955
|
-
import java.math.BigDecimal;
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* 仕訳明細
|
|
959
|
-
*/
|
|
960
|
-
public record JournalLine(
|
|
961
|
-
String accountCode,
|
|
962
|
-
String accountName,
|
|
963
|
-
BigDecimal debitAmount,
|
|
964
|
-
BigDecimal creditAmount
|
|
965
|
-
) {
|
|
966
|
-
public JournalLine {
|
|
967
|
-
if (debitAmount == null) debitAmount = BigDecimal.ZERO;
|
|
968
|
-
if (creditAmount == null) creditAmount = BigDecimal.ZERO;
|
|
969
|
-
|
|
970
|
-
// 借方と貸方は排他的
|
|
971
|
-
if (debitAmount.compareTo(BigDecimal.ZERO) > 0 &&
|
|
972
|
-
creditAmount.compareTo(BigDecimal.ZERO) > 0) {
|
|
973
|
-
throw new IllegalArgumentException(
|
|
974
|
-
"A journal line cannot have both debit and credit amounts"
|
|
975
|
-
);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
```
|
|
980
|
-
|
|
981
|
-
</details>
|
|
982
|
-
|
|
983
|
-
**ドメインモデル設計原則:**
|
|
984
|
-
|
|
985
|
-
| 原則 | 説明 |
|
|
986
|
-
|------|------|
|
|
987
|
-
| **Axon 非依存** | ドメインモデルにフレームワーク依存を持たせない |
|
|
988
|
-
| **イミュータブル** | record でイミュータブルに設計 |
|
|
989
|
-
| **イベントを返す** | 状態遷移メソッドはイベントを返す |
|
|
990
|
-
| **最小限のフィールド** | 状態遷移の判定に必要な最小限のみ保持 |
|
|
991
|
-
| **ビジネスルール** | 借方・貸方の一致検証をドメインモデル内で実施 |
|
|
992
|
-
|
|
993
|
-
---
|
|
994
|
-
|
|
995
|
-
### 27.5 Account Bounded Context
|
|
996
|
-
|
|
997
|
-
勘定科目の残高管理を行います。
|
|
998
|
-
|
|
999
|
-
#### イベント定義
|
|
1000
|
-
|
|
1001
|
-
<details>
|
|
1002
|
-
<summary>コード例: AccountEvent.java</summary>
|
|
1003
|
-
|
|
1004
|
-
```java
|
|
1005
|
-
package com.example.accounting.account.api.events;
|
|
1006
|
-
|
|
1007
|
-
/**
|
|
1008
|
-
* Account イベントの sealed interface
|
|
1009
|
-
*/
|
|
1010
|
-
public sealed interface AccountEvent permits
|
|
1011
|
-
AccountCreatedEvent,
|
|
1012
|
-
AccountBalanceUpdatedEvent,
|
|
1013
|
-
AccountClosedEvent {
|
|
1014
|
-
|
|
1015
|
-
String accountCode();
|
|
1016
|
-
}
|
|
1017
|
-
```
|
|
1018
|
-
|
|
1019
|
-
</details>
|
|
1020
|
-
|
|
1021
|
-
<details>
|
|
1022
|
-
<summary>コード例: AccountBalanceUpdatedEvent.java</summary>
|
|
1023
|
-
|
|
1024
|
-
```java
|
|
1025
|
-
package com.example.accounting.account.api.events;
|
|
1026
|
-
|
|
1027
|
-
import java.math.BigDecimal;
|
|
1028
|
-
import java.time.LocalDateTime;
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* 勘定残高更新イベント
|
|
1032
|
-
*/
|
|
1033
|
-
public record AccountBalanceUpdatedEvent(
|
|
1034
|
-
String accountCode,
|
|
1035
|
-
String journalEntryId,
|
|
1036
|
-
BigDecimal debitAmount,
|
|
1037
|
-
BigDecimal creditAmount,
|
|
1038
|
-
BigDecimal balanceAfter,
|
|
1039
|
-
LocalDateTime updatedAt
|
|
1040
|
-
) implements AccountEvent {
|
|
1041
|
-
}
|
|
1042
|
-
```
|
|
1043
|
-
|
|
1044
|
-
</details>
|
|
1045
|
-
|
|
1046
|
-
---
|
|
1047
|
-
|
|
1048
|
-
## 第28章:Aggregate Adapter と Policy Handler
|
|
1049
|
-
|
|
1050
|
-
### 28.1 Aggregate Adapter パターン
|
|
1051
|
-
|
|
1052
|
-
Aggregate Adapter は、純粋なドメインモデルと Axon Framework を繋ぐアダプター層です。フレームワーク固有のアノテーションやライフサイクル処理をドメインモデルから分離します。
|
|
1053
|
-
|
|
1054
|
-
```plantuml
|
|
1055
|
-
@startuml aggregate_adapter
|
|
1056
|
-
skinparam backgroundColor #FEFEFE
|
|
1057
|
-
|
|
1058
|
-
package "Application Layer" {
|
|
1059
|
-
class JournalEntryAggregateAdapter << @Aggregate >> {
|
|
1060
|
-
- journalEntryId: String
|
|
1061
|
-
- journalEntry: JournalEntry
|
|
1062
|
-
+ @CommandHandler: handle(CreateJournalEntryCommand)
|
|
1063
|
-
+ @CommandHandler: handle(ApproveJournalEntryCommand)
|
|
1064
|
-
+ @EventSourcingHandler: on(JournalEntryCreatedEvent)
|
|
1065
|
-
+ @EventSourcingHandler: on(JournalEntryApprovedEvent)
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
package "Domain Layer" {
|
|
1070
|
-
class JournalEntry << record >> {
|
|
1071
|
-
+ create(): JournalEntryCreatedEvent
|
|
1072
|
-
+ approve(): JournalEntryApprovedEvent
|
|
1073
|
-
+ apply(event): JournalEntry
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
JournalEntryAggregateAdapter --> JournalEntry : "委譲"
|
|
1078
|
-
|
|
1079
|
-
note bottom of JournalEntryAggregateAdapter
|
|
1080
|
-
Axon Framework 用アダプター
|
|
1081
|
-
フレームワーク依存を吸収
|
|
1082
|
-
end note
|
|
1083
|
-
|
|
1084
|
-
note bottom of JournalEntry
|
|
1085
|
-
純粋なドメインモデル
|
|
1086
|
-
Axon に依存しない
|
|
1087
|
-
end note
|
|
1088
|
-
|
|
1089
|
-
@enduml
|
|
1090
|
-
```
|
|
1091
|
-
|
|
1092
|
-
---
|
|
1093
|
-
|
|
1094
|
-
### 28.2 JournalEntry Aggregate Adapter
|
|
1095
|
-
|
|
1096
|
-
<details>
|
|
1097
|
-
<summary>コード例: JournalEntryAggregateAdapter.java</summary>
|
|
1098
|
-
|
|
1099
|
-
```java
|
|
1100
|
-
package com.example.accounting.journal.application.aggregate;
|
|
1101
|
-
|
|
1102
|
-
import com.example.accounting.journal.api.events.*;
|
|
1103
|
-
import com.example.accounting.journal.api.events.JournalEntryCreatedEvent.JournalLineData;
|
|
1104
|
-
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntry;
|
|
1105
|
-
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntryCommands.*;
|
|
1106
|
-
import org.axonframework.commandhandling.CommandHandler;
|
|
1107
|
-
import org.axonframework.eventsourcing.EventSourcingHandler;
|
|
1108
|
-
import org.axonframework.modelling.command.AggregateIdentifier;
|
|
1109
|
-
import org.axonframework.modelling.command.AggregateLifecycle;
|
|
1110
|
-
import org.axonframework.spring.stereotype.Aggregate;
|
|
1111
|
-
|
|
1112
|
-
import java.util.UUID;
|
|
1113
|
-
|
|
1114
|
-
/**
|
|
1115
|
-
* JournalEntry Aggregate Adapter(Axon Framework 用)
|
|
1116
|
-
*/
|
|
1117
|
-
@Aggregate
|
|
1118
|
-
public class JournalEntryAggregateAdapter {
|
|
1119
|
-
|
|
1120
|
-
@AggregateIdentifier
|
|
1121
|
-
private String journalEntryId;
|
|
1122
|
-
|
|
1123
|
-
private JournalEntry journalEntry;
|
|
1124
|
-
|
|
1125
|
-
/**
|
|
1126
|
-
* Axon が使用するデフォルトコンストラクタ
|
|
1127
|
-
*/
|
|
1128
|
-
protected JournalEntryAggregateAdapter() {
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
// ======== Command Handlers ========
|
|
1132
|
-
|
|
1133
|
-
/**
|
|
1134
|
-
* 集約作成コマンドハンドラー(コンストラクタ)
|
|
1135
|
-
*/
|
|
1136
|
-
@CommandHandler
|
|
1137
|
-
public JournalEntryAggregateAdapter(CreateJournalEntryCommand command) {
|
|
1138
|
-
// 仕訳番号を生成
|
|
1139
|
-
String journalNumber = generateJournalNumber();
|
|
1140
|
-
|
|
1141
|
-
// 明細データを変換
|
|
1142
|
-
var lines = command.lines().stream()
|
|
1143
|
-
.map(input -> new JournalLineData(
|
|
1144
|
-
input.accountCode(),
|
|
1145
|
-
input.accountName(),
|
|
1146
|
-
input.debitAmount(),
|
|
1147
|
-
input.creditAmount()
|
|
1148
|
-
))
|
|
1149
|
-
.toList();
|
|
1150
|
-
|
|
1151
|
-
// ドメインモデルのファクトリメソッドを呼び出し
|
|
1152
|
-
var event = JournalEntry.create(
|
|
1153
|
-
command.journalEntryId(),
|
|
1154
|
-
journalNumber,
|
|
1155
|
-
command.transactionDate(),
|
|
1156
|
-
command.description(),
|
|
1157
|
-
lines,
|
|
1158
|
-
command.createdBy()
|
|
1159
|
-
);
|
|
1160
|
-
// イベントを発行
|
|
1161
|
-
AggregateLifecycle.apply(event);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
/**
|
|
1165
|
-
* 承認申請コマンドハンドラー
|
|
1166
|
-
*/
|
|
1167
|
-
@CommandHandler
|
|
1168
|
-
public void handle(SubmitJournalEntryCommand command) {
|
|
1169
|
-
var event = journalEntry.submit(command.submittedBy());
|
|
1170
|
-
AggregateLifecycle.apply(event);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
/**
|
|
1174
|
-
* 承認コマンドハンドラー
|
|
1175
|
-
*/
|
|
1176
|
-
@CommandHandler
|
|
1177
|
-
public void handle(ApproveJournalEntryCommand command) {
|
|
1178
|
-
var event = journalEntry.approve(command.approvedBy(), command.comment());
|
|
1179
|
-
AggregateLifecycle.apply(event);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
/**
|
|
1183
|
-
* 却下コマンドハンドラー
|
|
1184
|
-
*/
|
|
1185
|
-
@CommandHandler
|
|
1186
|
-
public void handle(RejectJournalEntryCommand command) {
|
|
1187
|
-
var event = journalEntry.reject(command.rejectedBy(), command.reason());
|
|
1188
|
-
AggregateLifecycle.apply(event);
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
/**
|
|
1192
|
-
* 修正コマンドハンドラー
|
|
1193
|
-
*/
|
|
1194
|
-
@CommandHandler
|
|
1195
|
-
public void handle(ReviseJournalEntryCommand command) {
|
|
1196
|
-
var lines = command.lines().stream()
|
|
1197
|
-
.map(input -> new JournalLineData(
|
|
1198
|
-
input.accountCode(),
|
|
1199
|
-
input.accountName(),
|
|
1200
|
-
input.debitAmount(),
|
|
1201
|
-
input.creditAmount()
|
|
1202
|
-
))
|
|
1203
|
-
.toList();
|
|
1204
|
-
|
|
1205
|
-
var event = journalEntry.revise(
|
|
1206
|
-
command.transactionDate(),
|
|
1207
|
-
command.description(),
|
|
1208
|
-
lines,
|
|
1209
|
-
command.revisedBy()
|
|
1210
|
-
);
|
|
1211
|
-
AggregateLifecycle.apply(event);
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
/**
|
|
1215
|
-
* 転記コマンドハンドラー
|
|
1216
|
-
*/
|
|
1217
|
-
@CommandHandler
|
|
1218
|
-
public void handle(PostJournalEntryCommand command) {
|
|
1219
|
-
var event = journalEntry.post(command.fiscalPeriod(), command.postedBy());
|
|
1220
|
-
AggregateLifecycle.apply(event);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
/**
|
|
1224
|
-
* キャンセルコマンドハンドラー
|
|
1225
|
-
*/
|
|
1226
|
-
@CommandHandler
|
|
1227
|
-
public void handle(CancelJournalEntryCommand command) {
|
|
1228
|
-
var event = journalEntry.cancel(command.cancelledBy(), command.reason());
|
|
1229
|
-
AggregateLifecycle.apply(event);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
// ======== Event Sourcing Handlers(状態復元)========
|
|
1233
|
-
|
|
1234
|
-
@EventSourcingHandler
|
|
1235
|
-
public void on(JournalEntryCreatedEvent event) {
|
|
1236
|
-
this.journalEntryId = event.journalEntryId();
|
|
1237
|
-
this.journalEntry = JournalEntry.from(event);
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
@EventSourcingHandler
|
|
1241
|
-
public void on(JournalEntrySubmittedEvent event) {
|
|
1242
|
-
this.journalEntry = journalEntry.apply(event);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
@EventSourcingHandler
|
|
1246
|
-
public void on(JournalEntryApprovedEvent event) {
|
|
1247
|
-
this.journalEntry = journalEntry.apply(event);
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
@EventSourcingHandler
|
|
1251
|
-
public void on(JournalEntryRejectedEvent event) {
|
|
1252
|
-
this.journalEntry = journalEntry.apply(event);
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
@EventSourcingHandler
|
|
1256
|
-
public void on(JournalEntryRevisedEvent event) {
|
|
1257
|
-
this.journalEntry = journalEntry.apply(event);
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
@EventSourcingHandler
|
|
1261
|
-
public void on(JournalEntryPostedEvent event) {
|
|
1262
|
-
this.journalEntry = journalEntry.apply(event);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
@EventSourcingHandler
|
|
1266
|
-
public void on(JournalEntryCancelledEvent event) {
|
|
1267
|
-
this.journalEntry = journalEntry.apply(event);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// ======== ヘルパーメソッド ========
|
|
1271
|
-
|
|
1272
|
-
private String generateJournalNumber() {
|
|
1273
|
-
// 実際のシステムでは年月日 + 連番など
|
|
1274
|
-
return "JE-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
```
|
|
1278
|
-
|
|
1279
|
-
</details>
|
|
1280
|
-
|
|
1281
|
-
**Axon アノテーション解説:**
|
|
1282
|
-
|
|
1283
|
-
| アノテーション | 説明 |
|
|
1284
|
-
|--------------|------|
|
|
1285
|
-
| `@Aggregate` | Event Sourcing 集約であることを宣言 |
|
|
1286
|
-
| `@AggregateIdentifier` | 集約の識別子フィールド |
|
|
1287
|
-
| `@CommandHandler` | コマンドを処理するメソッド |
|
|
1288
|
-
| `@EventSourcingHandler` | イベントから状態を復元するメソッド |
|
|
1289
|
-
| `AggregateLifecycle.apply()` | イベントを発行するメソッド |
|
|
1290
|
-
|
|
1291
|
-
---
|
|
1292
|
-
|
|
1293
|
-
### 28.3 Policy Handler(Choreography パターン)
|
|
1294
|
-
|
|
1295
|
-
Policy Handler は、他の Bounded Context から発行されたイベントを購読し、自律的に反応する処理を実装します。Choreography パターンでは、各 Context が独立して動作し、イベントを介して連携します。
|
|
1296
|
-
|
|
1297
|
-
```plantuml
|
|
1298
|
-
@startuml choreography
|
|
1299
|
-
skinparam backgroundColor #FEFEFE
|
|
1300
|
-
|
|
1301
|
-
participant "Journal Context" as Journal
|
|
1302
|
-
participant "Event Bus" as Bus
|
|
1303
|
-
participant "Account Context" as Account
|
|
1304
|
-
|
|
1305
|
-
== 仕訳転記フロー ==
|
|
1306
|
-
Journal -> Bus : JournalEntryPostedEvent
|
|
1307
|
-
Bus -> Account : JournalEntryPostedEvent
|
|
1308
|
-
Account -> Account : updateBalance()
|
|
1309
|
-
Account -> Bus : AccountBalanceUpdatedEvent
|
|
1310
|
-
|
|
1311
|
-
@enduml
|
|
1312
|
-
```
|
|
1313
|
-
|
|
1314
|
-
---
|
|
1315
|
-
|
|
1316
|
-
### 28.4 JournalEventHandler(Account Context 内)
|
|
1317
|
-
|
|
1318
|
-
<details>
|
|
1319
|
-
<summary>コード例: JournalEventHandler.java</summary>
|
|
1320
|
-
|
|
1321
|
-
```java
|
|
1322
|
-
package com.example.accounting.account.application.policy;
|
|
1323
|
-
|
|
1324
|
-
import com.example.accounting.account.domain.model.aggregate.account.AccountCommands.*;
|
|
1325
|
-
import com.example.accounting.journal.api.events.JournalEntryPostedEvent;
|
|
1326
|
-
import org.axonframework.commandhandling.gateway.CommandGateway;
|
|
1327
|
-
import org.axonframework.eventhandling.EventHandler;
|
|
1328
|
-
import org.springframework.stereotype.Component;
|
|
1329
|
-
|
|
1330
|
-
/**
|
|
1331
|
-
* Journal イベントに反応して勘定残高を更新する Policy Handler
|
|
1332
|
-
*/
|
|
1333
|
-
@Component
|
|
1334
|
-
public class JournalEventHandler {
|
|
1335
|
-
|
|
1336
|
-
private final CommandGateway commandGateway;
|
|
1337
|
-
|
|
1338
|
-
public JournalEventHandler(CommandGateway commandGateway) {
|
|
1339
|
-
this.commandGateway = commandGateway;
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
/**
|
|
1343
|
-
* Policy: "When a journal entry is posted, update account balances"
|
|
1344
|
-
*/
|
|
1345
|
-
@EventHandler
|
|
1346
|
-
public void on(JournalEntryPostedEvent event) {
|
|
1347
|
-
// 仕訳の各明細に対して勘定残高を更新
|
|
1348
|
-
// 実際の実装では、イベントから明細情報を取得して処理
|
|
1349
|
-
var command = new UpdateAccountBalanceCommand(
|
|
1350
|
-
event.journalEntryId(),
|
|
1351
|
-
event.fiscalPeriod()
|
|
1352
|
-
);
|
|
1353
|
-
commandGateway.send(command);
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
```
|
|
1357
|
-
|
|
1358
|
-
</details>
|
|
1359
|
-
|
|
1360
|
-
**Policy Handler の設計原則:**
|
|
1361
|
-
|
|
1362
|
-
| 原則 | 説明 |
|
|
1363
|
-
|------|------|
|
|
1364
|
-
| **単一責任** | 1 つの Policy Handler は 1 つの関心事のみを扱う |
|
|
1365
|
-
| **疎結合** | 他の Context のイベントを購読し、自 Context のコマンドを発行 |
|
|
1366
|
-
| **自律性** | 他のサービスに依存せず独立して動作 |
|
|
1367
|
-
| **冪等性** | 同じイベントを複数回受信しても問題ないよう設計 |
|
|
1368
|
-
|
|
1369
|
-
---
|
|
1370
|
-
|
|
1371
|
-
## 第29章:Projection と Read Model
|
|
1372
|
-
|
|
1373
|
-
### 29.1 Projection の役割
|
|
1374
|
-
|
|
1375
|
-
Projection は、イベントを購読して Read Model(クエリ用のデータモデル)を更新するコンポーネントです。CQRS では、書き込みモデル(Event Store)と読み取りモデル(Read Model DB)を分離し、それぞれに最適化されたデータ構造を使用します。
|
|
1376
|
-
|
|
1377
|
-
```plantuml
|
|
1378
|
-
@startuml projection
|
|
1379
|
-
skinparam backgroundColor #FEFEFE
|
|
1380
|
-
|
|
1381
|
-
participant "CommandHandler" as CH
|
|
1382
|
-
participant "Event Store" as ES
|
|
1383
|
-
participant "Projection" as P
|
|
1384
|
-
participant "MyBatis Mapper" as MB
|
|
1385
|
-
participant "Read Model DB" as DB
|
|
1386
|
-
participant "REST Controller" as RC
|
|
1387
|
-
|
|
1388
|
-
== 書き込み(Command Side)==
|
|
1389
|
-
CH -> ES : イベント保存
|
|
1390
|
-
ES -> P : イベント通知
|
|
1391
|
-
|
|
1392
|
-
== 読み取りモデル更新 ==
|
|
1393
|
-
P -> MB : insert/update
|
|
1394
|
-
MB -> DB : SQL 実行
|
|
1395
|
-
|
|
1396
|
-
== 読み取り(Query Side)==
|
|
1397
|
-
RC -> MB : findById
|
|
1398
|
-
MB -> DB : SELECT
|
|
1399
|
-
DB --> MB : 結果
|
|
1400
|
-
MB --> RC : Entity
|
|
1401
|
-
|
|
1402
|
-
note right of P
|
|
1403
|
-
Projection は
|
|
1404
|
-
イベントを購読して
|
|
1405
|
-
MyBatis 経由で
|
|
1406
|
-
Read Model を更新
|
|
1407
|
-
end note
|
|
1408
|
-
|
|
1409
|
-
@enduml
|
|
1410
|
-
```
|
|
1411
|
-
|
|
1412
|
-
---
|
|
1413
|
-
|
|
1414
|
-
### 29.2 JournalEntry Projection(MyBatis 版)
|
|
1415
|
-
|
|
1416
|
-
<details>
|
|
1417
|
-
<summary>コード例: JournalEntryProjection.java</summary>
|
|
1418
|
-
|
|
1419
|
-
```java
|
|
1420
|
-
package com.example.accounting.journal.application.query;
|
|
1421
|
-
|
|
1422
|
-
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalEntryEntity;
|
|
1423
|
-
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalLineEntity;
|
|
1424
|
-
import com.example.accounting.journal.adapter.outbound.persistence.mapper.JournalEntryMapper;
|
|
1425
|
-
import com.example.accounting.journal.api.events.*;
|
|
1426
|
-
import org.axonframework.eventhandling.EventHandler;
|
|
1427
|
-
import org.springframework.stereotype.Component;
|
|
1428
|
-
|
|
1429
|
-
import java.util.List;
|
|
1430
|
-
|
|
1431
|
-
/**
|
|
1432
|
-
* JournalEntry Projection(MyBatis を使用した Read Model の更新)
|
|
1433
|
-
*/
|
|
1434
|
-
@Component
|
|
1435
|
-
public class JournalEntryProjection {
|
|
1436
|
-
|
|
1437
|
-
private final JournalEntryMapper journalEntryMapper;
|
|
1438
|
-
|
|
1439
|
-
public JournalEntryProjection(JournalEntryMapper journalEntryMapper) {
|
|
1440
|
-
this.journalEntryMapper = journalEntryMapper;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
@EventHandler
|
|
1444
|
-
public void on(JournalEntryCreatedEvent event) {
|
|
1445
|
-
var entity = new JournalEntryEntity();
|
|
1446
|
-
entity.setJournalEntryId(event.journalEntryId());
|
|
1447
|
-
entity.setJournalNumber(event.journalNumber());
|
|
1448
|
-
entity.setTransactionDate(event.transactionDate());
|
|
1449
|
-
entity.setDescription(event.description());
|
|
1450
|
-
entity.setStatus("DRAFT");
|
|
1451
|
-
entity.setCreatedBy(event.createdBy());
|
|
1452
|
-
entity.setCreatedAt(event.createdAt());
|
|
1453
|
-
|
|
1454
|
-
journalEntryMapper.insert(entity);
|
|
1455
|
-
|
|
1456
|
-
// 明細を保存
|
|
1457
|
-
List<JournalLineEntity> lines = event.lines().stream()
|
|
1458
|
-
.map(data -> {
|
|
1459
|
-
var line = new JournalLineEntity();
|
|
1460
|
-
line.setJournalEntryId(event.journalEntryId());
|
|
1461
|
-
line.setAccountCode(data.accountCode());
|
|
1462
|
-
line.setAccountName(data.accountName());
|
|
1463
|
-
line.setDebitAmount(data.debitAmount());
|
|
1464
|
-
line.setCreditAmount(data.creditAmount());
|
|
1465
|
-
return line;
|
|
1466
|
-
})
|
|
1467
|
-
.toList();
|
|
1468
|
-
|
|
1469
|
-
journalEntryMapper.insertLines(lines);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
@EventHandler
|
|
1473
|
-
public void on(JournalEntrySubmittedEvent event) {
|
|
1474
|
-
journalEntryMapper.updateStatus(event.journalEntryId(), "PENDING_APPROVAL");
|
|
1475
|
-
journalEntryMapper.updateSubmission(event.journalEntryId(), event.submittedBy(), event.submittedAt());
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
@EventHandler
|
|
1479
|
-
public void on(JournalEntryApprovedEvent event) {
|
|
1480
|
-
journalEntryMapper.updateStatus(event.journalEntryId(), "APPROVED");
|
|
1481
|
-
journalEntryMapper.updateApproval(
|
|
1482
|
-
event.journalEntryId(),
|
|
1483
|
-
event.approvedBy(),
|
|
1484
|
-
event.comment(),
|
|
1485
|
-
event.approvedAt()
|
|
1486
|
-
);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
@EventHandler
|
|
1490
|
-
public void on(JournalEntryRejectedEvent event) {
|
|
1491
|
-
journalEntryMapper.updateStatus(event.journalEntryId(), "REJECTED");
|
|
1492
|
-
journalEntryMapper.updateRejection(
|
|
1493
|
-
event.journalEntryId(),
|
|
1494
|
-
event.rejectedBy(),
|
|
1495
|
-
event.reason(),
|
|
1496
|
-
event.rejectedAt()
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
@EventHandler
|
|
1501
|
-
public void on(JournalEntryRevisedEvent event) {
|
|
1502
|
-
journalEntryMapper.updateStatus(event.journalEntryId(), "DRAFT");
|
|
1503
|
-
journalEntryMapper.updateRevision(
|
|
1504
|
-
event.journalEntryId(),
|
|
1505
|
-
event.transactionDate(),
|
|
1506
|
-
event.description(),
|
|
1507
|
-
event.revisedBy(),
|
|
1508
|
-
event.revisedAt()
|
|
1509
|
-
);
|
|
1510
|
-
|
|
1511
|
-
// 明細を削除して再作成
|
|
1512
|
-
journalEntryMapper.deleteLines(event.journalEntryId());
|
|
1513
|
-
List<JournalLineEntity> lines = event.lines().stream()
|
|
1514
|
-
.map(data -> {
|
|
1515
|
-
var line = new JournalLineEntity();
|
|
1516
|
-
line.setJournalEntryId(event.journalEntryId());
|
|
1517
|
-
line.setAccountCode(data.accountCode());
|
|
1518
|
-
line.setAccountName(data.accountName());
|
|
1519
|
-
line.setDebitAmount(data.debitAmount());
|
|
1520
|
-
line.setCreditAmount(data.creditAmount());
|
|
1521
|
-
return line;
|
|
1522
|
-
})
|
|
1523
|
-
.toList();
|
|
1524
|
-
journalEntryMapper.insertLines(lines);
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
@EventHandler
|
|
1528
|
-
public void on(JournalEntryPostedEvent event) {
|
|
1529
|
-
journalEntryMapper.updateStatus(event.journalEntryId(), "POSTED");
|
|
1530
|
-
journalEntryMapper.updatePosting(
|
|
1531
|
-
event.journalEntryId(),
|
|
1532
|
-
event.fiscalPeriod(),
|
|
1533
|
-
event.postedBy(),
|
|
1534
|
-
event.postedAt()
|
|
1535
|
-
);
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
@EventHandler
|
|
1539
|
-
public void on(JournalEntryCancelledEvent event) {
|
|
1540
|
-
journalEntryMapper.updateStatus(event.journalEntryId(), "CANCELLED");
|
|
1541
|
-
journalEntryMapper.updateCancellation(
|
|
1542
|
-
event.journalEntryId(),
|
|
1543
|
-
event.cancelledBy(),
|
|
1544
|
-
event.reason(),
|
|
1545
|
-
event.cancelledAt()
|
|
1546
|
-
);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
```
|
|
1550
|
-
|
|
1551
|
-
</details>
|
|
1552
|
-
|
|
1553
|
-
---
|
|
1554
|
-
|
|
1555
|
-
### 29.3 Read Model Entity
|
|
1556
|
-
|
|
1557
|
-
<details>
|
|
1558
|
-
<summary>コード例: JournalEntryEntity.java</summary>
|
|
1559
|
-
|
|
1560
|
-
```java
|
|
1561
|
-
package com.example.accounting.journal.adapter.outbound.persistence.entity;
|
|
1562
|
-
|
|
1563
|
-
import java.math.BigDecimal;
|
|
1564
|
-
import java.time.LocalDate;
|
|
1565
|
-
import java.time.LocalDateTime;
|
|
1566
|
-
import java.util.List;
|
|
1567
|
-
|
|
1568
|
-
/**
|
|
1569
|
-
* JournalEntry Read Model(表示用のすべてのフィールドを保持)
|
|
1570
|
-
*/
|
|
1571
|
-
public class JournalEntryEntity {
|
|
1572
|
-
|
|
1573
|
-
private String journalEntryId;
|
|
1574
|
-
private String journalNumber;
|
|
1575
|
-
private LocalDate transactionDate;
|
|
1576
|
-
private String description;
|
|
1577
|
-
private String status;
|
|
1578
|
-
|
|
1579
|
-
// 合計金額(集計用)
|
|
1580
|
-
private BigDecimal totalDebitAmount;
|
|
1581
|
-
private BigDecimal totalCreditAmount;
|
|
1582
|
-
|
|
1583
|
-
// 監査情報
|
|
1584
|
-
private String createdBy;
|
|
1585
|
-
private LocalDateTime createdAt;
|
|
1586
|
-
private String submittedBy;
|
|
1587
|
-
private LocalDateTime submittedAt;
|
|
1588
|
-
private String approvedBy;
|
|
1589
|
-
private String approvalComment;
|
|
1590
|
-
private LocalDateTime approvedAt;
|
|
1591
|
-
private String rejectedBy;
|
|
1592
|
-
private String rejectionReason;
|
|
1593
|
-
private LocalDateTime rejectedAt;
|
|
1594
|
-
private String revisedBy;
|
|
1595
|
-
private LocalDateTime revisedAt;
|
|
1596
|
-
private String fiscalPeriod;
|
|
1597
|
-
private String postedBy;
|
|
1598
|
-
private LocalDateTime postedAt;
|
|
1599
|
-
private String cancelledBy;
|
|
1600
|
-
private String cancellationReason;
|
|
1601
|
-
private LocalDateTime cancelledAt;
|
|
1602
|
-
|
|
1603
|
-
// 明細
|
|
1604
|
-
private List<JournalLineEntity> lines;
|
|
1605
|
-
|
|
1606
|
-
// Getters and Setters
|
|
1607
|
-
public String getJournalEntryId() { return journalEntryId; }
|
|
1608
|
-
public void setJournalEntryId(String journalEntryId) { this.journalEntryId = journalEntryId; }
|
|
1609
|
-
|
|
1610
|
-
public String getJournalNumber() { return journalNumber; }
|
|
1611
|
-
public void setJournalNumber(String journalNumber) { this.journalNumber = journalNumber; }
|
|
1612
|
-
|
|
1613
|
-
public LocalDate getTransactionDate() { return transactionDate; }
|
|
1614
|
-
public void setTransactionDate(LocalDate transactionDate) { this.transactionDate = transactionDate; }
|
|
1615
|
-
|
|
1616
|
-
public String getDescription() { return description; }
|
|
1617
|
-
public void setDescription(String description) { this.description = description; }
|
|
1618
|
-
|
|
1619
|
-
public String getStatus() { return status; }
|
|
1620
|
-
public void setStatus(String status) { this.status = status; }
|
|
1621
|
-
|
|
1622
|
-
public BigDecimal getTotalDebitAmount() { return totalDebitAmount; }
|
|
1623
|
-
public void setTotalDebitAmount(BigDecimal totalDebitAmount) { this.totalDebitAmount = totalDebitAmount; }
|
|
1624
|
-
|
|
1625
|
-
public BigDecimal getTotalCreditAmount() { return totalCreditAmount; }
|
|
1626
|
-
public void setTotalCreditAmount(BigDecimal totalCreditAmount) { this.totalCreditAmount = totalCreditAmount; }
|
|
1627
|
-
|
|
1628
|
-
public String getCreatedBy() { return createdBy; }
|
|
1629
|
-
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
|
|
1630
|
-
|
|
1631
|
-
public LocalDateTime getCreatedAt() { return createdAt; }
|
|
1632
|
-
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
|
1633
|
-
|
|
1634
|
-
public String getSubmittedBy() { return submittedBy; }
|
|
1635
|
-
public void setSubmittedBy(String submittedBy) { this.submittedBy = submittedBy; }
|
|
1636
|
-
|
|
1637
|
-
public LocalDateTime getSubmittedAt() { return submittedAt; }
|
|
1638
|
-
public void setSubmittedAt(LocalDateTime submittedAt) { this.submittedAt = submittedAt; }
|
|
1639
|
-
|
|
1640
|
-
public String getApprovedBy() { return approvedBy; }
|
|
1641
|
-
public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; }
|
|
1642
|
-
|
|
1643
|
-
public String getApprovalComment() { return approvalComment; }
|
|
1644
|
-
public void setApprovalComment(String approvalComment) { this.approvalComment = approvalComment; }
|
|
1645
|
-
|
|
1646
|
-
public LocalDateTime getApprovedAt() { return approvedAt; }
|
|
1647
|
-
public void setApprovedAt(LocalDateTime approvedAt) { this.approvedAt = approvedAt; }
|
|
1648
|
-
|
|
1649
|
-
public String getRejectedBy() { return rejectedBy; }
|
|
1650
|
-
public void setRejectedBy(String rejectedBy) { this.rejectedBy = rejectedBy; }
|
|
1651
|
-
|
|
1652
|
-
public String getRejectionReason() { return rejectionReason; }
|
|
1653
|
-
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
|
|
1654
|
-
|
|
1655
|
-
public LocalDateTime getRejectedAt() { return rejectedAt; }
|
|
1656
|
-
public void setRejectedAt(LocalDateTime rejectedAt) { this.rejectedAt = rejectedAt; }
|
|
1657
|
-
|
|
1658
|
-
public String getRevisedBy() { return revisedBy; }
|
|
1659
|
-
public void setRevisedBy(String revisedBy) { this.revisedBy = revisedBy; }
|
|
1660
|
-
|
|
1661
|
-
public LocalDateTime getRevisedAt() { return revisedAt; }
|
|
1662
|
-
public void setRevisedAt(LocalDateTime revisedAt) { this.revisedAt = revisedAt; }
|
|
1663
|
-
|
|
1664
|
-
public String getFiscalPeriod() { return fiscalPeriod; }
|
|
1665
|
-
public void setFiscalPeriod(String fiscalPeriod) { this.fiscalPeriod = fiscalPeriod; }
|
|
1666
|
-
|
|
1667
|
-
public String getPostedBy() { return postedBy; }
|
|
1668
|
-
public void setPostedBy(String postedBy) { this.postedBy = postedBy; }
|
|
1669
|
-
|
|
1670
|
-
public LocalDateTime getPostedAt() { return postedAt; }
|
|
1671
|
-
public void setPostedAt(LocalDateTime postedAt) { this.postedAt = postedAt; }
|
|
1672
|
-
|
|
1673
|
-
public String getCancelledBy() { return cancelledBy; }
|
|
1674
|
-
public void setCancelledBy(String cancelledBy) { this.cancelledBy = cancelledBy; }
|
|
1675
|
-
|
|
1676
|
-
public String getCancellationReason() { return cancellationReason; }
|
|
1677
|
-
public void setCancellationReason(String cancellationReason) { this.cancellationReason = cancellationReason; }
|
|
1678
|
-
|
|
1679
|
-
public LocalDateTime getCancelledAt() { return cancelledAt; }
|
|
1680
|
-
public void setCancelledAt(LocalDateTime cancelledAt) { this.cancelledAt = cancelledAt; }
|
|
1681
|
-
|
|
1682
|
-
public List<JournalLineEntity> getLines() { return lines; }
|
|
1683
|
-
public void setLines(List<JournalLineEntity> lines) { this.lines = lines; }
|
|
1684
|
-
}
|
|
1685
|
-
```
|
|
1686
|
-
|
|
1687
|
-
</details>
|
|
1688
|
-
|
|
1689
|
-
<details>
|
|
1690
|
-
<summary>コード例: JournalLineEntity.java</summary>
|
|
1691
|
-
|
|
1692
|
-
```java
|
|
1693
|
-
package com.example.accounting.journal.adapter.outbound.persistence.entity;
|
|
1694
|
-
|
|
1695
|
-
import java.math.BigDecimal;
|
|
1696
|
-
|
|
1697
|
-
/**
|
|
1698
|
-
* JournalLine Read Model
|
|
1699
|
-
*/
|
|
1700
|
-
public class JournalLineEntity {
|
|
1701
|
-
|
|
1702
|
-
private Long lineId;
|
|
1703
|
-
private String journalEntryId;
|
|
1704
|
-
private String accountCode;
|
|
1705
|
-
private String accountName;
|
|
1706
|
-
private BigDecimal debitAmount;
|
|
1707
|
-
private BigDecimal creditAmount;
|
|
1708
|
-
|
|
1709
|
-
// Getters and Setters
|
|
1710
|
-
public Long getLineId() { return lineId; }
|
|
1711
|
-
public void setLineId(Long lineId) { this.lineId = lineId; }
|
|
1712
|
-
|
|
1713
|
-
public String getJournalEntryId() { return journalEntryId; }
|
|
1714
|
-
public void setJournalEntryId(String journalEntryId) { this.journalEntryId = journalEntryId; }
|
|
1715
|
-
|
|
1716
|
-
public String getAccountCode() { return accountCode; }
|
|
1717
|
-
public void setAccountCode(String accountCode) { this.accountCode = accountCode; }
|
|
1718
|
-
|
|
1719
|
-
public String getAccountName() { return accountName; }
|
|
1720
|
-
public void setAccountName(String accountName) { this.accountName = accountName; }
|
|
1721
|
-
|
|
1722
|
-
public BigDecimal getDebitAmount() { return debitAmount; }
|
|
1723
|
-
public void setDebitAmount(BigDecimal debitAmount) { this.debitAmount = debitAmount; }
|
|
1724
|
-
|
|
1725
|
-
public BigDecimal getCreditAmount() { return creditAmount; }
|
|
1726
|
-
public void setCreditAmount(BigDecimal creditAmount) { this.creditAmount = creditAmount; }
|
|
1727
|
-
}
|
|
1728
|
-
```
|
|
1729
|
-
|
|
1730
|
-
</details>
|
|
1731
|
-
|
|
1732
|
-
---
|
|
1733
|
-
|
|
1734
|
-
### 29.4 MyBatis Mapper インターフェース
|
|
1735
|
-
|
|
1736
|
-
<details>
|
|
1737
|
-
<summary>コード例: JournalEntryMapper.java</summary>
|
|
1738
|
-
|
|
1739
|
-
```java
|
|
1740
|
-
package com.example.accounting.journal.adapter.outbound.persistence.mapper;
|
|
1741
|
-
|
|
1742
|
-
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalEntryEntity;
|
|
1743
|
-
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalLineEntity;
|
|
1744
|
-
import org.apache.ibatis.annotations.Mapper;
|
|
1745
|
-
import org.apache.ibatis.annotations.Param;
|
|
1746
|
-
|
|
1747
|
-
import java.time.LocalDate;
|
|
1748
|
-
import java.time.LocalDateTime;
|
|
1749
|
-
import java.util.List;
|
|
1750
|
-
import java.util.Optional;
|
|
1751
|
-
|
|
1752
|
-
/**
|
|
1753
|
-
* JournalEntry MyBatis Mapper
|
|
1754
|
-
*/
|
|
1755
|
-
@Mapper
|
|
1756
|
-
public interface JournalEntryMapper {
|
|
1757
|
-
|
|
1758
|
-
// 挿入
|
|
1759
|
-
void insert(JournalEntryEntity entity);
|
|
1760
|
-
|
|
1761
|
-
void insertLines(List<JournalLineEntity> lines);
|
|
1762
|
-
|
|
1763
|
-
// 検索
|
|
1764
|
-
Optional<JournalEntryEntity> findById(@Param("journalEntryId") String journalEntryId);
|
|
1765
|
-
|
|
1766
|
-
List<JournalEntryEntity> findAll();
|
|
1767
|
-
|
|
1768
|
-
List<JournalEntryEntity> findByStatus(@Param("status") String status);
|
|
1769
|
-
|
|
1770
|
-
List<JournalEntryEntity> findByFiscalPeriod(@Param("fiscalPeriod") String fiscalPeriod);
|
|
1771
|
-
|
|
1772
|
-
List<JournalEntryEntity> findByTransactionDateRange(
|
|
1773
|
-
@Param("fromDate") LocalDate fromDate,
|
|
1774
|
-
@Param("toDate") LocalDate toDate
|
|
1775
|
-
);
|
|
1776
|
-
|
|
1777
|
-
List<JournalLineEntity> findLinesByJournalEntryId(@Param("journalEntryId") String journalEntryId);
|
|
1778
|
-
|
|
1779
|
-
// 更新
|
|
1780
|
-
void updateStatus(@Param("journalEntryId") String journalEntryId, @Param("status") String status);
|
|
1781
|
-
|
|
1782
|
-
void updateSubmission(
|
|
1783
|
-
@Param("journalEntryId") String journalEntryId,
|
|
1784
|
-
@Param("submittedBy") String submittedBy,
|
|
1785
|
-
@Param("submittedAt") LocalDateTime submittedAt
|
|
1786
|
-
);
|
|
1787
|
-
|
|
1788
|
-
void updateApproval(
|
|
1789
|
-
@Param("journalEntryId") String journalEntryId,
|
|
1790
|
-
@Param("approvedBy") String approvedBy,
|
|
1791
|
-
@Param("comment") String comment,
|
|
1792
|
-
@Param("approvedAt") LocalDateTime approvedAt
|
|
1793
|
-
);
|
|
1794
|
-
|
|
1795
|
-
void updateRejection(
|
|
1796
|
-
@Param("journalEntryId") String journalEntryId,
|
|
1797
|
-
@Param("rejectedBy") String rejectedBy,
|
|
1798
|
-
@Param("reason") String reason,
|
|
1799
|
-
@Param("rejectedAt") LocalDateTime rejectedAt
|
|
1800
|
-
);
|
|
1801
|
-
|
|
1802
|
-
void updateRevision(
|
|
1803
|
-
@Param("journalEntryId") String journalEntryId,
|
|
1804
|
-
@Param("transactionDate") LocalDate transactionDate,
|
|
1805
|
-
@Param("description") String description,
|
|
1806
|
-
@Param("revisedBy") String revisedBy,
|
|
1807
|
-
@Param("revisedAt") LocalDateTime revisedAt
|
|
1808
|
-
);
|
|
1809
|
-
|
|
1810
|
-
void updatePosting(
|
|
1811
|
-
@Param("journalEntryId") String journalEntryId,
|
|
1812
|
-
@Param("fiscalPeriod") String fiscalPeriod,
|
|
1813
|
-
@Param("postedBy") String postedBy,
|
|
1814
|
-
@Param("postedAt") LocalDateTime postedAt
|
|
1815
|
-
);
|
|
1816
|
-
|
|
1817
|
-
void updateCancellation(
|
|
1818
|
-
@Param("journalEntryId") String journalEntryId,
|
|
1819
|
-
@Param("cancelledBy") String cancelledBy,
|
|
1820
|
-
@Param("reason") String reason,
|
|
1821
|
-
@Param("cancelledAt") LocalDateTime cancelledAt
|
|
1822
|
-
);
|
|
1823
|
-
|
|
1824
|
-
// 削除
|
|
1825
|
-
void deleteLines(@Param("journalEntryId") String journalEntryId);
|
|
1826
|
-
}
|
|
1827
|
-
```
|
|
1828
|
-
|
|
1829
|
-
</details>
|
|
1830
|
-
|
|
1831
|
-
---
|
|
1832
|
-
|
|
1833
|
-
### 29.5 MyBatis Mapper XML
|
|
1834
|
-
|
|
1835
|
-
<details>
|
|
1836
|
-
<summary>コード例: JournalEntryMapper.xml</summary>
|
|
1837
|
-
|
|
1838
|
-
```xml
|
|
1839
|
-
<?xml version="1.0" encoding="UTF-8" ?>
|
|
1840
|
-
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
1841
|
-
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
1842
|
-
|
|
1843
|
-
<mapper namespace="com.example.accounting.journal.adapter.outbound.persistence.mapper.JournalEntryMapper">
|
|
1844
|
-
|
|
1845
|
-
<resultMap id="journalEntryResultMap" type="JournalEntryEntity">
|
|
1846
|
-
<id property="journalEntryId" column="journal_entry_id"/>
|
|
1847
|
-
<result property="journalNumber" column="journal_number"/>
|
|
1848
|
-
<result property="transactionDate" column="transaction_date"/>
|
|
1849
|
-
<result property="description" column="description"/>
|
|
1850
|
-
<result property="status" column="status"/>
|
|
1851
|
-
<result property="totalDebitAmount" column="total_debit_amount"/>
|
|
1852
|
-
<result property="totalCreditAmount" column="total_credit_amount"/>
|
|
1853
|
-
<result property="createdBy" column="created_by"/>
|
|
1854
|
-
<result property="createdAt" column="created_at"/>
|
|
1855
|
-
<result property="submittedBy" column="submitted_by"/>
|
|
1856
|
-
<result property="submittedAt" column="submitted_at"/>
|
|
1857
|
-
<result property="approvedBy" column="approved_by"/>
|
|
1858
|
-
<result property="approvalComment" column="approval_comment"/>
|
|
1859
|
-
<result property="approvedAt" column="approved_at"/>
|
|
1860
|
-
<result property="rejectedBy" column="rejected_by"/>
|
|
1861
|
-
<result property="rejectionReason" column="rejection_reason"/>
|
|
1862
|
-
<result property="rejectedAt" column="rejected_at"/>
|
|
1863
|
-
<result property="revisedBy" column="revised_by"/>
|
|
1864
|
-
<result property="revisedAt" column="revised_at"/>
|
|
1865
|
-
<result property="fiscalPeriod" column="fiscal_period"/>
|
|
1866
|
-
<result property="postedBy" column="posted_by"/>
|
|
1867
|
-
<result property="postedAt" column="posted_at"/>
|
|
1868
|
-
<result property="cancelledBy" column="cancelled_by"/>
|
|
1869
|
-
<result property="cancellationReason" column="cancellation_reason"/>
|
|
1870
|
-
<result property="cancelledAt" column="cancelled_at"/>
|
|
1871
|
-
<collection property="lines" ofType="JournalLineEntity"
|
|
1872
|
-
select="findLinesByJournalEntryId" column="journal_entry_id"/>
|
|
1873
|
-
</resultMap>
|
|
1874
|
-
|
|
1875
|
-
<resultMap id="journalLineResultMap" type="JournalLineEntity">
|
|
1876
|
-
<id property="lineId" column="line_id"/>
|
|
1877
|
-
<result property="journalEntryId" column="journal_entry_id"/>
|
|
1878
|
-
<result property="accountCode" column="account_code"/>
|
|
1879
|
-
<result property="accountName" column="account_name"/>
|
|
1880
|
-
<result property="debitAmount" column="debit_amount"/>
|
|
1881
|
-
<result property="creditAmount" column="credit_amount"/>
|
|
1882
|
-
</resultMap>
|
|
1883
|
-
|
|
1884
|
-
<insert id="insert" parameterType="JournalEntryEntity">
|
|
1885
|
-
INSERT INTO journal_entries (
|
|
1886
|
-
journal_entry_id, journal_number, transaction_date, description,
|
|
1887
|
-
status, created_by, created_at
|
|
1888
|
-
) VALUES (
|
|
1889
|
-
#{journalEntryId}, #{journalNumber}, #{transactionDate}, #{description},
|
|
1890
|
-
#{status}, #{createdBy}, #{createdAt}
|
|
1891
|
-
)
|
|
1892
|
-
</insert>
|
|
1893
|
-
|
|
1894
|
-
<insert id="insertLines" parameterType="list">
|
|
1895
|
-
INSERT INTO journal_lines (
|
|
1896
|
-
journal_entry_id, account_code, account_name, debit_amount, credit_amount
|
|
1897
|
-
) VALUES
|
|
1898
|
-
<foreach collection="list" item="line" separator=",">
|
|
1899
|
-
(#{line.journalEntryId}, #{line.accountCode}, #{line.accountName},
|
|
1900
|
-
#{line.debitAmount}, #{line.creditAmount})
|
|
1901
|
-
</foreach>
|
|
1902
|
-
</insert>
|
|
1903
|
-
|
|
1904
|
-
<select id="findById" resultMap="journalEntryResultMap">
|
|
1905
|
-
SELECT je.*,
|
|
1906
|
-
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1907
|
-
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1908
|
-
FROM journal_entries je
|
|
1909
|
-
WHERE je.journal_entry_id = #{journalEntryId}
|
|
1910
|
-
</select>
|
|
1911
|
-
|
|
1912
|
-
<select id="findAll" resultMap="journalEntryResultMap">
|
|
1913
|
-
SELECT je.*,
|
|
1914
|
-
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1915
|
-
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1916
|
-
FROM journal_entries je
|
|
1917
|
-
ORDER BY je.created_at DESC
|
|
1918
|
-
</select>
|
|
1919
|
-
|
|
1920
|
-
<select id="findByStatus" resultMap="journalEntryResultMap">
|
|
1921
|
-
SELECT je.*,
|
|
1922
|
-
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1923
|
-
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1924
|
-
FROM journal_entries je
|
|
1925
|
-
WHERE je.status = #{status}
|
|
1926
|
-
ORDER BY je.created_at DESC
|
|
1927
|
-
</select>
|
|
1928
|
-
|
|
1929
|
-
<select id="findByFiscalPeriod" resultMap="journalEntryResultMap">
|
|
1930
|
-
SELECT je.*,
|
|
1931
|
-
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1932
|
-
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1933
|
-
FROM journal_entries je
|
|
1934
|
-
WHERE je.fiscal_period = #{fiscalPeriod}
|
|
1935
|
-
ORDER BY je.transaction_date, je.journal_number
|
|
1936
|
-
</select>
|
|
1937
|
-
|
|
1938
|
-
<select id="findByTransactionDateRange" resultMap="journalEntryResultMap">
|
|
1939
|
-
SELECT je.*,
|
|
1940
|
-
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1941
|
-
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1942
|
-
FROM journal_entries je
|
|
1943
|
-
WHERE je.transaction_date BETWEEN #{fromDate} AND #{toDate}
|
|
1944
|
-
ORDER BY je.transaction_date, je.journal_number
|
|
1945
|
-
</select>
|
|
1946
|
-
|
|
1947
|
-
<select id="findLinesByJournalEntryId" resultMap="journalLineResultMap">
|
|
1948
|
-
SELECT * FROM journal_lines WHERE journal_entry_id = #{journalEntryId} ORDER BY line_id
|
|
1949
|
-
</select>
|
|
1950
|
-
|
|
1951
|
-
<update id="updateStatus">
|
|
1952
|
-
UPDATE journal_entries SET status = #{status} WHERE journal_entry_id = #{journalEntryId}
|
|
1953
|
-
</update>
|
|
1954
|
-
|
|
1955
|
-
<update id="updateSubmission">
|
|
1956
|
-
UPDATE journal_entries
|
|
1957
|
-
SET submitted_by = #{submittedBy}, submitted_at = #{submittedAt}
|
|
1958
|
-
WHERE journal_entry_id = #{journalEntryId}
|
|
1959
|
-
</update>
|
|
1960
|
-
|
|
1961
|
-
<update id="updateApproval">
|
|
1962
|
-
UPDATE journal_entries
|
|
1963
|
-
SET approved_by = #{approvedBy}, approval_comment = #{comment}, approved_at = #{approvedAt}
|
|
1964
|
-
WHERE journal_entry_id = #{journalEntryId}
|
|
1965
|
-
</update>
|
|
1966
|
-
|
|
1967
|
-
<update id="updateRejection">
|
|
1968
|
-
UPDATE journal_entries
|
|
1969
|
-
SET rejected_by = #{rejectedBy}, rejection_reason = #{reason}, rejected_at = #{rejectedAt}
|
|
1970
|
-
WHERE journal_entry_id = #{journalEntryId}
|
|
1971
|
-
</update>
|
|
1972
|
-
|
|
1973
|
-
<update id="updateRevision">
|
|
1974
|
-
UPDATE journal_entries
|
|
1975
|
-
SET transaction_date = #{transactionDate}, description = #{description},
|
|
1976
|
-
revised_by = #{revisedBy}, revised_at = #{revisedAt}
|
|
1977
|
-
WHERE journal_entry_id = #{journalEntryId}
|
|
1978
|
-
</update>
|
|
1979
|
-
|
|
1980
|
-
<update id="updatePosting">
|
|
1981
|
-
UPDATE journal_entries
|
|
1982
|
-
SET fiscal_period = #{fiscalPeriod}, posted_by = #{postedBy}, posted_at = #{postedAt}
|
|
1983
|
-
WHERE journal_entry_id = #{journalEntryId}
|
|
1984
|
-
</update>
|
|
1985
|
-
|
|
1986
|
-
<update id="updateCancellation">
|
|
1987
|
-
UPDATE journal_entries
|
|
1988
|
-
SET cancelled_by = #{cancelledBy}, cancellation_reason = #{reason}, cancelled_at = #{cancelledAt}
|
|
1989
|
-
WHERE journal_entry_id = #{journalEntryId}
|
|
1990
|
-
</update>
|
|
1991
|
-
|
|
1992
|
-
<delete id="deleteLines">
|
|
1993
|
-
DELETE FROM journal_lines WHERE journal_entry_id = #{journalEntryId}
|
|
1994
|
-
</delete>
|
|
1995
|
-
|
|
1996
|
-
</mapper>
|
|
1997
|
-
```
|
|
1998
|
-
|
|
1999
|
-
</details>
|
|
2000
|
-
|
|
2001
|
-
---
|
|
2002
|
-
|
|
2003
|
-
### 29.6 Read Model スキーマ
|
|
2004
|
-
|
|
2005
|
-
<details>
|
|
2006
|
-
<summary>コード例: schema.sql</summary>
|
|
2007
|
-
|
|
2008
|
-
```sql
|
|
2009
|
-
-- Read Model: Journal Entries テーブル
|
|
2010
|
-
CREATE TABLE IF NOT EXISTS journal_entries (
|
|
2011
|
-
journal_entry_id VARCHAR(36) PRIMARY KEY,
|
|
2012
|
-
journal_number VARCHAR(20) NOT NULL UNIQUE,
|
|
2013
|
-
transaction_date DATE NOT NULL,
|
|
2014
|
-
description TEXT,
|
|
2015
|
-
status VARCHAR(20) NOT NULL,
|
|
2016
|
-
|
|
2017
|
-
-- 監査情報
|
|
2018
|
-
created_by VARCHAR(100) NOT NULL,
|
|
2019
|
-
created_at TIMESTAMP NOT NULL,
|
|
2020
|
-
submitted_by VARCHAR(100),
|
|
2021
|
-
submitted_at TIMESTAMP,
|
|
2022
|
-
approved_by VARCHAR(100),
|
|
2023
|
-
approval_comment TEXT,
|
|
2024
|
-
approved_at TIMESTAMP,
|
|
2025
|
-
rejected_by VARCHAR(100),
|
|
2026
|
-
rejection_reason TEXT,
|
|
2027
|
-
rejected_at TIMESTAMP,
|
|
2028
|
-
revised_by VARCHAR(100),
|
|
2029
|
-
revised_at TIMESTAMP,
|
|
2030
|
-
fiscal_period VARCHAR(10),
|
|
2031
|
-
posted_by VARCHAR(100),
|
|
2032
|
-
posted_at TIMESTAMP,
|
|
2033
|
-
cancelled_by VARCHAR(100),
|
|
2034
|
-
cancellation_reason TEXT,
|
|
2035
|
-
cancelled_at TIMESTAMP
|
|
2036
|
-
);
|
|
2037
|
-
|
|
2038
|
-
-- Read Model: Journal Lines テーブル
|
|
2039
|
-
CREATE TABLE IF NOT EXISTS journal_lines (
|
|
2040
|
-
line_id SERIAL PRIMARY KEY,
|
|
2041
|
-
journal_entry_id VARCHAR(36) NOT NULL REFERENCES journal_entries(journal_entry_id),
|
|
2042
|
-
account_code VARCHAR(10) NOT NULL,
|
|
2043
|
-
account_name VARCHAR(100) NOT NULL,
|
|
2044
|
-
debit_amount DECIMAL(15, 2) DEFAULT 0,
|
|
2045
|
-
credit_amount DECIMAL(15, 2) DEFAULT 0
|
|
2046
|
-
);
|
|
2047
|
-
|
|
2048
|
-
-- インデックス
|
|
2049
|
-
CREATE INDEX IF NOT EXISTS idx_journal_entries_status ON journal_entries(status);
|
|
2050
|
-
CREATE INDEX IF NOT EXISTS idx_journal_entries_transaction_date ON journal_entries(transaction_date);
|
|
2051
|
-
CREATE INDEX IF NOT EXISTS idx_journal_entries_fiscal_period ON journal_entries(fiscal_period);
|
|
2052
|
-
CREATE INDEX IF NOT EXISTS idx_journal_lines_journal_entry_id ON journal_lines(journal_entry_id);
|
|
2053
|
-
CREATE INDEX IF NOT EXISTS idx_journal_lines_account_code ON journal_lines(account_code);
|
|
2054
|
-
```
|
|
2055
|
-
|
|
2056
|
-
</details>
|
|
2057
|
-
|
|
2058
|
-
---
|
|
2059
|
-
|
|
2060
|
-
### 29.7 最小限フィールドの原則
|
|
2061
|
-
|
|
2062
|
-
Event Sourcing では、ドメインモデルは「次のコマンドを処理するために必要な最小限の状態」のみを保持します。一方、Read Model は表示に必要なすべてのフィールドを保持します。
|
|
2063
|
-
|
|
2064
|
-
| フィールド | ドメインモデル | Read Model | 理由 |
|
|
2065
|
-
|-----------|:-------------:|:----------:|------|
|
|
2066
|
-
| journalEntryId | O | O | 識別子として必要 |
|
|
2067
|
-
| status | O | O | 状態遷移の判定に必要 |
|
|
2068
|
-
| lines | O | O | 借方・貸方の検証に必要 |
|
|
2069
|
-
| approvedBy | X | O | 判定に不要、表示のみ |
|
|
2070
|
-
| approvedAt | X | O | 判定に不要、表示のみ |
|
|
2071
|
-
| fiscalPeriod | X | O | 判定に不要、表示のみ |
|
|
2072
|
-
| totalDebitAmount | X | O | 集計値、表示のみ |
|
|
2073
|
-
|
|
2074
|
-
---
|
|
2075
|
-
|
|
2076
|
-
## 第30章:REST API と統合テスト
|
|
2077
|
-
|
|
2078
|
-
### 30.1 REST Controller(Inbound Adapter)
|
|
2079
|
-
|
|
2080
|
-
<details>
|
|
2081
|
-
<summary>コード例: JournalsController.java</summary>
|
|
2082
|
-
|
|
2083
|
-
```java
|
|
2084
|
-
package com.example.accounting.journal.adapter.inbound.rest.journals;
|
|
2085
|
-
|
|
2086
|
-
import com.example.accounting.journal.adapter.inbound.rest.journals.protocol.*;
|
|
2087
|
-
import com.example.accounting.journal.adapter.outbound.persistence.mapper.JournalEntryMapper;
|
|
2088
|
-
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntryCommands.*;
|
|
2089
|
-
import io.swagger.v3.oas.annotations.Operation;
|
|
2090
|
-
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
2091
|
-
import jakarta.validation.Valid;
|
|
2092
|
-
import org.axonframework.commandhandling.gateway.CommandGateway;
|
|
2093
|
-
import org.springframework.http.HttpStatus;
|
|
2094
|
-
import org.springframework.http.ResponseEntity;
|
|
2095
|
-
import org.springframework.web.bind.annotation.*;
|
|
2096
|
-
|
|
2097
|
-
import java.util.UUID;
|
|
2098
|
-
import java.util.concurrent.CompletableFuture;
|
|
2099
|
-
|
|
2100
|
-
/**
|
|
2101
|
-
* JournalEntry REST Controller(Inbound Adapter)
|
|
2102
|
-
*/
|
|
2103
|
-
@RestController
|
|
2104
|
-
@RequestMapping("/api/journals")
|
|
2105
|
-
@Tag(name = "Journals", description = "Journal Entry management API")
|
|
2106
|
-
public class JournalsController {
|
|
2107
|
-
|
|
2108
|
-
private final CommandGateway commandGateway;
|
|
2109
|
-
private final JournalEntryMapper journalEntryMapper;
|
|
2110
|
-
|
|
2111
|
-
public JournalsController(CommandGateway commandGateway, JournalEntryMapper journalEntryMapper) {
|
|
2112
|
-
this.commandGateway = commandGateway;
|
|
2113
|
-
this.journalEntryMapper = journalEntryMapper;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// ========== Command Side (Write) ==========
|
|
2117
|
-
|
|
2118
|
-
@PostMapping
|
|
2119
|
-
@Operation(summary = "Create a new journal entry")
|
|
2120
|
-
public CompletableFuture<ResponseEntity<JournalEntryCreateResponse>> create(
|
|
2121
|
-
@Valid @RequestBody JournalEntryCreateRequest request
|
|
2122
|
-
) {
|
|
2123
|
-
var journalEntryId = UUID.randomUUID().toString();
|
|
2124
|
-
var lines = request.lines().stream()
|
|
2125
|
-
.map(line -> new JournalLineInput(
|
|
2126
|
-
line.accountCode(),
|
|
2127
|
-
line.accountName(),
|
|
2128
|
-
line.debitAmount(),
|
|
2129
|
-
line.creditAmount()
|
|
2130
|
-
))
|
|
2131
|
-
.toList();
|
|
2132
|
-
|
|
2133
|
-
var command = new CreateJournalEntryCommand(
|
|
2134
|
-
journalEntryId,
|
|
2135
|
-
request.transactionDate(),
|
|
2136
|
-
request.description(),
|
|
2137
|
-
lines,
|
|
2138
|
-
request.createdBy()
|
|
2139
|
-
);
|
|
2140
|
-
|
|
2141
|
-
return commandGateway.send(command)
|
|
2142
|
-
.thenApply(result -> ResponseEntity
|
|
2143
|
-
.status(HttpStatus.CREATED)
|
|
2144
|
-
.body(new JournalEntryCreateResponse(journalEntryId)));
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
@PostMapping("/{id}/submit")
|
|
2148
|
-
@Operation(summary = "Submit a journal entry for approval")
|
|
2149
|
-
public CompletableFuture<ResponseEntity<Void>> submit(
|
|
2150
|
-
@PathVariable String id,
|
|
2151
|
-
@Valid @RequestBody JournalEntrySubmitRequest request
|
|
2152
|
-
) {
|
|
2153
|
-
var command = new SubmitJournalEntryCommand(id, request.submittedBy());
|
|
2154
|
-
|
|
2155
|
-
return commandGateway.send(command)
|
|
2156
|
-
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
@PostMapping("/{id}/approve")
|
|
2160
|
-
@Operation(summary = "Approve a journal entry")
|
|
2161
|
-
public CompletableFuture<ResponseEntity<Void>> approve(
|
|
2162
|
-
@PathVariable String id,
|
|
2163
|
-
@Valid @RequestBody JournalEntryApproveRequest request
|
|
2164
|
-
) {
|
|
2165
|
-
var command = new ApproveJournalEntryCommand(id, request.approvedBy(), request.comment());
|
|
2166
|
-
|
|
2167
|
-
return commandGateway.send(command)
|
|
2168
|
-
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2169
|
-
}
|
|
2170
|
-
|
|
2171
|
-
@PostMapping("/{id}/reject")
|
|
2172
|
-
@Operation(summary = "Reject a journal entry")
|
|
2173
|
-
public CompletableFuture<ResponseEntity<Void>> reject(
|
|
2174
|
-
@PathVariable String id,
|
|
2175
|
-
@Valid @RequestBody JournalEntryRejectRequest request
|
|
2176
|
-
) {
|
|
2177
|
-
var command = new RejectJournalEntryCommand(id, request.rejectedBy(), request.reason());
|
|
2178
|
-
|
|
2179
|
-
return commandGateway.send(command)
|
|
2180
|
-
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
@PostMapping("/{id}/post")
|
|
2184
|
-
@Operation(summary = "Post a journal entry to the ledger")
|
|
2185
|
-
public CompletableFuture<ResponseEntity<Void>> post(
|
|
2186
|
-
@PathVariable String id,
|
|
2187
|
-
@Valid @RequestBody JournalEntryPostRequest request
|
|
2188
|
-
) {
|
|
2189
|
-
var command = new PostJournalEntryCommand(id, request.fiscalPeriod(), request.postedBy());
|
|
2190
|
-
|
|
2191
|
-
return commandGateway.send(command)
|
|
2192
|
-
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
@PostMapping("/{id}/cancel")
|
|
2196
|
-
@Operation(summary = "Cancel a journal entry")
|
|
2197
|
-
public CompletableFuture<ResponseEntity<Void>> cancel(
|
|
2198
|
-
@PathVariable String id,
|
|
2199
|
-
@Valid @RequestBody JournalEntryCancelRequest request
|
|
2200
|
-
) {
|
|
2201
|
-
var command = new CancelJournalEntryCommand(id, request.cancelledBy(), request.reason());
|
|
2202
|
-
|
|
2203
|
-
return commandGateway.send(command)
|
|
2204
|
-
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
// ========== Query Side (Read) ==========
|
|
2208
|
-
|
|
2209
|
-
@GetMapping("/{id}")
|
|
2210
|
-
@Operation(summary = "Get a journal entry by ID")
|
|
2211
|
-
public ResponseEntity<JournalEntryGetResponse> get(@PathVariable String id) {
|
|
2212
|
-
return journalEntryMapper.findById(id)
|
|
2213
|
-
.map(entity -> ResponseEntity.ok(JournalEntryGetResponse.from(entity)))
|
|
2214
|
-
.orElse(ResponseEntity.notFound().build());
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
@GetMapping
|
|
2218
|
-
@Operation(summary = "Get all journal entries")
|
|
2219
|
-
public ResponseEntity<JournalEntryListResponse> getAll() {
|
|
2220
|
-
var entities = journalEntryMapper.findAll();
|
|
2221
|
-
var items = entities.stream()
|
|
2222
|
-
.map(JournalEntryGetResponse::from)
|
|
2223
|
-
.toList();
|
|
2224
|
-
return ResponseEntity.ok(new JournalEntryListResponse(items));
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
@GetMapping("/status/{status}")
|
|
2228
|
-
@Operation(summary = "Get journal entries by status")
|
|
2229
|
-
public ResponseEntity<JournalEntryListResponse> getByStatus(@PathVariable String status) {
|
|
2230
|
-
var entities = journalEntryMapper.findByStatus(status);
|
|
2231
|
-
var items = entities.stream()
|
|
2232
|
-
.map(JournalEntryGetResponse::from)
|
|
2233
|
-
.toList();
|
|
2234
|
-
return ResponseEntity.ok(new JournalEntryListResponse(items));
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
```
|
|
2238
|
-
|
|
2239
|
-
</details>
|
|
2240
|
-
|
|
2241
|
-
---
|
|
2242
|
-
|
|
2243
|
-
### 30.2 API エンドポイント一覧
|
|
2244
|
-
|
|
2245
|
-
| メソッド | パス | 説明 | 種別 |
|
|
2246
|
-
|---------|------|------|------|
|
|
2247
|
-
| POST | `/api/journals` | 仕訳作成 | Command |
|
|
2248
|
-
| POST | `/api/journals/{id}/submit` | 承認申請 | Command |
|
|
2249
|
-
| POST | `/api/journals/{id}/approve` | 承認 | Command |
|
|
2250
|
-
| POST | `/api/journals/{id}/reject` | 却下 | Command |
|
|
2251
|
-
| POST | `/api/journals/{id}/post` | 転記 | Command |
|
|
2252
|
-
| POST | `/api/journals/{id}/cancel` | キャンセル | Command |
|
|
2253
|
-
| GET | `/api/journals` | 仕訳一覧取得 | Query |
|
|
2254
|
-
| GET | `/api/journals/{id}` | 仕訳詳細取得 | Query |
|
|
2255
|
-
| GET | `/api/journals/status/{status}` | ステータス別一覧 | Query |
|
|
2256
|
-
|
|
2257
|
-
---
|
|
2258
|
-
|
|
2259
|
-
### 30.3 Axon Test による単体テスト
|
|
2260
|
-
|
|
2261
|
-
<details>
|
|
2262
|
-
<summary>コード例: JournalEntryAggregateTest.java</summary>
|
|
2263
|
-
|
|
2264
|
-
```java
|
|
2265
|
-
package com.example.accounting.journal.application.aggregate;
|
|
2266
|
-
|
|
2267
|
-
import com.example.accounting.journal.api.events.*;
|
|
2268
|
-
import com.example.accounting.journal.api.events.JournalEntryCreatedEvent.JournalLineData;
|
|
2269
|
-
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntryCommands.*;
|
|
2270
|
-
import org.axonframework.test.aggregate.AggregateTestFixture;
|
|
2271
|
-
import org.axonframework.test.aggregate.FixtureConfiguration;
|
|
2272
|
-
import org.junit.jupiter.api.BeforeEach;
|
|
2273
|
-
import org.junit.jupiter.api.DisplayName;
|
|
2274
|
-
import org.junit.jupiter.api.Nested;
|
|
2275
|
-
import org.junit.jupiter.api.Test;
|
|
2276
|
-
|
|
2277
|
-
import java.math.BigDecimal;
|
|
2278
|
-
import java.time.LocalDate;
|
|
2279
|
-
import java.time.LocalDateTime;
|
|
2280
|
-
import java.util.List;
|
|
2281
|
-
|
|
2282
|
-
class JournalEntryAggregateTest {
|
|
2283
|
-
|
|
2284
|
-
private FixtureConfiguration<JournalEntryAggregateAdapter> fixture;
|
|
2285
|
-
|
|
2286
|
-
@BeforeEach
|
|
2287
|
-
void setUp() {
|
|
2288
|
-
fixture = new AggregateTestFixture<>(JournalEntryAggregateAdapter.class);
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
@Nested
|
|
2292
|
-
@DisplayName("仕訳作成")
|
|
2293
|
-
class CreateJournalEntry {
|
|
2294
|
-
|
|
2295
|
-
@Test
|
|
2296
|
-
@DisplayName("借方・貸方が一致する仕訳を作成できる")
|
|
2297
|
-
void shouldCreateBalancedJournalEntry() {
|
|
2298
|
-
var lines = List.of(
|
|
2299
|
-
new JournalLineInput("1000", "現金", BigDecimal.valueOf(10000), BigDecimal.ZERO),
|
|
2300
|
-
new JournalLineInput("4000", "売上", BigDecimal.ZERO, BigDecimal.valueOf(10000))
|
|
2301
|
-
);
|
|
2302
|
-
|
|
2303
|
-
var command = new CreateJournalEntryCommand(
|
|
2304
|
-
"je-1",
|
|
2305
|
-
LocalDate.of(2024, 1, 15),
|
|
2306
|
-
"売上計上",
|
|
2307
|
-
lines,
|
|
2308
|
-
"user-1"
|
|
2309
|
-
);
|
|
2310
|
-
|
|
2311
|
-
fixture.givenNoPriorActivity()
|
|
2312
|
-
.when(command)
|
|
2313
|
-
.expectSuccessfulHandlerExecution()
|
|
2314
|
-
.expectEventsMatching(events ->
|
|
2315
|
-
events.getPayload() instanceof JournalEntryCreatedEvent
|
|
2316
|
-
);
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
|
-
@Test
|
|
2320
|
-
@DisplayName("借方・貸方が一致しない場合はエラー")
|
|
2321
|
-
void shouldRejectUnbalancedJournalEntry() {
|
|
2322
|
-
var lines = List.of(
|
|
2323
|
-
new JournalLineInput("1000", "現金", BigDecimal.valueOf(10000), BigDecimal.ZERO),
|
|
2324
|
-
new JournalLineInput("4000", "売上", BigDecimal.ZERO, BigDecimal.valueOf(5000))
|
|
2325
|
-
);
|
|
2326
|
-
|
|
2327
|
-
var command = new CreateJournalEntryCommand(
|
|
2328
|
-
"je-1",
|
|
2329
|
-
LocalDate.of(2024, 1, 15),
|
|
2330
|
-
"不均衡な仕訳",
|
|
2331
|
-
lines,
|
|
2332
|
-
"user-1"
|
|
2333
|
-
);
|
|
2334
|
-
|
|
2335
|
-
fixture.givenNoPriorActivity()
|
|
2336
|
-
.when(command)
|
|
2337
|
-
.expectException(IllegalArgumentException.class);
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
@Nested
|
|
2342
|
-
@DisplayName("仕訳承認")
|
|
2343
|
-
class ApproveJournalEntry {
|
|
2344
|
-
|
|
2345
|
-
@Test
|
|
2346
|
-
@DisplayName("承認待ちの仕訳を承認できる")
|
|
2347
|
-
void shouldApprovePendingJournalEntry() {
|
|
2348
|
-
var createdEvent = createSampleCreatedEvent();
|
|
2349
|
-
var submittedEvent = new JournalEntrySubmittedEvent("je-1", "user-1", LocalDateTime.now());
|
|
2350
|
-
|
|
2351
|
-
fixture.given(createdEvent, submittedEvent)
|
|
2352
|
-
.when(new ApproveJournalEntryCommand("je-1", "approver-1", "承認します"))
|
|
2353
|
-
.expectSuccessfulHandlerExecution()
|
|
2354
|
-
.expectEventsMatching(events ->
|
|
2355
|
-
events.getPayload() instanceof JournalEntryApprovedEvent
|
|
2356
|
-
);
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
@Test
|
|
2360
|
-
@DisplayName("下書きの仕訳は承認できない")
|
|
2361
|
-
void shouldNotApproveDraftJournalEntry() {
|
|
2362
|
-
var createdEvent = createSampleCreatedEvent();
|
|
2363
|
-
|
|
2364
|
-
fixture.given(createdEvent)
|
|
2365
|
-
.when(new ApproveJournalEntryCommand("je-1", "approver-1", "承認します"))
|
|
2366
|
-
.expectException(IllegalStateException.class);
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
|
|
2370
|
-
@Nested
|
|
2371
|
-
@DisplayName("仕訳転記")
|
|
2372
|
-
class PostJournalEntry {
|
|
2373
|
-
|
|
2374
|
-
@Test
|
|
2375
|
-
@DisplayName("承認済みの仕訳を転記できる")
|
|
2376
|
-
void shouldPostApprovedJournalEntry() {
|
|
2377
|
-
var createdEvent = createSampleCreatedEvent();
|
|
2378
|
-
var submittedEvent = new JournalEntrySubmittedEvent("je-1", "user-1", LocalDateTime.now());
|
|
2379
|
-
var approvedEvent = new JournalEntryApprovedEvent("je-1", "approver-1", "OK", LocalDateTime.now());
|
|
2380
|
-
|
|
2381
|
-
fixture.given(createdEvent, submittedEvent, approvedEvent)
|
|
2382
|
-
.when(new PostJournalEntryCommand("je-1", "2024-01", "poster-1"))
|
|
2383
|
-
.expectSuccessfulHandlerExecution()
|
|
2384
|
-
.expectEventsMatching(events ->
|
|
2385
|
-
events.getPayload() instanceof JournalEntryPostedEvent
|
|
2386
|
-
);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
|
|
2390
|
-
private JournalEntryCreatedEvent createSampleCreatedEvent() {
|
|
2391
|
-
var lines = List.of(
|
|
2392
|
-
new JournalLineData("1000", "現金", BigDecimal.valueOf(10000), BigDecimal.ZERO),
|
|
2393
|
-
new JournalLineData("4000", "売上", BigDecimal.ZERO, BigDecimal.valueOf(10000))
|
|
2394
|
-
);
|
|
2395
|
-
|
|
2396
|
-
return new JournalEntryCreatedEvent(
|
|
2397
|
-
"je-1",
|
|
2398
|
-
"JE-00000001",
|
|
2399
|
-
LocalDate.of(2024, 1, 15),
|
|
2400
|
-
"売上計上",
|
|
2401
|
-
lines,
|
|
2402
|
-
"user-1",
|
|
2403
|
-
LocalDateTime.now()
|
|
2404
|
-
);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
```
|
|
2408
|
-
|
|
2409
|
-
</details>
|
|
2410
|
-
|
|
2411
|
-
---
|
|
2412
|
-
|
|
2413
|
-
## Part 5-F のまとめ
|
|
2414
|
-
|
|
2415
|
-
### 実装した機能一覧
|
|
2416
|
-
|
|
2417
|
-
| 章 | 内容 | 主要技術 |
|
|
2418
|
-
|---|---|---|
|
|
2419
|
-
| **第26章: 基礎** | CQRS/ES アーキテクチャの基礎 | Axon Framework, Event Sourcing |
|
|
2420
|
-
| **第27章: ドメイン設計** | イベント、コマンド、ドメインモデル | sealed interface, record |
|
|
2421
|
-
| **第28章: Aggregate/Policy** | Aggregate Adapter, Choreography | @Aggregate, @EventHandler |
|
|
2422
|
-
| **第29章: Projection** | Read Model の更新、最小限フィールド | @EventHandler, MyBatis |
|
|
2423
|
-
| **第30章: REST API** | Inbound Adapter, テスト | CommandGateway, Axon Test |
|
|
2424
|
-
|
|
2425
|
-
### アーキテクチャの特徴
|
|
2426
|
-
|
|
2427
|
-
```plantuml
|
|
2428
|
-
@startuml cqrs_summary
|
|
2429
|
-
skinparam backgroundColor #FEFEFE
|
|
2430
|
-
|
|
2431
|
-
title CQRS/ES アーキテクチャ(財務会計システム)
|
|
2432
|
-
|
|
2433
|
-
package "Command Side" {
|
|
2434
|
-
[REST Controller] as RC1
|
|
2435
|
-
[CommandGateway] as CG
|
|
2436
|
-
[Aggregate Adapter] as AA
|
|
2437
|
-
[Domain Model] as DM
|
|
2438
|
-
[Event Store] as ES
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
package "Query Side" {
|
|
2442
|
-
[REST Controller] as RC2
|
|
2443
|
-
[MyBatis Mapper] as MB
|
|
2444
|
-
[Read Model DB] as DB
|
|
2445
|
-
[Projection] as P
|
|
2446
|
-
}
|
|
2447
|
-
|
|
2448
|
-
RC1 --> CG : send(command)
|
|
2449
|
-
CG --> AA : @CommandHandler
|
|
2450
|
-
AA --> DM : ビジネスロジック
|
|
2451
|
-
DM --> AA : event
|
|
2452
|
-
AA --> ES : apply(event)
|
|
2453
|
-
ES --> P : @EventHandler
|
|
2454
|
-
P --> MB : insert/update
|
|
2455
|
-
MB --> DB : SQL
|
|
2456
|
-
|
|
2457
|
-
RC2 --> MB : query
|
|
2458
|
-
MB --> DB : SELECT
|
|
2459
|
-
|
|
2460
|
-
note right of DM
|
|
2461
|
-
純粋なドメインモデル
|
|
2462
|
-
Axon に依存しない
|
|
2463
|
-
end note
|
|
2464
|
-
|
|
2465
|
-
note right of P
|
|
2466
|
-
イベントを購読して
|
|
2467
|
-
MyBatis 経由で
|
|
2468
|
-
Read Model を更新
|
|
2469
|
-
end note
|
|
2470
|
-
|
|
2471
|
-
@enduml
|
|
2472
|
-
```
|
|
2473
|
-
|
|
2474
|
-
### 設計上の特徴
|
|
2475
|
-
|
|
2476
|
-
| 特徴 | 説明 |
|
|
2477
|
-
|------|------|
|
|
2478
|
-
| **ドメインモデルとフレームワークの分離** | `JournalEntry.java` は純粋なドメインモデル、`JournalEntryAggregateAdapter.java` は Axon 用アダプター |
|
|
2479
|
-
| **Choreography による疎結合** | 各 Context は独立して動作、イベントを介した非同期連携 |
|
|
2480
|
-
| **sealed interface による型安全性** | イベントの網羅性チェック、switch 式でコンパイル時検証 |
|
|
2481
|
-
| **record によるイミュータブル設計** | 状態変更は常に新しいインスタンスを生成 |
|
|
2482
|
-
| **最小限フィールドの原則** | ドメインモデルは状態遷移判定に必要な最小限のみ保持 |
|
|
2483
|
-
| **MyBatis による柔軟な SQL 制御** | Read Model の更新に MyBatis Mapper を使用 |
|
|
2484
|
-
| **ビジネスルールの集約** | 借方・貸方一致検証をドメインモデル内で実施 |
|
|
2485
|
-
|
|
2486
|
-
### 財務会計システムでの CQRS/ES の意義
|
|
2487
|
-
|
|
2488
|
-
| 観点 | 説明 |
|
|
2489
|
-
|------|------|
|
|
2490
|
-
| **監査証跡** | すべての仕訳変更履歴を完全に保持(SOX 法対応) |
|
|
2491
|
-
| **訂正仕訳** | 過去の仕訳を直接変更せず、訂正仕訳として追記 |
|
|
2492
|
-
| **期末決算** | 任意の時点での残高を再計算可能 |
|
|
2493
|
-
| **ワークフロー** | 仕訳の作成→承認→転記の状態管理 |
|
|
2494
|
-
| **内部統制** | 誰が・いつ・何を承認したかの記録 |
|
|
2495
|
-
|
|
2496
|
-
### 技術スタック
|
|
2497
|
-
|
|
2498
|
-
| カテゴリ | 技術 |
|
|
2499
|
-
|---------|------|
|
|
2500
|
-
| **言語** | Java 21 |
|
|
2501
|
-
| **フレームワーク** | Spring Boot 3.4.1 |
|
|
2502
|
-
| **CQRS/ES** | Axon Framework 4.10.3 |
|
|
2503
|
-
| **ORM** | MyBatis 3.0.4 |
|
|
2504
|
-
| **データベース** | H2 / PostgreSQL |
|
|
2505
|
-
| **テスト** | JUnit 5, Axon Test |
|
|
2506
|
-
|
|
2507
|
-
CQRS/ES は財務会計システムに特に適しています。すべての取引履歴を完全に保持し、監査要件に対応できるだけでなく、仕訳の承認ワークフローやビジネスルール(借方・貸方の一致)を明確に表現できます。MyBatis を使用することで、複雑な財務レポート(試算表、財務諸表など)のクエリにも柔軟に対応できます。
|
|
1
|
+
# 実践データベース設計:財務会計システム 研究 5 - Axon CQRS/ES の実装
|
|
2
|
+
|
|
3
|
+
## はじめに
|
|
4
|
+
|
|
5
|
+
本研究では、REST API(第21章)、gRPC(研究 3)、GraphQL(研究 4)とは異なるアプローチとして、**CQRS(Command Query Responsibility Segregation)** と **Event Sourcing** による財務会計システムを実装します。Axon Framework を使用し、コマンド(書き込み)とクエリ(読み取り)を分離し、すべての状態変更をイベントとして記録するアーキテクチャを構築します。
|
|
6
|
+
|
|
7
|
+
研究 1 で構築したヘキサゴナルアーキテクチャの考え方を踏襲しつつ、**ドメインモデルをフレームワークから分離**し、Axon Aggregate Adapter を介して連携させます。Read Model の永続化には **MyBatis** を使用します。
|
|
8
|
+
|
|
9
|
+
財務会計システムでは、**仕訳(Journal Entry)** の作成から承認、転記までのワークフローを CQRS/ES で実装し、すべての取引履歴を完全に追跡可能な形で保存します。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 第26章:Axon CQRS/ES アーキテクチャの基礎
|
|
14
|
+
|
|
15
|
+
### 26.1 CQRS とは
|
|
16
|
+
|
|
17
|
+
CQRS(Command Query Responsibility Segregation)は、コマンド(書き込み)とクエリ(読み取り)の責務を分離するアーキテクチャパターンです。従来の CRUD 操作を単一のモデルで処理するのではなく、書き込み用のモデルと読み取り用のモデルを分けることで、それぞれに最適化された設計が可能になります。
|
|
18
|
+
|
|
19
|
+
```plantuml
|
|
20
|
+
@startuml cqrs_architecture
|
|
21
|
+
skinparam componentStyle rectangle
|
|
22
|
+
skinparam backgroundColor #FEFEFE
|
|
23
|
+
|
|
24
|
+
title CQRS アーキテクチャ(財務会計システム)
|
|
25
|
+
|
|
26
|
+
package "Command Side (Write)" {
|
|
27
|
+
[REST Controller] --> [CommandGateway]
|
|
28
|
+
[CommandGateway] --> [Aggregate]
|
|
29
|
+
[Aggregate] --> [Event Store]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
package "Query Side (Read)" {
|
|
33
|
+
[Event Store] --> [Projection]
|
|
34
|
+
[Projection] --> [Read Model DB]
|
|
35
|
+
[REST Controller] --> [MyBatis Mapper]
|
|
36
|
+
[MyBatis Mapper] --> [Read Model DB]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@enduml
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**CQRS の利点:**
|
|
43
|
+
|
|
44
|
+
| 観点 | 説明 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| **スケーラビリティ** | 読み取りと書き込みを独立してスケール可能 |
|
|
47
|
+
| **パフォーマンス** | 読み取りに最適化されたモデルで高速クエリ |
|
|
48
|
+
| **複雑性の分離** | 書き込みロジックと読み取りロジックを独立して開発 |
|
|
49
|
+
| **監査対応** | Event Sourcing と組み合わせて完全な履歴を保持 |
|
|
50
|
+
|
|
51
|
+
**財務会計システムでの CQRS の意義:**
|
|
52
|
+
|
|
53
|
+
- **監査証跡**: すべての仕訳変更履歴を完全に保持
|
|
54
|
+
- **複雑なクエリ**: 試算表、財務諸表など多様なビューを効率的に生成
|
|
55
|
+
- **データ整合性**: イベントベースで一貫性を保証
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### 26.2 Event Sourcing とは
|
|
60
|
+
|
|
61
|
+
Event Sourcing は、アプリケーションの状態をイベントの連続として保存するパターンです。現在の状態を直接保存するのではなく、状態変更を表すイベントを記録し、必要に応じてイベントを再生して現在の状態を再構築します。
|
|
62
|
+
|
|
63
|
+
```plantuml
|
|
64
|
+
@startuml event_sourcing
|
|
65
|
+
skinparam componentStyle rectangle
|
|
66
|
+
skinparam backgroundColor #FEFEFE
|
|
67
|
+
|
|
68
|
+
[Command] --> [Aggregate] : 1. コマンド受信
|
|
69
|
+
[Aggregate] --> [Domain Model] : 2. ビジネスロジック実行
|
|
70
|
+
[Domain Model] --> [Event] : 3. イベント生成
|
|
71
|
+
[Event] --> [Event Store] : 4. イベント永続化
|
|
72
|
+
[Event Store] --> [Aggregate] : 5. イベント再生(状態復元)
|
|
73
|
+
|
|
74
|
+
@enduml
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Event Sourcing の利点:**
|
|
78
|
+
|
|
79
|
+
| 観点 | 説明 |
|
|
80
|
+
|------|------|
|
|
81
|
+
| **完全な履歴** | すべての状態変更が記録される |
|
|
82
|
+
| **監査証跡** | いつ、誰が、何を変更したかが明確 |
|
|
83
|
+
| **時間旅行** | 過去の任意の時点の状態を再構築可能 |
|
|
84
|
+
| **イベント駆動** | 他システムとの連携が容易 |
|
|
85
|
+
| **デバッグ** | 問題発生時にイベントを追跡可能 |
|
|
86
|
+
|
|
87
|
+
**財務会計システムでの Event Sourcing の意義:**
|
|
88
|
+
|
|
89
|
+
- **訂正仕訳の追跡**: 修正履歴を完全に保持
|
|
90
|
+
- **期末決算処理**: 任意の時点での残高を再計算可能
|
|
91
|
+
- **内部統制**: SOX 法対応の監査証跡を自動生成
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
### 26.3 ヘキサゴナルアーキテクチャとの統合
|
|
96
|
+
|
|
97
|
+
本実装では、ヘキサゴナルアーキテクチャ(Ports & Adapters)を採用し、ビジネスロジックを外部依存から分離します。
|
|
98
|
+
|
|
99
|
+
```plantuml
|
|
100
|
+
@startuml hexagonal_cqrs
|
|
101
|
+
!define RECTANGLE class
|
|
102
|
+
skinparam backgroundColor #FEFEFE
|
|
103
|
+
|
|
104
|
+
package "Hexagonal Architecture (CQRS/ES 版)" {
|
|
105
|
+
|
|
106
|
+
package "Inbound Adapters" {
|
|
107
|
+
[REST Controller]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
package "Application Core" {
|
|
111
|
+
package "Application Layer" {
|
|
112
|
+
[Aggregate Adapter]
|
|
113
|
+
[Projection]
|
|
114
|
+
[Policy Handler]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
package "Domain Layer" {
|
|
118
|
+
[Domain Model]
|
|
119
|
+
[Commands]
|
|
120
|
+
[Value Objects]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
package "API Layer" {
|
|
124
|
+
[Events]
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
package "Outbound Adapters" {
|
|
129
|
+
[MyBatis Mapper]
|
|
130
|
+
[Event Store]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[REST Controller] --> [Aggregate Adapter]
|
|
135
|
+
[Aggregate Adapter] --> [Domain Model]
|
|
136
|
+
[Projection] --> [MyBatis Mapper]
|
|
137
|
+
|
|
138
|
+
note top of [Domain Model]
|
|
139
|
+
純粋なドメインモデル
|
|
140
|
+
Axon Framework に依存しない
|
|
141
|
+
ビジネスロジックのみ
|
|
142
|
+
end note
|
|
143
|
+
|
|
144
|
+
note left of [Aggregate Adapter]
|
|
145
|
+
Axon 用アダプター
|
|
146
|
+
@Aggregate, @CommandHandler
|
|
147
|
+
フレームワーク依存を吸収
|
|
148
|
+
end note
|
|
149
|
+
|
|
150
|
+
@enduml
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**設計原則:**
|
|
154
|
+
|
|
155
|
+
1. **ドメインモデルの純粋性**: ドメインモデルは Axon に依存しない純粋な Java コード
|
|
156
|
+
2. **Aggregate Adapter**: Axon Framework 用のアダプターを Application Layer に配置
|
|
157
|
+
3. **イベントは公開 API**: イベントは他の Context から参照される公開 API として定義
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### 26.4 技術スタック
|
|
162
|
+
|
|
163
|
+
| カテゴリ | 技術 | バージョン |
|
|
164
|
+
|---------|------|-----------|
|
|
165
|
+
| 言語 | Java | 21 |
|
|
166
|
+
| フレームワーク | Spring Boot | 3.4.1 |
|
|
167
|
+
| CQRS/ES | Axon Framework | 4.10.3 |
|
|
168
|
+
| ORM | MyBatis | 3.0.4 |
|
|
169
|
+
| API ドキュメント | SpringDoc OpenAPI | 2.7.0 |
|
|
170
|
+
| データベース | H2 (開発) / PostgreSQL (本番) | - |
|
|
171
|
+
|
|
172
|
+
#### build.gradle.kts
|
|
173
|
+
|
|
174
|
+
<details>
|
|
175
|
+
<summary>コード例: build.gradle.kts</summary>
|
|
176
|
+
|
|
177
|
+
```kotlin
|
|
178
|
+
dependencies {
|
|
179
|
+
// Spring Boot
|
|
180
|
+
implementation("org.springframework.boot:spring-boot-starter-web")
|
|
181
|
+
implementation("org.springframework.boot:spring-boot-starter-validation")
|
|
182
|
+
|
|
183
|
+
// Axon Framework
|
|
184
|
+
implementation("org.axonframework:axon-spring-boot-starter:4.10.3") {
|
|
185
|
+
exclude(group = "org.axonframework", module = "axon-server-connector")
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// MyBatis
|
|
189
|
+
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4")
|
|
190
|
+
|
|
191
|
+
// OpenAPI
|
|
192
|
+
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
|
|
193
|
+
|
|
194
|
+
// Database
|
|
195
|
+
runtimeOnly("com.h2database:h2")
|
|
196
|
+
runtimeOnly("org.postgresql:postgresql")
|
|
197
|
+
|
|
198
|
+
// Test
|
|
199
|
+
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
200
|
+
testImplementation("org.axonframework:axon-test:4.10.3")
|
|
201
|
+
testImplementation("org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.4")
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
</details>
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### 26.5 ディレクトリ構造
|
|
210
|
+
|
|
211
|
+
<details>
|
|
212
|
+
<summary>コード例: ディレクトリ構成</summary>
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
src/main/java/com/example/accounting/
|
|
216
|
+
├── app/ # アプリケーション共通
|
|
217
|
+
│ └── RootController.java
|
|
218
|
+
├── config/ # 設定クラス
|
|
219
|
+
│ ├── AxonConfig.java
|
|
220
|
+
│ ├── MyBatisConfig.java
|
|
221
|
+
│ └── OpenApiConfig.java
|
|
222
|
+
├── journal/ # Journal Bounded Context(仕訳)
|
|
223
|
+
│ ├── api/
|
|
224
|
+
│ │ └── events/ # 公開イベント API
|
|
225
|
+
│ │ ├── JournalEntryEvent.java # sealed interface
|
|
226
|
+
│ │ ├── JournalEntryCreatedEvent.java
|
|
227
|
+
│ │ ├── JournalEntryApprovedEvent.java
|
|
228
|
+
│ │ └── JournalEntryPostedEvent.java
|
|
229
|
+
│ ├── adapter/
|
|
230
|
+
│ │ ├── inbound/rest/journals/ # Inbound Adapter (REST)
|
|
231
|
+
│ │ │ ├── JournalsController.java
|
|
232
|
+
│ │ │ └── protocol/ # Request/Response DTO
|
|
233
|
+
│ │ └── outbound/persistence/ # Outbound Adapter (MyBatis)
|
|
234
|
+
│ │ ├── entity/
|
|
235
|
+
│ │ │ └── JournalEntryEntity.java
|
|
236
|
+
│ │ └── mapper/
|
|
237
|
+
│ │ └── JournalEntryMapper.java
|
|
238
|
+
│ ├── application/
|
|
239
|
+
│ │ ├── aggregate/ # Axon Aggregate Adapter
|
|
240
|
+
│ │ │ └── JournalEntryAggregateAdapter.java
|
|
241
|
+
│ │ ├── policy/ # イベントハンドラー(Choreography)
|
|
242
|
+
│ │ │ └── AccountEventHandler.java
|
|
243
|
+
│ │ └── query/ # Projection
|
|
244
|
+
│ │ └── JournalEntryProjection.java
|
|
245
|
+
│ └── domain/
|
|
246
|
+
│ └── model/aggregate/journal/ # 純粋なドメインモデル
|
|
247
|
+
│ ├── JournalEntry.java
|
|
248
|
+
│ ├── JournalEntryCommands.java
|
|
249
|
+
│ ├── JournalEntryStatus.java
|
|
250
|
+
│ └── JournalLine.java
|
|
251
|
+
└── account/ # Account Bounded Context(勘定科目)
|
|
252
|
+
├── api/events/
|
|
253
|
+
│ ├── AccountEvent.java
|
|
254
|
+
│ ├── AccountBalanceUpdatedEvent.java
|
|
255
|
+
│ └── AccountClosedEvent.java
|
|
256
|
+
├── application/
|
|
257
|
+
│ ├── aggregate/
|
|
258
|
+
│ │ └── AccountAggregateAdapter.java
|
|
259
|
+
│ └── policy/
|
|
260
|
+
│ └── JournalEventHandler.java
|
|
261
|
+
└── domain/model/aggregate/account/
|
|
262
|
+
├── Account.java
|
|
263
|
+
└── AccountCommands.java
|
|
264
|
+
|
|
265
|
+
src/main/resources/
|
|
266
|
+
├── application.yml
|
|
267
|
+
├── schema.sql # Read Model スキーマ
|
|
268
|
+
└── mapper/
|
|
269
|
+
└── JournalEntryMapper.xml # MyBatis マッパー XML
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
</details>
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
### 26.6 Axon 設定クラス
|
|
277
|
+
|
|
278
|
+
<details>
|
|
279
|
+
<summary>コード例: AxonConfig.java</summary>
|
|
280
|
+
|
|
281
|
+
```java
|
|
282
|
+
package com.example.accounting.config;
|
|
283
|
+
|
|
284
|
+
import org.axonframework.eventsourcing.eventstore.EmbeddedEventStore;
|
|
285
|
+
import org.axonframework.eventsourcing.eventstore.EventStorageEngine;
|
|
286
|
+
import org.axonframework.eventsourcing.eventstore.EventStore;
|
|
287
|
+
import org.axonframework.eventsourcing.eventstore.jdbc.JdbcEventStorageEngine;
|
|
288
|
+
import org.axonframework.serialization.Serializer;
|
|
289
|
+
import org.axonframework.serialization.json.JacksonSerializer;
|
|
290
|
+
import org.springframework.context.annotation.Bean;
|
|
291
|
+
import org.springframework.context.annotation.Configuration;
|
|
292
|
+
|
|
293
|
+
import javax.sql.DataSource;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Axon Framework 設定
|
|
297
|
+
*/
|
|
298
|
+
@Configuration
|
|
299
|
+
public class AxonConfig {
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* JDBC ベースの Event Storage Engine
|
|
303
|
+
*/
|
|
304
|
+
@Bean
|
|
305
|
+
public EventStorageEngine eventStorageEngine(
|
|
306
|
+
DataSource dataSource,
|
|
307
|
+
Serializer serializer) {
|
|
308
|
+
return JdbcEventStorageEngine.builder()
|
|
309
|
+
.snapshotSerializer(serializer)
|
|
310
|
+
.eventSerializer(serializer)
|
|
311
|
+
.dataSource(dataSource)
|
|
312
|
+
.build();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Event Store
|
|
317
|
+
*/
|
|
318
|
+
@Bean
|
|
319
|
+
public EventStore eventStore(EventStorageEngine eventStorageEngine) {
|
|
320
|
+
return EmbeddedEventStore.builder()
|
|
321
|
+
.storageEngine(eventStorageEngine)
|
|
322
|
+
.build();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* JSON シリアライザー
|
|
327
|
+
*/
|
|
328
|
+
@Bean
|
|
329
|
+
public Serializer eventSerializer() {
|
|
330
|
+
return JacksonSerializer.defaultSerializer();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
</details>
|
|
336
|
+
|
|
337
|
+
### 26.7 MyBatis 設定クラス
|
|
338
|
+
|
|
339
|
+
<details>
|
|
340
|
+
<summary>コード例: MyBatisConfig.java</summary>
|
|
341
|
+
|
|
342
|
+
```java
|
|
343
|
+
package com.example.accounting.config;
|
|
344
|
+
|
|
345
|
+
import org.apache.ibatis.session.SqlSessionFactory;
|
|
346
|
+
import org.mybatis.spring.SqlSessionFactoryBean;
|
|
347
|
+
import org.mybatis.spring.annotation.MapperScan;
|
|
348
|
+
import org.springframework.context.annotation.Bean;
|
|
349
|
+
import org.springframework.context.annotation.Configuration;
|
|
350
|
+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
|
351
|
+
|
|
352
|
+
import javax.sql.DataSource;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* MyBatis 設定
|
|
356
|
+
*/
|
|
357
|
+
@Configuration
|
|
358
|
+
@MapperScan(basePackages = {
|
|
359
|
+
"com.example.accounting.journal.adapter.outbound.persistence.mapper",
|
|
360
|
+
"com.example.accounting.account.adapter.outbound.persistence.mapper"
|
|
361
|
+
})
|
|
362
|
+
public class MyBatisConfig {
|
|
363
|
+
|
|
364
|
+
@Bean
|
|
365
|
+
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
|
366
|
+
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
|
|
367
|
+
factoryBean.setDataSource(dataSource);
|
|
368
|
+
factoryBean.setMapperLocations(
|
|
369
|
+
new PathMatchingResourcePatternResolver()
|
|
370
|
+
.getResources("classpath:mapper/**/*.xml")
|
|
371
|
+
);
|
|
372
|
+
factoryBean.setTypeAliasesPackage(
|
|
373
|
+
"com.example.accounting.journal.adapter.outbound.persistence.entity," +
|
|
374
|
+
"com.example.accounting.account.adapter.outbound.persistence.entity"
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
org.apache.ibatis.session.Configuration configuration =
|
|
378
|
+
new org.apache.ibatis.session.Configuration();
|
|
379
|
+
configuration.setMapUnderscoreToCamelCase(true);
|
|
380
|
+
factoryBean.setConfiguration(configuration);
|
|
381
|
+
|
|
382
|
+
return factoryBean.getObject();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
</details>
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 第27章:ドメインモデルとイベント設計
|
|
392
|
+
|
|
393
|
+
### 27.1 Journal Bounded Context
|
|
394
|
+
|
|
395
|
+
仕訳の作成から転記完了までのライフサイクルを管理します。
|
|
396
|
+
|
|
397
|
+
#### 状態遷移図
|
|
398
|
+
|
|
399
|
+
```plantuml
|
|
400
|
+
@startuml journal_state
|
|
401
|
+
skinparam backgroundColor #FEFEFE
|
|
402
|
+
|
|
403
|
+
title 仕訳(Journal Entry)の状態遷移
|
|
404
|
+
|
|
405
|
+
[*] --> DRAFT : JournalEntryCreated
|
|
406
|
+
DRAFT --> PENDING_APPROVAL : JournalEntrySubmitted
|
|
407
|
+
DRAFT --> CANCELLED : JournalEntryCancelled
|
|
408
|
+
PENDING_APPROVAL --> APPROVED : JournalEntryApproved
|
|
409
|
+
PENDING_APPROVAL --> REJECTED : JournalEntryRejected
|
|
410
|
+
PENDING_APPROVAL --> CANCELLED : JournalEntryCancelled
|
|
411
|
+
APPROVED --> POSTED : JournalEntryPosted
|
|
412
|
+
REJECTED --> DRAFT : JournalEntryRevised
|
|
413
|
+
|
|
414
|
+
@enduml
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### イベント一覧
|
|
418
|
+
|
|
419
|
+
| イベント | 説明 |
|
|
420
|
+
|---------|------|
|
|
421
|
+
| `JournalEntryCreatedEvent` | 仕訳が作成された |
|
|
422
|
+
| `JournalEntrySubmittedEvent` | 仕訳が承認申請された |
|
|
423
|
+
| `JournalEntryApprovedEvent` | 仕訳が承認された |
|
|
424
|
+
| `JournalEntryRejectedEvent` | 仕訳が却下された |
|
|
425
|
+
| `JournalEntryRevisedEvent` | 却下された仕訳が修正された |
|
|
426
|
+
| `JournalEntryPostedEvent` | 仕訳が転記された(元帳に反映) |
|
|
427
|
+
| `JournalEntryCancelledEvent` | 仕訳がキャンセルされた |
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
### 27.2 イベント定義(API Layer)
|
|
432
|
+
|
|
433
|
+
#### sealed interface によるイベントの型安全性
|
|
434
|
+
|
|
435
|
+
<details>
|
|
436
|
+
<summary>コード例: JournalEntryEvent.java</summary>
|
|
437
|
+
|
|
438
|
+
```java
|
|
439
|
+
package com.example.accounting.journal.api.events;
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* JournalEntry イベントの sealed interface
|
|
443
|
+
* すべての JournalEntry イベントの親インターフェース
|
|
444
|
+
*/
|
|
445
|
+
public sealed interface JournalEntryEvent permits
|
|
446
|
+
JournalEntryCreatedEvent,
|
|
447
|
+
JournalEntrySubmittedEvent,
|
|
448
|
+
JournalEntryApprovedEvent,
|
|
449
|
+
JournalEntryRejectedEvent,
|
|
450
|
+
JournalEntryRevisedEvent,
|
|
451
|
+
JournalEntryPostedEvent,
|
|
452
|
+
JournalEntryCancelledEvent {
|
|
453
|
+
|
|
454
|
+
String journalEntryId();
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
</details>
|
|
459
|
+
|
|
460
|
+
**sealed interface の利点:**
|
|
461
|
+
|
|
462
|
+
- **網羅性チェック**: switch 式でコンパイル時に全ケースをチェック
|
|
463
|
+
- **型安全性**: 許可されたクラスのみが実装可能
|
|
464
|
+
- **ドキュメント**: 存在するイベントが一目でわかる
|
|
465
|
+
|
|
466
|
+
#### イベント record の実装
|
|
467
|
+
|
|
468
|
+
<details>
|
|
469
|
+
<summary>コード例: JournalEntryCreatedEvent.java</summary>
|
|
470
|
+
|
|
471
|
+
```java
|
|
472
|
+
package com.example.accounting.journal.api.events;
|
|
473
|
+
|
|
474
|
+
import java.math.BigDecimal;
|
|
475
|
+
import java.time.LocalDate;
|
|
476
|
+
import java.time.LocalDateTime;
|
|
477
|
+
import java.util.List;
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 仕訳作成イベント
|
|
481
|
+
*/
|
|
482
|
+
public record JournalEntryCreatedEvent(
|
|
483
|
+
String journalEntryId,
|
|
484
|
+
String journalNumber,
|
|
485
|
+
LocalDate transactionDate,
|
|
486
|
+
String description,
|
|
487
|
+
List<JournalLineData> lines,
|
|
488
|
+
String createdBy,
|
|
489
|
+
LocalDateTime createdAt
|
|
490
|
+
) implements JournalEntryEvent {
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 仕訳明細データ
|
|
494
|
+
*/
|
|
495
|
+
public record JournalLineData(
|
|
496
|
+
String accountCode,
|
|
497
|
+
String accountName,
|
|
498
|
+
BigDecimal debitAmount,
|
|
499
|
+
BigDecimal creditAmount
|
|
500
|
+
) {
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
</details>
|
|
506
|
+
|
|
507
|
+
<details>
|
|
508
|
+
<summary>コード例: JournalEntryApprovedEvent.java</summary>
|
|
509
|
+
|
|
510
|
+
```java
|
|
511
|
+
package com.example.accounting.journal.api.events;
|
|
512
|
+
|
|
513
|
+
import java.time.LocalDateTime;
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* 仕訳承認イベント
|
|
517
|
+
*/
|
|
518
|
+
public record JournalEntryApprovedEvent(
|
|
519
|
+
String journalEntryId,
|
|
520
|
+
String approvedBy,
|
|
521
|
+
String comment,
|
|
522
|
+
LocalDateTime approvedAt
|
|
523
|
+
) implements JournalEntryEvent {
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
</details>
|
|
528
|
+
|
|
529
|
+
<details>
|
|
530
|
+
<summary>コード例: JournalEntryPostedEvent.java</summary>
|
|
531
|
+
|
|
532
|
+
```java
|
|
533
|
+
package com.example.accounting.journal.api.events;
|
|
534
|
+
|
|
535
|
+
import java.time.LocalDateTime;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 仕訳転記イベント
|
|
539
|
+
*/
|
|
540
|
+
public record JournalEntryPostedEvent(
|
|
541
|
+
String journalEntryId,
|
|
542
|
+
String fiscalPeriod,
|
|
543
|
+
String postedBy,
|
|
544
|
+
LocalDateTime postedAt
|
|
545
|
+
) implements JournalEntryEvent {
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
</details>
|
|
550
|
+
|
|
551
|
+
<details>
|
|
552
|
+
<summary>コード例: JournalEntryCancelledEvent.java</summary>
|
|
553
|
+
|
|
554
|
+
```java
|
|
555
|
+
package com.example.accounting.journal.api.events;
|
|
556
|
+
|
|
557
|
+
import java.time.LocalDateTime;
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* 仕訳キャンセルイベント
|
|
561
|
+
*/
|
|
562
|
+
public record JournalEntryCancelledEvent(
|
|
563
|
+
String journalEntryId,
|
|
564
|
+
String cancelledBy,
|
|
565
|
+
String reason,
|
|
566
|
+
LocalDateTime cancelledAt
|
|
567
|
+
) implements JournalEntryEvent {
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
</details>
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
### 27.3 コマンド定義(Domain Layer)
|
|
576
|
+
|
|
577
|
+
<details>
|
|
578
|
+
<summary>コード例: JournalEntryCommands.java</summary>
|
|
579
|
+
|
|
580
|
+
```java
|
|
581
|
+
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
582
|
+
|
|
583
|
+
import org.axonframework.modelling.command.TargetAggregateIdentifier;
|
|
584
|
+
import java.math.BigDecimal;
|
|
585
|
+
import java.time.LocalDate;
|
|
586
|
+
import java.util.List;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* JournalEntry 集約へのコマンド定義
|
|
590
|
+
*/
|
|
591
|
+
public final class JournalEntryCommands {
|
|
592
|
+
|
|
593
|
+
private JournalEntryCommands() {
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* 仕訳作成コマンド
|
|
598
|
+
*/
|
|
599
|
+
public record CreateJournalEntryCommand(
|
|
600
|
+
@TargetAggregateIdentifier
|
|
601
|
+
String journalEntryId,
|
|
602
|
+
LocalDate transactionDate,
|
|
603
|
+
String description,
|
|
604
|
+
List<JournalLineInput> lines,
|
|
605
|
+
String createdBy
|
|
606
|
+
) {
|
|
607
|
+
public record JournalLineInput(
|
|
608
|
+
String accountCode,
|
|
609
|
+
String accountName,
|
|
610
|
+
BigDecimal debitAmount,
|
|
611
|
+
BigDecimal creditAmount
|
|
612
|
+
) {
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 仕訳承認申請コマンド
|
|
618
|
+
*/
|
|
619
|
+
public record SubmitJournalEntryCommand(
|
|
620
|
+
@TargetAggregateIdentifier
|
|
621
|
+
String journalEntryId,
|
|
622
|
+
String submittedBy
|
|
623
|
+
) {
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* 仕訳承認コマンド
|
|
628
|
+
*/
|
|
629
|
+
public record ApproveJournalEntryCommand(
|
|
630
|
+
@TargetAggregateIdentifier
|
|
631
|
+
String journalEntryId,
|
|
632
|
+
String approvedBy,
|
|
633
|
+
String comment
|
|
634
|
+
) {
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* 仕訳却下コマンド
|
|
639
|
+
*/
|
|
640
|
+
public record RejectJournalEntryCommand(
|
|
641
|
+
@TargetAggregateIdentifier
|
|
642
|
+
String journalEntryId,
|
|
643
|
+
String rejectedBy,
|
|
644
|
+
String reason
|
|
645
|
+
) {
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* 仕訳修正コマンド
|
|
650
|
+
*/
|
|
651
|
+
public record ReviseJournalEntryCommand(
|
|
652
|
+
@TargetAggregateIdentifier
|
|
653
|
+
String journalEntryId,
|
|
654
|
+
LocalDate transactionDate,
|
|
655
|
+
String description,
|
|
656
|
+
List<CreateJournalEntryCommand.JournalLineInput> lines,
|
|
657
|
+
String revisedBy
|
|
658
|
+
) {
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* 仕訳転記コマンド
|
|
663
|
+
*/
|
|
664
|
+
public record PostJournalEntryCommand(
|
|
665
|
+
@TargetAggregateIdentifier
|
|
666
|
+
String journalEntryId,
|
|
667
|
+
String fiscalPeriod,
|
|
668
|
+
String postedBy
|
|
669
|
+
) {
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* 仕訳キャンセルコマンド
|
|
674
|
+
*/
|
|
675
|
+
public record CancelJournalEntryCommand(
|
|
676
|
+
@TargetAggregateIdentifier
|
|
677
|
+
String journalEntryId,
|
|
678
|
+
String cancelledBy,
|
|
679
|
+
String reason
|
|
680
|
+
) {
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
</details>
|
|
686
|
+
|
|
687
|
+
**@TargetAggregateIdentifier の役割:**
|
|
688
|
+
|
|
689
|
+
- Axon がコマンドをどの集約インスタンスにルーティングするかを決定
|
|
690
|
+
- 集約の一意識別子となるフィールドに付与
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
### 27.4 ドメインモデル(純粋な Java)
|
|
695
|
+
|
|
696
|
+
<details>
|
|
697
|
+
<summary>コード例: JournalEntry.java</summary>
|
|
698
|
+
|
|
699
|
+
```java
|
|
700
|
+
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
701
|
+
|
|
702
|
+
import com.example.accounting.journal.api.events.*;
|
|
703
|
+
import com.example.accounting.journal.api.events.JournalEntryCreatedEvent.JournalLineData;
|
|
704
|
+
import java.math.BigDecimal;
|
|
705
|
+
import java.time.LocalDate;
|
|
706
|
+
import java.time.LocalDateTime;
|
|
707
|
+
import java.util.List;
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* JournalEntry ドメインモデル(Axon 非依存)
|
|
711
|
+
* 純粋なビジネスロジックのみを含む
|
|
712
|
+
*/
|
|
713
|
+
public record JournalEntry(
|
|
714
|
+
String journalEntryId,
|
|
715
|
+
String journalNumber,
|
|
716
|
+
LocalDate transactionDate,
|
|
717
|
+
String description,
|
|
718
|
+
List<JournalLine> lines,
|
|
719
|
+
JournalEntryStatus status
|
|
720
|
+
) {
|
|
721
|
+
|
|
722
|
+
// ======== ファクトリメソッド ========
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* 仕訳作成
|
|
726
|
+
*/
|
|
727
|
+
public static JournalEntryCreatedEvent create(
|
|
728
|
+
String journalEntryId,
|
|
729
|
+
String journalNumber,
|
|
730
|
+
LocalDate transactionDate,
|
|
731
|
+
String description,
|
|
732
|
+
List<JournalLineData> lines,
|
|
733
|
+
String createdBy
|
|
734
|
+
) {
|
|
735
|
+
// バリデーション: 借方・貸方の合計が一致すること
|
|
736
|
+
BigDecimal totalDebit = lines.stream()
|
|
737
|
+
.map(JournalLineData::debitAmount)
|
|
738
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
739
|
+
BigDecimal totalCredit = lines.stream()
|
|
740
|
+
.map(JournalLineData::creditAmount)
|
|
741
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
742
|
+
|
|
743
|
+
if (totalDebit.compareTo(totalCredit) != 0) {
|
|
744
|
+
throw new IllegalArgumentException(
|
|
745
|
+
"Debit and credit amounts must be equal. Debit: " + totalDebit + ", Credit: " + totalCredit
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (lines.isEmpty()) {
|
|
750
|
+
throw new IllegalArgumentException("Journal entry must have at least one line");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return new JournalEntryCreatedEvent(
|
|
754
|
+
journalEntryId,
|
|
755
|
+
journalNumber,
|
|
756
|
+
transactionDate,
|
|
757
|
+
description,
|
|
758
|
+
lines,
|
|
759
|
+
createdBy,
|
|
760
|
+
LocalDateTime.now()
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* イベントからの再構築
|
|
766
|
+
*/
|
|
767
|
+
public static JournalEntry from(JournalEntryCreatedEvent event) {
|
|
768
|
+
List<JournalLine> lines = event.lines().stream()
|
|
769
|
+
.map(data -> new JournalLine(
|
|
770
|
+
data.accountCode(),
|
|
771
|
+
data.accountName(),
|
|
772
|
+
data.debitAmount(),
|
|
773
|
+
data.creditAmount()
|
|
774
|
+
))
|
|
775
|
+
.toList();
|
|
776
|
+
|
|
777
|
+
return new JournalEntry(
|
|
778
|
+
event.journalEntryId(),
|
|
779
|
+
event.journalNumber(),
|
|
780
|
+
event.transactionDate(),
|
|
781
|
+
event.description(),
|
|
782
|
+
lines,
|
|
783
|
+
JournalEntryStatus.DRAFT
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ======== 状態遷移メソッド ========
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* 承認申請
|
|
791
|
+
*/
|
|
792
|
+
public JournalEntrySubmittedEvent submit(String submittedBy) {
|
|
793
|
+
if (status != JournalEntryStatus.DRAFT) {
|
|
794
|
+
throw new IllegalStateException("Only draft journal entries can be submitted");
|
|
795
|
+
}
|
|
796
|
+
return new JournalEntrySubmittedEvent(journalEntryId, submittedBy, LocalDateTime.now());
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* 承認
|
|
801
|
+
*/
|
|
802
|
+
public JournalEntryApprovedEvent approve(String approvedBy, String comment) {
|
|
803
|
+
if (status != JournalEntryStatus.PENDING_APPROVAL) {
|
|
804
|
+
throw new IllegalStateException("Only pending journal entries can be approved");
|
|
805
|
+
}
|
|
806
|
+
return new JournalEntryApprovedEvent(journalEntryId, approvedBy, comment, LocalDateTime.now());
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* 却下
|
|
811
|
+
*/
|
|
812
|
+
public JournalEntryRejectedEvent reject(String rejectedBy, String reason) {
|
|
813
|
+
if (status != JournalEntryStatus.PENDING_APPROVAL) {
|
|
814
|
+
throw new IllegalStateException("Only pending journal entries can be rejected");
|
|
815
|
+
}
|
|
816
|
+
return new JournalEntryRejectedEvent(journalEntryId, rejectedBy, reason, LocalDateTime.now());
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* 修正
|
|
821
|
+
*/
|
|
822
|
+
public JournalEntryRevisedEvent revise(
|
|
823
|
+
LocalDate transactionDate,
|
|
824
|
+
String description,
|
|
825
|
+
List<JournalLineData> lines,
|
|
826
|
+
String revisedBy
|
|
827
|
+
) {
|
|
828
|
+
if (status != JournalEntryStatus.REJECTED) {
|
|
829
|
+
throw new IllegalStateException("Only rejected journal entries can be revised");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// バリデーション
|
|
833
|
+
BigDecimal totalDebit = lines.stream()
|
|
834
|
+
.map(JournalLineData::debitAmount)
|
|
835
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
836
|
+
BigDecimal totalCredit = lines.stream()
|
|
837
|
+
.map(JournalLineData::creditAmount)
|
|
838
|
+
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
839
|
+
|
|
840
|
+
if (totalDebit.compareTo(totalCredit) != 0) {
|
|
841
|
+
throw new IllegalArgumentException("Debit and credit amounts must be equal");
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return new JournalEntryRevisedEvent(
|
|
845
|
+
journalEntryId,
|
|
846
|
+
transactionDate,
|
|
847
|
+
description,
|
|
848
|
+
lines,
|
|
849
|
+
revisedBy,
|
|
850
|
+
LocalDateTime.now()
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* 転記
|
|
856
|
+
*/
|
|
857
|
+
public JournalEntryPostedEvent post(String fiscalPeriod, String postedBy) {
|
|
858
|
+
if (status != JournalEntryStatus.APPROVED) {
|
|
859
|
+
throw new IllegalStateException("Only approved journal entries can be posted");
|
|
860
|
+
}
|
|
861
|
+
return new JournalEntryPostedEvent(journalEntryId, fiscalPeriod, postedBy, LocalDateTime.now());
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* キャンセル
|
|
866
|
+
*/
|
|
867
|
+
public JournalEntryCancelledEvent cancel(String cancelledBy, String reason) {
|
|
868
|
+
if (status == JournalEntryStatus.POSTED) {
|
|
869
|
+
throw new IllegalStateException("Posted journal entries cannot be cancelled");
|
|
870
|
+
}
|
|
871
|
+
if (status == JournalEntryStatus.CANCELLED) {
|
|
872
|
+
throw new IllegalStateException("Journal entry is already cancelled");
|
|
873
|
+
}
|
|
874
|
+
return new JournalEntryCancelledEvent(journalEntryId, cancelledBy, reason, LocalDateTime.now());
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// ======== イベント適用メソッド ========
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* イベントを適用して新しい状態を生成
|
|
881
|
+
*/
|
|
882
|
+
public JournalEntry apply(JournalEntryEvent event) {
|
|
883
|
+
return switch (event) {
|
|
884
|
+
case JournalEntryCreatedEvent e -> from(e);
|
|
885
|
+
case JournalEntrySubmittedEvent e -> withStatus(JournalEntryStatus.PENDING_APPROVAL);
|
|
886
|
+
case JournalEntryApprovedEvent e -> withStatus(JournalEntryStatus.APPROVED);
|
|
887
|
+
case JournalEntryRejectedEvent e -> withStatus(JournalEntryStatus.REJECTED);
|
|
888
|
+
case JournalEntryRevisedEvent e -> revised(e);
|
|
889
|
+
case JournalEntryPostedEvent e -> withStatus(JournalEntryStatus.POSTED);
|
|
890
|
+
case JournalEntryCancelledEvent e -> withStatus(JournalEntryStatus.CANCELLED);
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* 状態更新ヘルパー
|
|
896
|
+
*/
|
|
897
|
+
private JournalEntry withStatus(JournalEntryStatus newStatus) {
|
|
898
|
+
return new JournalEntry(journalEntryId, journalNumber, transactionDate, description, lines, newStatus);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* 修正適用
|
|
903
|
+
*/
|
|
904
|
+
private JournalEntry revised(JournalEntryRevisedEvent event) {
|
|
905
|
+
List<JournalLine> newLines = event.lines().stream()
|
|
906
|
+
.map(data -> new JournalLine(
|
|
907
|
+
data.accountCode(),
|
|
908
|
+
data.accountName(),
|
|
909
|
+
data.debitAmount(),
|
|
910
|
+
data.creditAmount()
|
|
911
|
+
))
|
|
912
|
+
.toList();
|
|
913
|
+
|
|
914
|
+
return new JournalEntry(
|
|
915
|
+
journalEntryId,
|
|
916
|
+
journalNumber,
|
|
917
|
+
event.transactionDate(),
|
|
918
|
+
event.description(),
|
|
919
|
+
newLines,
|
|
920
|
+
JournalEntryStatus.DRAFT
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
</details>
|
|
927
|
+
|
|
928
|
+
<details>
|
|
929
|
+
<summary>コード例: JournalEntryStatus.java</summary>
|
|
930
|
+
|
|
931
|
+
```java
|
|
932
|
+
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* 仕訳ステータス
|
|
936
|
+
*/
|
|
937
|
+
public enum JournalEntryStatus {
|
|
938
|
+
DRAFT, // 下書き
|
|
939
|
+
PENDING_APPROVAL, // 承認待ち
|
|
940
|
+
APPROVED, // 承認済み
|
|
941
|
+
REJECTED, // 却下
|
|
942
|
+
POSTED, // 転記済み
|
|
943
|
+
CANCELLED // キャンセル
|
|
944
|
+
}
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
</details>
|
|
948
|
+
|
|
949
|
+
<details>
|
|
950
|
+
<summary>コード例: JournalLine.java</summary>
|
|
951
|
+
|
|
952
|
+
```java
|
|
953
|
+
package com.example.accounting.journal.domain.model.aggregate.journal;
|
|
954
|
+
|
|
955
|
+
import java.math.BigDecimal;
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* 仕訳明細
|
|
959
|
+
*/
|
|
960
|
+
public record JournalLine(
|
|
961
|
+
String accountCode,
|
|
962
|
+
String accountName,
|
|
963
|
+
BigDecimal debitAmount,
|
|
964
|
+
BigDecimal creditAmount
|
|
965
|
+
) {
|
|
966
|
+
public JournalLine {
|
|
967
|
+
if (debitAmount == null) debitAmount = BigDecimal.ZERO;
|
|
968
|
+
if (creditAmount == null) creditAmount = BigDecimal.ZERO;
|
|
969
|
+
|
|
970
|
+
// 借方と貸方は排他的
|
|
971
|
+
if (debitAmount.compareTo(BigDecimal.ZERO) > 0 &&
|
|
972
|
+
creditAmount.compareTo(BigDecimal.ZERO) > 0) {
|
|
973
|
+
throw new IllegalArgumentException(
|
|
974
|
+
"A journal line cannot have both debit and credit amounts"
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
</details>
|
|
982
|
+
|
|
983
|
+
**ドメインモデル設計原則:**
|
|
984
|
+
|
|
985
|
+
| 原則 | 説明 |
|
|
986
|
+
|------|------|
|
|
987
|
+
| **Axon 非依存** | ドメインモデルにフレームワーク依存を持たせない |
|
|
988
|
+
| **イミュータブル** | record でイミュータブルに設計 |
|
|
989
|
+
| **イベントを返す** | 状態遷移メソッドはイベントを返す |
|
|
990
|
+
| **最小限のフィールド** | 状態遷移の判定に必要な最小限のみ保持 |
|
|
991
|
+
| **ビジネスルール** | 借方・貸方の一致検証をドメインモデル内で実施 |
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
### 27.5 Account Bounded Context
|
|
996
|
+
|
|
997
|
+
勘定科目の残高管理を行います。
|
|
998
|
+
|
|
999
|
+
#### イベント定義
|
|
1000
|
+
|
|
1001
|
+
<details>
|
|
1002
|
+
<summary>コード例: AccountEvent.java</summary>
|
|
1003
|
+
|
|
1004
|
+
```java
|
|
1005
|
+
package com.example.accounting.account.api.events;
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Account イベントの sealed interface
|
|
1009
|
+
*/
|
|
1010
|
+
public sealed interface AccountEvent permits
|
|
1011
|
+
AccountCreatedEvent,
|
|
1012
|
+
AccountBalanceUpdatedEvent,
|
|
1013
|
+
AccountClosedEvent {
|
|
1014
|
+
|
|
1015
|
+
String accountCode();
|
|
1016
|
+
}
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
</details>
|
|
1020
|
+
|
|
1021
|
+
<details>
|
|
1022
|
+
<summary>コード例: AccountBalanceUpdatedEvent.java</summary>
|
|
1023
|
+
|
|
1024
|
+
```java
|
|
1025
|
+
package com.example.accounting.account.api.events;
|
|
1026
|
+
|
|
1027
|
+
import java.math.BigDecimal;
|
|
1028
|
+
import java.time.LocalDateTime;
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* 勘定残高更新イベント
|
|
1032
|
+
*/
|
|
1033
|
+
public record AccountBalanceUpdatedEvent(
|
|
1034
|
+
String accountCode,
|
|
1035
|
+
String journalEntryId,
|
|
1036
|
+
BigDecimal debitAmount,
|
|
1037
|
+
BigDecimal creditAmount,
|
|
1038
|
+
BigDecimal balanceAfter,
|
|
1039
|
+
LocalDateTime updatedAt
|
|
1040
|
+
) implements AccountEvent {
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
</details>
|
|
1045
|
+
|
|
1046
|
+
---
|
|
1047
|
+
|
|
1048
|
+
## 第28章:Aggregate Adapter と Policy Handler
|
|
1049
|
+
|
|
1050
|
+
### 28.1 Aggregate Adapter パターン
|
|
1051
|
+
|
|
1052
|
+
Aggregate Adapter は、純粋なドメインモデルと Axon Framework を繋ぐアダプター層です。フレームワーク固有のアノテーションやライフサイクル処理をドメインモデルから分離します。
|
|
1053
|
+
|
|
1054
|
+
```plantuml
|
|
1055
|
+
@startuml aggregate_adapter
|
|
1056
|
+
skinparam backgroundColor #FEFEFE
|
|
1057
|
+
|
|
1058
|
+
package "Application Layer" {
|
|
1059
|
+
class JournalEntryAggregateAdapter << @Aggregate >> {
|
|
1060
|
+
- journalEntryId: String
|
|
1061
|
+
- journalEntry: JournalEntry
|
|
1062
|
+
+ @CommandHandler: handle(CreateJournalEntryCommand)
|
|
1063
|
+
+ @CommandHandler: handle(ApproveJournalEntryCommand)
|
|
1064
|
+
+ @EventSourcingHandler: on(JournalEntryCreatedEvent)
|
|
1065
|
+
+ @EventSourcingHandler: on(JournalEntryApprovedEvent)
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
package "Domain Layer" {
|
|
1070
|
+
class JournalEntry << record >> {
|
|
1071
|
+
+ create(): JournalEntryCreatedEvent
|
|
1072
|
+
+ approve(): JournalEntryApprovedEvent
|
|
1073
|
+
+ apply(event): JournalEntry
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
JournalEntryAggregateAdapter --> JournalEntry : "委譲"
|
|
1078
|
+
|
|
1079
|
+
note bottom of JournalEntryAggregateAdapter
|
|
1080
|
+
Axon Framework 用アダプター
|
|
1081
|
+
フレームワーク依存を吸収
|
|
1082
|
+
end note
|
|
1083
|
+
|
|
1084
|
+
note bottom of JournalEntry
|
|
1085
|
+
純粋なドメインモデル
|
|
1086
|
+
Axon に依存しない
|
|
1087
|
+
end note
|
|
1088
|
+
|
|
1089
|
+
@enduml
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
### 28.2 JournalEntry Aggregate Adapter
|
|
1095
|
+
|
|
1096
|
+
<details>
|
|
1097
|
+
<summary>コード例: JournalEntryAggregateAdapter.java</summary>
|
|
1098
|
+
|
|
1099
|
+
```java
|
|
1100
|
+
package com.example.accounting.journal.application.aggregate;
|
|
1101
|
+
|
|
1102
|
+
import com.example.accounting.journal.api.events.*;
|
|
1103
|
+
import com.example.accounting.journal.api.events.JournalEntryCreatedEvent.JournalLineData;
|
|
1104
|
+
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntry;
|
|
1105
|
+
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntryCommands.*;
|
|
1106
|
+
import org.axonframework.commandhandling.CommandHandler;
|
|
1107
|
+
import org.axonframework.eventsourcing.EventSourcingHandler;
|
|
1108
|
+
import org.axonframework.modelling.command.AggregateIdentifier;
|
|
1109
|
+
import org.axonframework.modelling.command.AggregateLifecycle;
|
|
1110
|
+
import org.axonframework.spring.stereotype.Aggregate;
|
|
1111
|
+
|
|
1112
|
+
import java.util.UUID;
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* JournalEntry Aggregate Adapter(Axon Framework 用)
|
|
1116
|
+
*/
|
|
1117
|
+
@Aggregate
|
|
1118
|
+
public class JournalEntryAggregateAdapter {
|
|
1119
|
+
|
|
1120
|
+
@AggregateIdentifier
|
|
1121
|
+
private String journalEntryId;
|
|
1122
|
+
|
|
1123
|
+
private JournalEntry journalEntry;
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Axon が使用するデフォルトコンストラクタ
|
|
1127
|
+
*/
|
|
1128
|
+
protected JournalEntryAggregateAdapter() {
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// ======== Command Handlers ========
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* 集約作成コマンドハンドラー(コンストラクタ)
|
|
1135
|
+
*/
|
|
1136
|
+
@CommandHandler
|
|
1137
|
+
public JournalEntryAggregateAdapter(CreateJournalEntryCommand command) {
|
|
1138
|
+
// 仕訳番号を生成
|
|
1139
|
+
String journalNumber = generateJournalNumber();
|
|
1140
|
+
|
|
1141
|
+
// 明細データを変換
|
|
1142
|
+
var lines = command.lines().stream()
|
|
1143
|
+
.map(input -> new JournalLineData(
|
|
1144
|
+
input.accountCode(),
|
|
1145
|
+
input.accountName(),
|
|
1146
|
+
input.debitAmount(),
|
|
1147
|
+
input.creditAmount()
|
|
1148
|
+
))
|
|
1149
|
+
.toList();
|
|
1150
|
+
|
|
1151
|
+
// ドメインモデルのファクトリメソッドを呼び出し
|
|
1152
|
+
var event = JournalEntry.create(
|
|
1153
|
+
command.journalEntryId(),
|
|
1154
|
+
journalNumber,
|
|
1155
|
+
command.transactionDate(),
|
|
1156
|
+
command.description(),
|
|
1157
|
+
lines,
|
|
1158
|
+
command.createdBy()
|
|
1159
|
+
);
|
|
1160
|
+
// イベントを発行
|
|
1161
|
+
AggregateLifecycle.apply(event);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* 承認申請コマンドハンドラー
|
|
1166
|
+
*/
|
|
1167
|
+
@CommandHandler
|
|
1168
|
+
public void handle(SubmitJournalEntryCommand command) {
|
|
1169
|
+
var event = journalEntry.submit(command.submittedBy());
|
|
1170
|
+
AggregateLifecycle.apply(event);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* 承認コマンドハンドラー
|
|
1175
|
+
*/
|
|
1176
|
+
@CommandHandler
|
|
1177
|
+
public void handle(ApproveJournalEntryCommand command) {
|
|
1178
|
+
var event = journalEntry.approve(command.approvedBy(), command.comment());
|
|
1179
|
+
AggregateLifecycle.apply(event);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* 却下コマンドハンドラー
|
|
1184
|
+
*/
|
|
1185
|
+
@CommandHandler
|
|
1186
|
+
public void handle(RejectJournalEntryCommand command) {
|
|
1187
|
+
var event = journalEntry.reject(command.rejectedBy(), command.reason());
|
|
1188
|
+
AggregateLifecycle.apply(event);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/**
|
|
1192
|
+
* 修正コマンドハンドラー
|
|
1193
|
+
*/
|
|
1194
|
+
@CommandHandler
|
|
1195
|
+
public void handle(ReviseJournalEntryCommand command) {
|
|
1196
|
+
var lines = command.lines().stream()
|
|
1197
|
+
.map(input -> new JournalLineData(
|
|
1198
|
+
input.accountCode(),
|
|
1199
|
+
input.accountName(),
|
|
1200
|
+
input.debitAmount(),
|
|
1201
|
+
input.creditAmount()
|
|
1202
|
+
))
|
|
1203
|
+
.toList();
|
|
1204
|
+
|
|
1205
|
+
var event = journalEntry.revise(
|
|
1206
|
+
command.transactionDate(),
|
|
1207
|
+
command.description(),
|
|
1208
|
+
lines,
|
|
1209
|
+
command.revisedBy()
|
|
1210
|
+
);
|
|
1211
|
+
AggregateLifecycle.apply(event);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* 転記コマンドハンドラー
|
|
1216
|
+
*/
|
|
1217
|
+
@CommandHandler
|
|
1218
|
+
public void handle(PostJournalEntryCommand command) {
|
|
1219
|
+
var event = journalEntry.post(command.fiscalPeriod(), command.postedBy());
|
|
1220
|
+
AggregateLifecycle.apply(event);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* キャンセルコマンドハンドラー
|
|
1225
|
+
*/
|
|
1226
|
+
@CommandHandler
|
|
1227
|
+
public void handle(CancelJournalEntryCommand command) {
|
|
1228
|
+
var event = journalEntry.cancel(command.cancelledBy(), command.reason());
|
|
1229
|
+
AggregateLifecycle.apply(event);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// ======== Event Sourcing Handlers(状態復元)========
|
|
1233
|
+
|
|
1234
|
+
@EventSourcingHandler
|
|
1235
|
+
public void on(JournalEntryCreatedEvent event) {
|
|
1236
|
+
this.journalEntryId = event.journalEntryId();
|
|
1237
|
+
this.journalEntry = JournalEntry.from(event);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
@EventSourcingHandler
|
|
1241
|
+
public void on(JournalEntrySubmittedEvent event) {
|
|
1242
|
+
this.journalEntry = journalEntry.apply(event);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
@EventSourcingHandler
|
|
1246
|
+
public void on(JournalEntryApprovedEvent event) {
|
|
1247
|
+
this.journalEntry = journalEntry.apply(event);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
@EventSourcingHandler
|
|
1251
|
+
public void on(JournalEntryRejectedEvent event) {
|
|
1252
|
+
this.journalEntry = journalEntry.apply(event);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
@EventSourcingHandler
|
|
1256
|
+
public void on(JournalEntryRevisedEvent event) {
|
|
1257
|
+
this.journalEntry = journalEntry.apply(event);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
@EventSourcingHandler
|
|
1261
|
+
public void on(JournalEntryPostedEvent event) {
|
|
1262
|
+
this.journalEntry = journalEntry.apply(event);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
@EventSourcingHandler
|
|
1266
|
+
public void on(JournalEntryCancelledEvent event) {
|
|
1267
|
+
this.journalEntry = journalEntry.apply(event);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ======== ヘルパーメソッド ========
|
|
1271
|
+
|
|
1272
|
+
private String generateJournalNumber() {
|
|
1273
|
+
// 実際のシステムでは年月日 + 連番など
|
|
1274
|
+
return "JE-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
</details>
|
|
1280
|
+
|
|
1281
|
+
**Axon アノテーション解説:**
|
|
1282
|
+
|
|
1283
|
+
| アノテーション | 説明 |
|
|
1284
|
+
|--------------|------|
|
|
1285
|
+
| `@Aggregate` | Event Sourcing 集約であることを宣言 |
|
|
1286
|
+
| `@AggregateIdentifier` | 集約の識別子フィールド |
|
|
1287
|
+
| `@CommandHandler` | コマンドを処理するメソッド |
|
|
1288
|
+
| `@EventSourcingHandler` | イベントから状態を復元するメソッド |
|
|
1289
|
+
| `AggregateLifecycle.apply()` | イベントを発行するメソッド |
|
|
1290
|
+
|
|
1291
|
+
---
|
|
1292
|
+
|
|
1293
|
+
### 28.3 Policy Handler(Choreography パターン)
|
|
1294
|
+
|
|
1295
|
+
Policy Handler は、他の Bounded Context から発行されたイベントを購読し、自律的に反応する処理を実装します。Choreography パターンでは、各 Context が独立して動作し、イベントを介して連携します。
|
|
1296
|
+
|
|
1297
|
+
```plantuml
|
|
1298
|
+
@startuml choreography
|
|
1299
|
+
skinparam backgroundColor #FEFEFE
|
|
1300
|
+
|
|
1301
|
+
participant "Journal Context" as Journal
|
|
1302
|
+
participant "Event Bus" as Bus
|
|
1303
|
+
participant "Account Context" as Account
|
|
1304
|
+
|
|
1305
|
+
== 仕訳転記フロー ==
|
|
1306
|
+
Journal -> Bus : JournalEntryPostedEvent
|
|
1307
|
+
Bus -> Account : JournalEntryPostedEvent
|
|
1308
|
+
Account -> Account : updateBalance()
|
|
1309
|
+
Account -> Bus : AccountBalanceUpdatedEvent
|
|
1310
|
+
|
|
1311
|
+
@enduml
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
---
|
|
1315
|
+
|
|
1316
|
+
### 28.4 JournalEventHandler(Account Context 内)
|
|
1317
|
+
|
|
1318
|
+
<details>
|
|
1319
|
+
<summary>コード例: JournalEventHandler.java</summary>
|
|
1320
|
+
|
|
1321
|
+
```java
|
|
1322
|
+
package com.example.accounting.account.application.policy;
|
|
1323
|
+
|
|
1324
|
+
import com.example.accounting.account.domain.model.aggregate.account.AccountCommands.*;
|
|
1325
|
+
import com.example.accounting.journal.api.events.JournalEntryPostedEvent;
|
|
1326
|
+
import org.axonframework.commandhandling.gateway.CommandGateway;
|
|
1327
|
+
import org.axonframework.eventhandling.EventHandler;
|
|
1328
|
+
import org.springframework.stereotype.Component;
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Journal イベントに反応して勘定残高を更新する Policy Handler
|
|
1332
|
+
*/
|
|
1333
|
+
@Component
|
|
1334
|
+
public class JournalEventHandler {
|
|
1335
|
+
|
|
1336
|
+
private final CommandGateway commandGateway;
|
|
1337
|
+
|
|
1338
|
+
public JournalEventHandler(CommandGateway commandGateway) {
|
|
1339
|
+
this.commandGateway = commandGateway;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Policy: "When a journal entry is posted, update account balances"
|
|
1344
|
+
*/
|
|
1345
|
+
@EventHandler
|
|
1346
|
+
public void on(JournalEntryPostedEvent event) {
|
|
1347
|
+
// 仕訳の各明細に対して勘定残高を更新
|
|
1348
|
+
// 実際の実装では、イベントから明細情報を取得して処理
|
|
1349
|
+
var command = new UpdateAccountBalanceCommand(
|
|
1350
|
+
event.journalEntryId(),
|
|
1351
|
+
event.fiscalPeriod()
|
|
1352
|
+
);
|
|
1353
|
+
commandGateway.send(command);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
</details>
|
|
1359
|
+
|
|
1360
|
+
**Policy Handler の設計原則:**
|
|
1361
|
+
|
|
1362
|
+
| 原則 | 説明 |
|
|
1363
|
+
|------|------|
|
|
1364
|
+
| **単一責任** | 1 つの Policy Handler は 1 つの関心事のみを扱う |
|
|
1365
|
+
| **疎結合** | 他の Context のイベントを購読し、自 Context のコマンドを発行 |
|
|
1366
|
+
| **自律性** | 他のサービスに依存せず独立して動作 |
|
|
1367
|
+
| **冪等性** | 同じイベントを複数回受信しても問題ないよう設計 |
|
|
1368
|
+
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
## 第29章:Projection と Read Model
|
|
1372
|
+
|
|
1373
|
+
### 29.1 Projection の役割
|
|
1374
|
+
|
|
1375
|
+
Projection は、イベントを購読して Read Model(クエリ用のデータモデル)を更新するコンポーネントです。CQRS では、書き込みモデル(Event Store)と読み取りモデル(Read Model DB)を分離し、それぞれに最適化されたデータ構造を使用します。
|
|
1376
|
+
|
|
1377
|
+
```plantuml
|
|
1378
|
+
@startuml projection
|
|
1379
|
+
skinparam backgroundColor #FEFEFE
|
|
1380
|
+
|
|
1381
|
+
participant "CommandHandler" as CH
|
|
1382
|
+
participant "Event Store" as ES
|
|
1383
|
+
participant "Projection" as P
|
|
1384
|
+
participant "MyBatis Mapper" as MB
|
|
1385
|
+
participant "Read Model DB" as DB
|
|
1386
|
+
participant "REST Controller" as RC
|
|
1387
|
+
|
|
1388
|
+
== 書き込み(Command Side)==
|
|
1389
|
+
CH -> ES : イベント保存
|
|
1390
|
+
ES -> P : イベント通知
|
|
1391
|
+
|
|
1392
|
+
== 読み取りモデル更新 ==
|
|
1393
|
+
P -> MB : insert/update
|
|
1394
|
+
MB -> DB : SQL 実行
|
|
1395
|
+
|
|
1396
|
+
== 読み取り(Query Side)==
|
|
1397
|
+
RC -> MB : findById
|
|
1398
|
+
MB -> DB : SELECT
|
|
1399
|
+
DB --> MB : 結果
|
|
1400
|
+
MB --> RC : Entity
|
|
1401
|
+
|
|
1402
|
+
note right of P
|
|
1403
|
+
Projection は
|
|
1404
|
+
イベントを購読して
|
|
1405
|
+
MyBatis 経由で
|
|
1406
|
+
Read Model を更新
|
|
1407
|
+
end note
|
|
1408
|
+
|
|
1409
|
+
@enduml
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
---
|
|
1413
|
+
|
|
1414
|
+
### 29.2 JournalEntry Projection(MyBatis 版)
|
|
1415
|
+
|
|
1416
|
+
<details>
|
|
1417
|
+
<summary>コード例: JournalEntryProjection.java</summary>
|
|
1418
|
+
|
|
1419
|
+
```java
|
|
1420
|
+
package com.example.accounting.journal.application.query;
|
|
1421
|
+
|
|
1422
|
+
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalEntryEntity;
|
|
1423
|
+
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalLineEntity;
|
|
1424
|
+
import com.example.accounting.journal.adapter.outbound.persistence.mapper.JournalEntryMapper;
|
|
1425
|
+
import com.example.accounting.journal.api.events.*;
|
|
1426
|
+
import org.axonframework.eventhandling.EventHandler;
|
|
1427
|
+
import org.springframework.stereotype.Component;
|
|
1428
|
+
|
|
1429
|
+
import java.util.List;
|
|
1430
|
+
|
|
1431
|
+
/**
|
|
1432
|
+
* JournalEntry Projection(MyBatis を使用した Read Model の更新)
|
|
1433
|
+
*/
|
|
1434
|
+
@Component
|
|
1435
|
+
public class JournalEntryProjection {
|
|
1436
|
+
|
|
1437
|
+
private final JournalEntryMapper journalEntryMapper;
|
|
1438
|
+
|
|
1439
|
+
public JournalEntryProjection(JournalEntryMapper journalEntryMapper) {
|
|
1440
|
+
this.journalEntryMapper = journalEntryMapper;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
@EventHandler
|
|
1444
|
+
public void on(JournalEntryCreatedEvent event) {
|
|
1445
|
+
var entity = new JournalEntryEntity();
|
|
1446
|
+
entity.setJournalEntryId(event.journalEntryId());
|
|
1447
|
+
entity.setJournalNumber(event.journalNumber());
|
|
1448
|
+
entity.setTransactionDate(event.transactionDate());
|
|
1449
|
+
entity.setDescription(event.description());
|
|
1450
|
+
entity.setStatus("DRAFT");
|
|
1451
|
+
entity.setCreatedBy(event.createdBy());
|
|
1452
|
+
entity.setCreatedAt(event.createdAt());
|
|
1453
|
+
|
|
1454
|
+
journalEntryMapper.insert(entity);
|
|
1455
|
+
|
|
1456
|
+
// 明細を保存
|
|
1457
|
+
List<JournalLineEntity> lines = event.lines().stream()
|
|
1458
|
+
.map(data -> {
|
|
1459
|
+
var line = new JournalLineEntity();
|
|
1460
|
+
line.setJournalEntryId(event.journalEntryId());
|
|
1461
|
+
line.setAccountCode(data.accountCode());
|
|
1462
|
+
line.setAccountName(data.accountName());
|
|
1463
|
+
line.setDebitAmount(data.debitAmount());
|
|
1464
|
+
line.setCreditAmount(data.creditAmount());
|
|
1465
|
+
return line;
|
|
1466
|
+
})
|
|
1467
|
+
.toList();
|
|
1468
|
+
|
|
1469
|
+
journalEntryMapper.insertLines(lines);
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
@EventHandler
|
|
1473
|
+
public void on(JournalEntrySubmittedEvent event) {
|
|
1474
|
+
journalEntryMapper.updateStatus(event.journalEntryId(), "PENDING_APPROVAL");
|
|
1475
|
+
journalEntryMapper.updateSubmission(event.journalEntryId(), event.submittedBy(), event.submittedAt());
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
@EventHandler
|
|
1479
|
+
public void on(JournalEntryApprovedEvent event) {
|
|
1480
|
+
journalEntryMapper.updateStatus(event.journalEntryId(), "APPROVED");
|
|
1481
|
+
journalEntryMapper.updateApproval(
|
|
1482
|
+
event.journalEntryId(),
|
|
1483
|
+
event.approvedBy(),
|
|
1484
|
+
event.comment(),
|
|
1485
|
+
event.approvedAt()
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
@EventHandler
|
|
1490
|
+
public void on(JournalEntryRejectedEvent event) {
|
|
1491
|
+
journalEntryMapper.updateStatus(event.journalEntryId(), "REJECTED");
|
|
1492
|
+
journalEntryMapper.updateRejection(
|
|
1493
|
+
event.journalEntryId(),
|
|
1494
|
+
event.rejectedBy(),
|
|
1495
|
+
event.reason(),
|
|
1496
|
+
event.rejectedAt()
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
@EventHandler
|
|
1501
|
+
public void on(JournalEntryRevisedEvent event) {
|
|
1502
|
+
journalEntryMapper.updateStatus(event.journalEntryId(), "DRAFT");
|
|
1503
|
+
journalEntryMapper.updateRevision(
|
|
1504
|
+
event.journalEntryId(),
|
|
1505
|
+
event.transactionDate(),
|
|
1506
|
+
event.description(),
|
|
1507
|
+
event.revisedBy(),
|
|
1508
|
+
event.revisedAt()
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
// 明細を削除して再作成
|
|
1512
|
+
journalEntryMapper.deleteLines(event.journalEntryId());
|
|
1513
|
+
List<JournalLineEntity> lines = event.lines().stream()
|
|
1514
|
+
.map(data -> {
|
|
1515
|
+
var line = new JournalLineEntity();
|
|
1516
|
+
line.setJournalEntryId(event.journalEntryId());
|
|
1517
|
+
line.setAccountCode(data.accountCode());
|
|
1518
|
+
line.setAccountName(data.accountName());
|
|
1519
|
+
line.setDebitAmount(data.debitAmount());
|
|
1520
|
+
line.setCreditAmount(data.creditAmount());
|
|
1521
|
+
return line;
|
|
1522
|
+
})
|
|
1523
|
+
.toList();
|
|
1524
|
+
journalEntryMapper.insertLines(lines);
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
@EventHandler
|
|
1528
|
+
public void on(JournalEntryPostedEvent event) {
|
|
1529
|
+
journalEntryMapper.updateStatus(event.journalEntryId(), "POSTED");
|
|
1530
|
+
journalEntryMapper.updatePosting(
|
|
1531
|
+
event.journalEntryId(),
|
|
1532
|
+
event.fiscalPeriod(),
|
|
1533
|
+
event.postedBy(),
|
|
1534
|
+
event.postedAt()
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
@EventHandler
|
|
1539
|
+
public void on(JournalEntryCancelledEvent event) {
|
|
1540
|
+
journalEntryMapper.updateStatus(event.journalEntryId(), "CANCELLED");
|
|
1541
|
+
journalEntryMapper.updateCancellation(
|
|
1542
|
+
event.journalEntryId(),
|
|
1543
|
+
event.cancelledBy(),
|
|
1544
|
+
event.reason(),
|
|
1545
|
+
event.cancelledAt()
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
```
|
|
1550
|
+
|
|
1551
|
+
</details>
|
|
1552
|
+
|
|
1553
|
+
---
|
|
1554
|
+
|
|
1555
|
+
### 29.3 Read Model Entity
|
|
1556
|
+
|
|
1557
|
+
<details>
|
|
1558
|
+
<summary>コード例: JournalEntryEntity.java</summary>
|
|
1559
|
+
|
|
1560
|
+
```java
|
|
1561
|
+
package com.example.accounting.journal.adapter.outbound.persistence.entity;
|
|
1562
|
+
|
|
1563
|
+
import java.math.BigDecimal;
|
|
1564
|
+
import java.time.LocalDate;
|
|
1565
|
+
import java.time.LocalDateTime;
|
|
1566
|
+
import java.util.List;
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* JournalEntry Read Model(表示用のすべてのフィールドを保持)
|
|
1570
|
+
*/
|
|
1571
|
+
public class JournalEntryEntity {
|
|
1572
|
+
|
|
1573
|
+
private String journalEntryId;
|
|
1574
|
+
private String journalNumber;
|
|
1575
|
+
private LocalDate transactionDate;
|
|
1576
|
+
private String description;
|
|
1577
|
+
private String status;
|
|
1578
|
+
|
|
1579
|
+
// 合計金額(集計用)
|
|
1580
|
+
private BigDecimal totalDebitAmount;
|
|
1581
|
+
private BigDecimal totalCreditAmount;
|
|
1582
|
+
|
|
1583
|
+
// 監査情報
|
|
1584
|
+
private String createdBy;
|
|
1585
|
+
private LocalDateTime createdAt;
|
|
1586
|
+
private String submittedBy;
|
|
1587
|
+
private LocalDateTime submittedAt;
|
|
1588
|
+
private String approvedBy;
|
|
1589
|
+
private String approvalComment;
|
|
1590
|
+
private LocalDateTime approvedAt;
|
|
1591
|
+
private String rejectedBy;
|
|
1592
|
+
private String rejectionReason;
|
|
1593
|
+
private LocalDateTime rejectedAt;
|
|
1594
|
+
private String revisedBy;
|
|
1595
|
+
private LocalDateTime revisedAt;
|
|
1596
|
+
private String fiscalPeriod;
|
|
1597
|
+
private String postedBy;
|
|
1598
|
+
private LocalDateTime postedAt;
|
|
1599
|
+
private String cancelledBy;
|
|
1600
|
+
private String cancellationReason;
|
|
1601
|
+
private LocalDateTime cancelledAt;
|
|
1602
|
+
|
|
1603
|
+
// 明細
|
|
1604
|
+
private List<JournalLineEntity> lines;
|
|
1605
|
+
|
|
1606
|
+
// Getters and Setters
|
|
1607
|
+
public String getJournalEntryId() { return journalEntryId; }
|
|
1608
|
+
public void setJournalEntryId(String journalEntryId) { this.journalEntryId = journalEntryId; }
|
|
1609
|
+
|
|
1610
|
+
public String getJournalNumber() { return journalNumber; }
|
|
1611
|
+
public void setJournalNumber(String journalNumber) { this.journalNumber = journalNumber; }
|
|
1612
|
+
|
|
1613
|
+
public LocalDate getTransactionDate() { return transactionDate; }
|
|
1614
|
+
public void setTransactionDate(LocalDate transactionDate) { this.transactionDate = transactionDate; }
|
|
1615
|
+
|
|
1616
|
+
public String getDescription() { return description; }
|
|
1617
|
+
public void setDescription(String description) { this.description = description; }
|
|
1618
|
+
|
|
1619
|
+
public String getStatus() { return status; }
|
|
1620
|
+
public void setStatus(String status) { this.status = status; }
|
|
1621
|
+
|
|
1622
|
+
public BigDecimal getTotalDebitAmount() { return totalDebitAmount; }
|
|
1623
|
+
public void setTotalDebitAmount(BigDecimal totalDebitAmount) { this.totalDebitAmount = totalDebitAmount; }
|
|
1624
|
+
|
|
1625
|
+
public BigDecimal getTotalCreditAmount() { return totalCreditAmount; }
|
|
1626
|
+
public void setTotalCreditAmount(BigDecimal totalCreditAmount) { this.totalCreditAmount = totalCreditAmount; }
|
|
1627
|
+
|
|
1628
|
+
public String getCreatedBy() { return createdBy; }
|
|
1629
|
+
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
|
|
1630
|
+
|
|
1631
|
+
public LocalDateTime getCreatedAt() { return createdAt; }
|
|
1632
|
+
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
|
1633
|
+
|
|
1634
|
+
public String getSubmittedBy() { return submittedBy; }
|
|
1635
|
+
public void setSubmittedBy(String submittedBy) { this.submittedBy = submittedBy; }
|
|
1636
|
+
|
|
1637
|
+
public LocalDateTime getSubmittedAt() { return submittedAt; }
|
|
1638
|
+
public void setSubmittedAt(LocalDateTime submittedAt) { this.submittedAt = submittedAt; }
|
|
1639
|
+
|
|
1640
|
+
public String getApprovedBy() { return approvedBy; }
|
|
1641
|
+
public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; }
|
|
1642
|
+
|
|
1643
|
+
public String getApprovalComment() { return approvalComment; }
|
|
1644
|
+
public void setApprovalComment(String approvalComment) { this.approvalComment = approvalComment; }
|
|
1645
|
+
|
|
1646
|
+
public LocalDateTime getApprovedAt() { return approvedAt; }
|
|
1647
|
+
public void setApprovedAt(LocalDateTime approvedAt) { this.approvedAt = approvedAt; }
|
|
1648
|
+
|
|
1649
|
+
public String getRejectedBy() { return rejectedBy; }
|
|
1650
|
+
public void setRejectedBy(String rejectedBy) { this.rejectedBy = rejectedBy; }
|
|
1651
|
+
|
|
1652
|
+
public String getRejectionReason() { return rejectionReason; }
|
|
1653
|
+
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
|
|
1654
|
+
|
|
1655
|
+
public LocalDateTime getRejectedAt() { return rejectedAt; }
|
|
1656
|
+
public void setRejectedAt(LocalDateTime rejectedAt) { this.rejectedAt = rejectedAt; }
|
|
1657
|
+
|
|
1658
|
+
public String getRevisedBy() { return revisedBy; }
|
|
1659
|
+
public void setRevisedBy(String revisedBy) { this.revisedBy = revisedBy; }
|
|
1660
|
+
|
|
1661
|
+
public LocalDateTime getRevisedAt() { return revisedAt; }
|
|
1662
|
+
public void setRevisedAt(LocalDateTime revisedAt) { this.revisedAt = revisedAt; }
|
|
1663
|
+
|
|
1664
|
+
public String getFiscalPeriod() { return fiscalPeriod; }
|
|
1665
|
+
public void setFiscalPeriod(String fiscalPeriod) { this.fiscalPeriod = fiscalPeriod; }
|
|
1666
|
+
|
|
1667
|
+
public String getPostedBy() { return postedBy; }
|
|
1668
|
+
public void setPostedBy(String postedBy) { this.postedBy = postedBy; }
|
|
1669
|
+
|
|
1670
|
+
public LocalDateTime getPostedAt() { return postedAt; }
|
|
1671
|
+
public void setPostedAt(LocalDateTime postedAt) { this.postedAt = postedAt; }
|
|
1672
|
+
|
|
1673
|
+
public String getCancelledBy() { return cancelledBy; }
|
|
1674
|
+
public void setCancelledBy(String cancelledBy) { this.cancelledBy = cancelledBy; }
|
|
1675
|
+
|
|
1676
|
+
public String getCancellationReason() { return cancellationReason; }
|
|
1677
|
+
public void setCancellationReason(String cancellationReason) { this.cancellationReason = cancellationReason; }
|
|
1678
|
+
|
|
1679
|
+
public LocalDateTime getCancelledAt() { return cancelledAt; }
|
|
1680
|
+
public void setCancelledAt(LocalDateTime cancelledAt) { this.cancelledAt = cancelledAt; }
|
|
1681
|
+
|
|
1682
|
+
public List<JournalLineEntity> getLines() { return lines; }
|
|
1683
|
+
public void setLines(List<JournalLineEntity> lines) { this.lines = lines; }
|
|
1684
|
+
}
|
|
1685
|
+
```
|
|
1686
|
+
|
|
1687
|
+
</details>
|
|
1688
|
+
|
|
1689
|
+
<details>
|
|
1690
|
+
<summary>コード例: JournalLineEntity.java</summary>
|
|
1691
|
+
|
|
1692
|
+
```java
|
|
1693
|
+
package com.example.accounting.journal.adapter.outbound.persistence.entity;
|
|
1694
|
+
|
|
1695
|
+
import java.math.BigDecimal;
|
|
1696
|
+
|
|
1697
|
+
/**
|
|
1698
|
+
* JournalLine Read Model
|
|
1699
|
+
*/
|
|
1700
|
+
public class JournalLineEntity {
|
|
1701
|
+
|
|
1702
|
+
private Long lineId;
|
|
1703
|
+
private String journalEntryId;
|
|
1704
|
+
private String accountCode;
|
|
1705
|
+
private String accountName;
|
|
1706
|
+
private BigDecimal debitAmount;
|
|
1707
|
+
private BigDecimal creditAmount;
|
|
1708
|
+
|
|
1709
|
+
// Getters and Setters
|
|
1710
|
+
public Long getLineId() { return lineId; }
|
|
1711
|
+
public void setLineId(Long lineId) { this.lineId = lineId; }
|
|
1712
|
+
|
|
1713
|
+
public String getJournalEntryId() { return journalEntryId; }
|
|
1714
|
+
public void setJournalEntryId(String journalEntryId) { this.journalEntryId = journalEntryId; }
|
|
1715
|
+
|
|
1716
|
+
public String getAccountCode() { return accountCode; }
|
|
1717
|
+
public void setAccountCode(String accountCode) { this.accountCode = accountCode; }
|
|
1718
|
+
|
|
1719
|
+
public String getAccountName() { return accountName; }
|
|
1720
|
+
public void setAccountName(String accountName) { this.accountName = accountName; }
|
|
1721
|
+
|
|
1722
|
+
public BigDecimal getDebitAmount() { return debitAmount; }
|
|
1723
|
+
public void setDebitAmount(BigDecimal debitAmount) { this.debitAmount = debitAmount; }
|
|
1724
|
+
|
|
1725
|
+
public BigDecimal getCreditAmount() { return creditAmount; }
|
|
1726
|
+
public void setCreditAmount(BigDecimal creditAmount) { this.creditAmount = creditAmount; }
|
|
1727
|
+
}
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
</details>
|
|
1731
|
+
|
|
1732
|
+
---
|
|
1733
|
+
|
|
1734
|
+
### 29.4 MyBatis Mapper インターフェース
|
|
1735
|
+
|
|
1736
|
+
<details>
|
|
1737
|
+
<summary>コード例: JournalEntryMapper.java</summary>
|
|
1738
|
+
|
|
1739
|
+
```java
|
|
1740
|
+
package com.example.accounting.journal.adapter.outbound.persistence.mapper;
|
|
1741
|
+
|
|
1742
|
+
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalEntryEntity;
|
|
1743
|
+
import com.example.accounting.journal.adapter.outbound.persistence.entity.JournalLineEntity;
|
|
1744
|
+
import org.apache.ibatis.annotations.Mapper;
|
|
1745
|
+
import org.apache.ibatis.annotations.Param;
|
|
1746
|
+
|
|
1747
|
+
import java.time.LocalDate;
|
|
1748
|
+
import java.time.LocalDateTime;
|
|
1749
|
+
import java.util.List;
|
|
1750
|
+
import java.util.Optional;
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* JournalEntry MyBatis Mapper
|
|
1754
|
+
*/
|
|
1755
|
+
@Mapper
|
|
1756
|
+
public interface JournalEntryMapper {
|
|
1757
|
+
|
|
1758
|
+
// 挿入
|
|
1759
|
+
void insert(JournalEntryEntity entity);
|
|
1760
|
+
|
|
1761
|
+
void insertLines(List<JournalLineEntity> lines);
|
|
1762
|
+
|
|
1763
|
+
// 検索
|
|
1764
|
+
Optional<JournalEntryEntity> findById(@Param("journalEntryId") String journalEntryId);
|
|
1765
|
+
|
|
1766
|
+
List<JournalEntryEntity> findAll();
|
|
1767
|
+
|
|
1768
|
+
List<JournalEntryEntity> findByStatus(@Param("status") String status);
|
|
1769
|
+
|
|
1770
|
+
List<JournalEntryEntity> findByFiscalPeriod(@Param("fiscalPeriod") String fiscalPeriod);
|
|
1771
|
+
|
|
1772
|
+
List<JournalEntryEntity> findByTransactionDateRange(
|
|
1773
|
+
@Param("fromDate") LocalDate fromDate,
|
|
1774
|
+
@Param("toDate") LocalDate toDate
|
|
1775
|
+
);
|
|
1776
|
+
|
|
1777
|
+
List<JournalLineEntity> findLinesByJournalEntryId(@Param("journalEntryId") String journalEntryId);
|
|
1778
|
+
|
|
1779
|
+
// 更新
|
|
1780
|
+
void updateStatus(@Param("journalEntryId") String journalEntryId, @Param("status") String status);
|
|
1781
|
+
|
|
1782
|
+
void updateSubmission(
|
|
1783
|
+
@Param("journalEntryId") String journalEntryId,
|
|
1784
|
+
@Param("submittedBy") String submittedBy,
|
|
1785
|
+
@Param("submittedAt") LocalDateTime submittedAt
|
|
1786
|
+
);
|
|
1787
|
+
|
|
1788
|
+
void updateApproval(
|
|
1789
|
+
@Param("journalEntryId") String journalEntryId,
|
|
1790
|
+
@Param("approvedBy") String approvedBy,
|
|
1791
|
+
@Param("comment") String comment,
|
|
1792
|
+
@Param("approvedAt") LocalDateTime approvedAt
|
|
1793
|
+
);
|
|
1794
|
+
|
|
1795
|
+
void updateRejection(
|
|
1796
|
+
@Param("journalEntryId") String journalEntryId,
|
|
1797
|
+
@Param("rejectedBy") String rejectedBy,
|
|
1798
|
+
@Param("reason") String reason,
|
|
1799
|
+
@Param("rejectedAt") LocalDateTime rejectedAt
|
|
1800
|
+
);
|
|
1801
|
+
|
|
1802
|
+
void updateRevision(
|
|
1803
|
+
@Param("journalEntryId") String journalEntryId,
|
|
1804
|
+
@Param("transactionDate") LocalDate transactionDate,
|
|
1805
|
+
@Param("description") String description,
|
|
1806
|
+
@Param("revisedBy") String revisedBy,
|
|
1807
|
+
@Param("revisedAt") LocalDateTime revisedAt
|
|
1808
|
+
);
|
|
1809
|
+
|
|
1810
|
+
void updatePosting(
|
|
1811
|
+
@Param("journalEntryId") String journalEntryId,
|
|
1812
|
+
@Param("fiscalPeriod") String fiscalPeriod,
|
|
1813
|
+
@Param("postedBy") String postedBy,
|
|
1814
|
+
@Param("postedAt") LocalDateTime postedAt
|
|
1815
|
+
);
|
|
1816
|
+
|
|
1817
|
+
void updateCancellation(
|
|
1818
|
+
@Param("journalEntryId") String journalEntryId,
|
|
1819
|
+
@Param("cancelledBy") String cancelledBy,
|
|
1820
|
+
@Param("reason") String reason,
|
|
1821
|
+
@Param("cancelledAt") LocalDateTime cancelledAt
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
// 削除
|
|
1825
|
+
void deleteLines(@Param("journalEntryId") String journalEntryId);
|
|
1826
|
+
}
|
|
1827
|
+
```
|
|
1828
|
+
|
|
1829
|
+
</details>
|
|
1830
|
+
|
|
1831
|
+
---
|
|
1832
|
+
|
|
1833
|
+
### 29.5 MyBatis Mapper XML
|
|
1834
|
+
|
|
1835
|
+
<details>
|
|
1836
|
+
<summary>コード例: JournalEntryMapper.xml</summary>
|
|
1837
|
+
|
|
1838
|
+
```xml
|
|
1839
|
+
<?xml version="1.0" encoding="UTF-8" ?>
|
|
1840
|
+
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|
1841
|
+
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
|
1842
|
+
|
|
1843
|
+
<mapper namespace="com.example.accounting.journal.adapter.outbound.persistence.mapper.JournalEntryMapper">
|
|
1844
|
+
|
|
1845
|
+
<resultMap id="journalEntryResultMap" type="JournalEntryEntity">
|
|
1846
|
+
<id property="journalEntryId" column="journal_entry_id"/>
|
|
1847
|
+
<result property="journalNumber" column="journal_number"/>
|
|
1848
|
+
<result property="transactionDate" column="transaction_date"/>
|
|
1849
|
+
<result property="description" column="description"/>
|
|
1850
|
+
<result property="status" column="status"/>
|
|
1851
|
+
<result property="totalDebitAmount" column="total_debit_amount"/>
|
|
1852
|
+
<result property="totalCreditAmount" column="total_credit_amount"/>
|
|
1853
|
+
<result property="createdBy" column="created_by"/>
|
|
1854
|
+
<result property="createdAt" column="created_at"/>
|
|
1855
|
+
<result property="submittedBy" column="submitted_by"/>
|
|
1856
|
+
<result property="submittedAt" column="submitted_at"/>
|
|
1857
|
+
<result property="approvedBy" column="approved_by"/>
|
|
1858
|
+
<result property="approvalComment" column="approval_comment"/>
|
|
1859
|
+
<result property="approvedAt" column="approved_at"/>
|
|
1860
|
+
<result property="rejectedBy" column="rejected_by"/>
|
|
1861
|
+
<result property="rejectionReason" column="rejection_reason"/>
|
|
1862
|
+
<result property="rejectedAt" column="rejected_at"/>
|
|
1863
|
+
<result property="revisedBy" column="revised_by"/>
|
|
1864
|
+
<result property="revisedAt" column="revised_at"/>
|
|
1865
|
+
<result property="fiscalPeriod" column="fiscal_period"/>
|
|
1866
|
+
<result property="postedBy" column="posted_by"/>
|
|
1867
|
+
<result property="postedAt" column="posted_at"/>
|
|
1868
|
+
<result property="cancelledBy" column="cancelled_by"/>
|
|
1869
|
+
<result property="cancellationReason" column="cancellation_reason"/>
|
|
1870
|
+
<result property="cancelledAt" column="cancelled_at"/>
|
|
1871
|
+
<collection property="lines" ofType="JournalLineEntity"
|
|
1872
|
+
select="findLinesByJournalEntryId" column="journal_entry_id"/>
|
|
1873
|
+
</resultMap>
|
|
1874
|
+
|
|
1875
|
+
<resultMap id="journalLineResultMap" type="JournalLineEntity">
|
|
1876
|
+
<id property="lineId" column="line_id"/>
|
|
1877
|
+
<result property="journalEntryId" column="journal_entry_id"/>
|
|
1878
|
+
<result property="accountCode" column="account_code"/>
|
|
1879
|
+
<result property="accountName" column="account_name"/>
|
|
1880
|
+
<result property="debitAmount" column="debit_amount"/>
|
|
1881
|
+
<result property="creditAmount" column="credit_amount"/>
|
|
1882
|
+
</resultMap>
|
|
1883
|
+
|
|
1884
|
+
<insert id="insert" parameterType="JournalEntryEntity">
|
|
1885
|
+
INSERT INTO journal_entries (
|
|
1886
|
+
journal_entry_id, journal_number, transaction_date, description,
|
|
1887
|
+
status, created_by, created_at
|
|
1888
|
+
) VALUES (
|
|
1889
|
+
#{journalEntryId}, #{journalNumber}, #{transactionDate}, #{description},
|
|
1890
|
+
#{status}, #{createdBy}, #{createdAt}
|
|
1891
|
+
)
|
|
1892
|
+
</insert>
|
|
1893
|
+
|
|
1894
|
+
<insert id="insertLines" parameterType="list">
|
|
1895
|
+
INSERT INTO journal_lines (
|
|
1896
|
+
journal_entry_id, account_code, account_name, debit_amount, credit_amount
|
|
1897
|
+
) VALUES
|
|
1898
|
+
<foreach collection="list" item="line" separator=",">
|
|
1899
|
+
(#{line.journalEntryId}, #{line.accountCode}, #{line.accountName},
|
|
1900
|
+
#{line.debitAmount}, #{line.creditAmount})
|
|
1901
|
+
</foreach>
|
|
1902
|
+
</insert>
|
|
1903
|
+
|
|
1904
|
+
<select id="findById" resultMap="journalEntryResultMap">
|
|
1905
|
+
SELECT je.*,
|
|
1906
|
+
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1907
|
+
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1908
|
+
FROM journal_entries je
|
|
1909
|
+
WHERE je.journal_entry_id = #{journalEntryId}
|
|
1910
|
+
</select>
|
|
1911
|
+
|
|
1912
|
+
<select id="findAll" resultMap="journalEntryResultMap">
|
|
1913
|
+
SELECT je.*,
|
|
1914
|
+
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1915
|
+
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1916
|
+
FROM journal_entries je
|
|
1917
|
+
ORDER BY je.created_at DESC
|
|
1918
|
+
</select>
|
|
1919
|
+
|
|
1920
|
+
<select id="findByStatus" resultMap="journalEntryResultMap">
|
|
1921
|
+
SELECT je.*,
|
|
1922
|
+
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1923
|
+
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1924
|
+
FROM journal_entries je
|
|
1925
|
+
WHERE je.status = #{status}
|
|
1926
|
+
ORDER BY je.created_at DESC
|
|
1927
|
+
</select>
|
|
1928
|
+
|
|
1929
|
+
<select id="findByFiscalPeriod" resultMap="journalEntryResultMap">
|
|
1930
|
+
SELECT je.*,
|
|
1931
|
+
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1932
|
+
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1933
|
+
FROM journal_entries je
|
|
1934
|
+
WHERE je.fiscal_period = #{fiscalPeriod}
|
|
1935
|
+
ORDER BY je.transaction_date, je.journal_number
|
|
1936
|
+
</select>
|
|
1937
|
+
|
|
1938
|
+
<select id="findByTransactionDateRange" resultMap="journalEntryResultMap">
|
|
1939
|
+
SELECT je.*,
|
|
1940
|
+
(SELECT COALESCE(SUM(debit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_debit_amount,
|
|
1941
|
+
(SELECT COALESCE(SUM(credit_amount), 0) FROM journal_lines WHERE journal_entry_id = je.journal_entry_id) as total_credit_amount
|
|
1942
|
+
FROM journal_entries je
|
|
1943
|
+
WHERE je.transaction_date BETWEEN #{fromDate} AND #{toDate}
|
|
1944
|
+
ORDER BY je.transaction_date, je.journal_number
|
|
1945
|
+
</select>
|
|
1946
|
+
|
|
1947
|
+
<select id="findLinesByJournalEntryId" resultMap="journalLineResultMap">
|
|
1948
|
+
SELECT * FROM journal_lines WHERE journal_entry_id = #{journalEntryId} ORDER BY line_id
|
|
1949
|
+
</select>
|
|
1950
|
+
|
|
1951
|
+
<update id="updateStatus">
|
|
1952
|
+
UPDATE journal_entries SET status = #{status} WHERE journal_entry_id = #{journalEntryId}
|
|
1953
|
+
</update>
|
|
1954
|
+
|
|
1955
|
+
<update id="updateSubmission">
|
|
1956
|
+
UPDATE journal_entries
|
|
1957
|
+
SET submitted_by = #{submittedBy}, submitted_at = #{submittedAt}
|
|
1958
|
+
WHERE journal_entry_id = #{journalEntryId}
|
|
1959
|
+
</update>
|
|
1960
|
+
|
|
1961
|
+
<update id="updateApproval">
|
|
1962
|
+
UPDATE journal_entries
|
|
1963
|
+
SET approved_by = #{approvedBy}, approval_comment = #{comment}, approved_at = #{approvedAt}
|
|
1964
|
+
WHERE journal_entry_id = #{journalEntryId}
|
|
1965
|
+
</update>
|
|
1966
|
+
|
|
1967
|
+
<update id="updateRejection">
|
|
1968
|
+
UPDATE journal_entries
|
|
1969
|
+
SET rejected_by = #{rejectedBy}, rejection_reason = #{reason}, rejected_at = #{rejectedAt}
|
|
1970
|
+
WHERE journal_entry_id = #{journalEntryId}
|
|
1971
|
+
</update>
|
|
1972
|
+
|
|
1973
|
+
<update id="updateRevision">
|
|
1974
|
+
UPDATE journal_entries
|
|
1975
|
+
SET transaction_date = #{transactionDate}, description = #{description},
|
|
1976
|
+
revised_by = #{revisedBy}, revised_at = #{revisedAt}
|
|
1977
|
+
WHERE journal_entry_id = #{journalEntryId}
|
|
1978
|
+
</update>
|
|
1979
|
+
|
|
1980
|
+
<update id="updatePosting">
|
|
1981
|
+
UPDATE journal_entries
|
|
1982
|
+
SET fiscal_period = #{fiscalPeriod}, posted_by = #{postedBy}, posted_at = #{postedAt}
|
|
1983
|
+
WHERE journal_entry_id = #{journalEntryId}
|
|
1984
|
+
</update>
|
|
1985
|
+
|
|
1986
|
+
<update id="updateCancellation">
|
|
1987
|
+
UPDATE journal_entries
|
|
1988
|
+
SET cancelled_by = #{cancelledBy}, cancellation_reason = #{reason}, cancelled_at = #{cancelledAt}
|
|
1989
|
+
WHERE journal_entry_id = #{journalEntryId}
|
|
1990
|
+
</update>
|
|
1991
|
+
|
|
1992
|
+
<delete id="deleteLines">
|
|
1993
|
+
DELETE FROM journal_lines WHERE journal_entry_id = #{journalEntryId}
|
|
1994
|
+
</delete>
|
|
1995
|
+
|
|
1996
|
+
</mapper>
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
</details>
|
|
2000
|
+
|
|
2001
|
+
---
|
|
2002
|
+
|
|
2003
|
+
### 29.6 Read Model スキーマ
|
|
2004
|
+
|
|
2005
|
+
<details>
|
|
2006
|
+
<summary>コード例: schema.sql</summary>
|
|
2007
|
+
|
|
2008
|
+
```sql
|
|
2009
|
+
-- Read Model: Journal Entries テーブル
|
|
2010
|
+
CREATE TABLE IF NOT EXISTS journal_entries (
|
|
2011
|
+
journal_entry_id VARCHAR(36) PRIMARY KEY,
|
|
2012
|
+
journal_number VARCHAR(20) NOT NULL UNIQUE,
|
|
2013
|
+
transaction_date DATE NOT NULL,
|
|
2014
|
+
description TEXT,
|
|
2015
|
+
status VARCHAR(20) NOT NULL,
|
|
2016
|
+
|
|
2017
|
+
-- 監査情報
|
|
2018
|
+
created_by VARCHAR(100) NOT NULL,
|
|
2019
|
+
created_at TIMESTAMP NOT NULL,
|
|
2020
|
+
submitted_by VARCHAR(100),
|
|
2021
|
+
submitted_at TIMESTAMP,
|
|
2022
|
+
approved_by VARCHAR(100),
|
|
2023
|
+
approval_comment TEXT,
|
|
2024
|
+
approved_at TIMESTAMP,
|
|
2025
|
+
rejected_by VARCHAR(100),
|
|
2026
|
+
rejection_reason TEXT,
|
|
2027
|
+
rejected_at TIMESTAMP,
|
|
2028
|
+
revised_by VARCHAR(100),
|
|
2029
|
+
revised_at TIMESTAMP,
|
|
2030
|
+
fiscal_period VARCHAR(10),
|
|
2031
|
+
posted_by VARCHAR(100),
|
|
2032
|
+
posted_at TIMESTAMP,
|
|
2033
|
+
cancelled_by VARCHAR(100),
|
|
2034
|
+
cancellation_reason TEXT,
|
|
2035
|
+
cancelled_at TIMESTAMP
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
-- Read Model: Journal Lines テーブル
|
|
2039
|
+
CREATE TABLE IF NOT EXISTS journal_lines (
|
|
2040
|
+
line_id SERIAL PRIMARY KEY,
|
|
2041
|
+
journal_entry_id VARCHAR(36) NOT NULL REFERENCES journal_entries(journal_entry_id),
|
|
2042
|
+
account_code VARCHAR(10) NOT NULL,
|
|
2043
|
+
account_name VARCHAR(100) NOT NULL,
|
|
2044
|
+
debit_amount DECIMAL(15, 2) DEFAULT 0,
|
|
2045
|
+
credit_amount DECIMAL(15, 2) DEFAULT 0
|
|
2046
|
+
);
|
|
2047
|
+
|
|
2048
|
+
-- インデックス
|
|
2049
|
+
CREATE INDEX IF NOT EXISTS idx_journal_entries_status ON journal_entries(status);
|
|
2050
|
+
CREATE INDEX IF NOT EXISTS idx_journal_entries_transaction_date ON journal_entries(transaction_date);
|
|
2051
|
+
CREATE INDEX IF NOT EXISTS idx_journal_entries_fiscal_period ON journal_entries(fiscal_period);
|
|
2052
|
+
CREATE INDEX IF NOT EXISTS idx_journal_lines_journal_entry_id ON journal_lines(journal_entry_id);
|
|
2053
|
+
CREATE INDEX IF NOT EXISTS idx_journal_lines_account_code ON journal_lines(account_code);
|
|
2054
|
+
```
|
|
2055
|
+
|
|
2056
|
+
</details>
|
|
2057
|
+
|
|
2058
|
+
---
|
|
2059
|
+
|
|
2060
|
+
### 29.7 最小限フィールドの原則
|
|
2061
|
+
|
|
2062
|
+
Event Sourcing では、ドメインモデルは「次のコマンドを処理するために必要な最小限の状態」のみを保持します。一方、Read Model は表示に必要なすべてのフィールドを保持します。
|
|
2063
|
+
|
|
2064
|
+
| フィールド | ドメインモデル | Read Model | 理由 |
|
|
2065
|
+
|-----------|:-------------:|:----------:|------|
|
|
2066
|
+
| journalEntryId | O | O | 識別子として必要 |
|
|
2067
|
+
| status | O | O | 状態遷移の判定に必要 |
|
|
2068
|
+
| lines | O | O | 借方・貸方の検証に必要 |
|
|
2069
|
+
| approvedBy | X | O | 判定に不要、表示のみ |
|
|
2070
|
+
| approvedAt | X | O | 判定に不要、表示のみ |
|
|
2071
|
+
| fiscalPeriod | X | O | 判定に不要、表示のみ |
|
|
2072
|
+
| totalDebitAmount | X | O | 集計値、表示のみ |
|
|
2073
|
+
|
|
2074
|
+
---
|
|
2075
|
+
|
|
2076
|
+
## 第30章:REST API と統合テスト
|
|
2077
|
+
|
|
2078
|
+
### 30.1 REST Controller(Inbound Adapter)
|
|
2079
|
+
|
|
2080
|
+
<details>
|
|
2081
|
+
<summary>コード例: JournalsController.java</summary>
|
|
2082
|
+
|
|
2083
|
+
```java
|
|
2084
|
+
package com.example.accounting.journal.adapter.inbound.rest.journals;
|
|
2085
|
+
|
|
2086
|
+
import com.example.accounting.journal.adapter.inbound.rest.journals.protocol.*;
|
|
2087
|
+
import com.example.accounting.journal.adapter.outbound.persistence.mapper.JournalEntryMapper;
|
|
2088
|
+
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntryCommands.*;
|
|
2089
|
+
import io.swagger.v3.oas.annotations.Operation;
|
|
2090
|
+
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
2091
|
+
import jakarta.validation.Valid;
|
|
2092
|
+
import org.axonframework.commandhandling.gateway.CommandGateway;
|
|
2093
|
+
import org.springframework.http.HttpStatus;
|
|
2094
|
+
import org.springframework.http.ResponseEntity;
|
|
2095
|
+
import org.springframework.web.bind.annotation.*;
|
|
2096
|
+
|
|
2097
|
+
import java.util.UUID;
|
|
2098
|
+
import java.util.concurrent.CompletableFuture;
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* JournalEntry REST Controller(Inbound Adapter)
|
|
2102
|
+
*/
|
|
2103
|
+
@RestController
|
|
2104
|
+
@RequestMapping("/api/journals")
|
|
2105
|
+
@Tag(name = "Journals", description = "Journal Entry management API")
|
|
2106
|
+
public class JournalsController {
|
|
2107
|
+
|
|
2108
|
+
private final CommandGateway commandGateway;
|
|
2109
|
+
private final JournalEntryMapper journalEntryMapper;
|
|
2110
|
+
|
|
2111
|
+
public JournalsController(CommandGateway commandGateway, JournalEntryMapper journalEntryMapper) {
|
|
2112
|
+
this.commandGateway = commandGateway;
|
|
2113
|
+
this.journalEntryMapper = journalEntryMapper;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// ========== Command Side (Write) ==========
|
|
2117
|
+
|
|
2118
|
+
@PostMapping
|
|
2119
|
+
@Operation(summary = "Create a new journal entry")
|
|
2120
|
+
public CompletableFuture<ResponseEntity<JournalEntryCreateResponse>> create(
|
|
2121
|
+
@Valid @RequestBody JournalEntryCreateRequest request
|
|
2122
|
+
) {
|
|
2123
|
+
var journalEntryId = UUID.randomUUID().toString();
|
|
2124
|
+
var lines = request.lines().stream()
|
|
2125
|
+
.map(line -> new JournalLineInput(
|
|
2126
|
+
line.accountCode(),
|
|
2127
|
+
line.accountName(),
|
|
2128
|
+
line.debitAmount(),
|
|
2129
|
+
line.creditAmount()
|
|
2130
|
+
))
|
|
2131
|
+
.toList();
|
|
2132
|
+
|
|
2133
|
+
var command = new CreateJournalEntryCommand(
|
|
2134
|
+
journalEntryId,
|
|
2135
|
+
request.transactionDate(),
|
|
2136
|
+
request.description(),
|
|
2137
|
+
lines,
|
|
2138
|
+
request.createdBy()
|
|
2139
|
+
);
|
|
2140
|
+
|
|
2141
|
+
return commandGateway.send(command)
|
|
2142
|
+
.thenApply(result -> ResponseEntity
|
|
2143
|
+
.status(HttpStatus.CREATED)
|
|
2144
|
+
.body(new JournalEntryCreateResponse(journalEntryId)));
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
@PostMapping("/{id}/submit")
|
|
2148
|
+
@Operation(summary = "Submit a journal entry for approval")
|
|
2149
|
+
public CompletableFuture<ResponseEntity<Void>> submit(
|
|
2150
|
+
@PathVariable String id,
|
|
2151
|
+
@Valid @RequestBody JournalEntrySubmitRequest request
|
|
2152
|
+
) {
|
|
2153
|
+
var command = new SubmitJournalEntryCommand(id, request.submittedBy());
|
|
2154
|
+
|
|
2155
|
+
return commandGateway.send(command)
|
|
2156
|
+
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
@PostMapping("/{id}/approve")
|
|
2160
|
+
@Operation(summary = "Approve a journal entry")
|
|
2161
|
+
public CompletableFuture<ResponseEntity<Void>> approve(
|
|
2162
|
+
@PathVariable String id,
|
|
2163
|
+
@Valid @RequestBody JournalEntryApproveRequest request
|
|
2164
|
+
) {
|
|
2165
|
+
var command = new ApproveJournalEntryCommand(id, request.approvedBy(), request.comment());
|
|
2166
|
+
|
|
2167
|
+
return commandGateway.send(command)
|
|
2168
|
+
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
@PostMapping("/{id}/reject")
|
|
2172
|
+
@Operation(summary = "Reject a journal entry")
|
|
2173
|
+
public CompletableFuture<ResponseEntity<Void>> reject(
|
|
2174
|
+
@PathVariable String id,
|
|
2175
|
+
@Valid @RequestBody JournalEntryRejectRequest request
|
|
2176
|
+
) {
|
|
2177
|
+
var command = new RejectJournalEntryCommand(id, request.rejectedBy(), request.reason());
|
|
2178
|
+
|
|
2179
|
+
return commandGateway.send(command)
|
|
2180
|
+
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
@PostMapping("/{id}/post")
|
|
2184
|
+
@Operation(summary = "Post a journal entry to the ledger")
|
|
2185
|
+
public CompletableFuture<ResponseEntity<Void>> post(
|
|
2186
|
+
@PathVariable String id,
|
|
2187
|
+
@Valid @RequestBody JournalEntryPostRequest request
|
|
2188
|
+
) {
|
|
2189
|
+
var command = new PostJournalEntryCommand(id, request.fiscalPeriod(), request.postedBy());
|
|
2190
|
+
|
|
2191
|
+
return commandGateway.send(command)
|
|
2192
|
+
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
@PostMapping("/{id}/cancel")
|
|
2196
|
+
@Operation(summary = "Cancel a journal entry")
|
|
2197
|
+
public CompletableFuture<ResponseEntity<Void>> cancel(
|
|
2198
|
+
@PathVariable String id,
|
|
2199
|
+
@Valid @RequestBody JournalEntryCancelRequest request
|
|
2200
|
+
) {
|
|
2201
|
+
var command = new CancelJournalEntryCommand(id, request.cancelledBy(), request.reason());
|
|
2202
|
+
|
|
2203
|
+
return commandGateway.send(command)
|
|
2204
|
+
.thenApply(result -> ResponseEntity.ok().<Void>build());
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// ========== Query Side (Read) ==========
|
|
2208
|
+
|
|
2209
|
+
@GetMapping("/{id}")
|
|
2210
|
+
@Operation(summary = "Get a journal entry by ID")
|
|
2211
|
+
public ResponseEntity<JournalEntryGetResponse> get(@PathVariable String id) {
|
|
2212
|
+
return journalEntryMapper.findById(id)
|
|
2213
|
+
.map(entity -> ResponseEntity.ok(JournalEntryGetResponse.from(entity)))
|
|
2214
|
+
.orElse(ResponseEntity.notFound().build());
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
@GetMapping
|
|
2218
|
+
@Operation(summary = "Get all journal entries")
|
|
2219
|
+
public ResponseEntity<JournalEntryListResponse> getAll() {
|
|
2220
|
+
var entities = journalEntryMapper.findAll();
|
|
2221
|
+
var items = entities.stream()
|
|
2222
|
+
.map(JournalEntryGetResponse::from)
|
|
2223
|
+
.toList();
|
|
2224
|
+
return ResponseEntity.ok(new JournalEntryListResponse(items));
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
@GetMapping("/status/{status}")
|
|
2228
|
+
@Operation(summary = "Get journal entries by status")
|
|
2229
|
+
public ResponseEntity<JournalEntryListResponse> getByStatus(@PathVariable String status) {
|
|
2230
|
+
var entities = journalEntryMapper.findByStatus(status);
|
|
2231
|
+
var items = entities.stream()
|
|
2232
|
+
.map(JournalEntryGetResponse::from)
|
|
2233
|
+
.toList();
|
|
2234
|
+
return ResponseEntity.ok(new JournalEntryListResponse(items));
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
</details>
|
|
2240
|
+
|
|
2241
|
+
---
|
|
2242
|
+
|
|
2243
|
+
### 30.2 API エンドポイント一覧
|
|
2244
|
+
|
|
2245
|
+
| メソッド | パス | 説明 | 種別 |
|
|
2246
|
+
|---------|------|------|------|
|
|
2247
|
+
| POST | `/api/journals` | 仕訳作成 | Command |
|
|
2248
|
+
| POST | `/api/journals/{id}/submit` | 承認申請 | Command |
|
|
2249
|
+
| POST | `/api/journals/{id}/approve` | 承認 | Command |
|
|
2250
|
+
| POST | `/api/journals/{id}/reject` | 却下 | Command |
|
|
2251
|
+
| POST | `/api/journals/{id}/post` | 転記 | Command |
|
|
2252
|
+
| POST | `/api/journals/{id}/cancel` | キャンセル | Command |
|
|
2253
|
+
| GET | `/api/journals` | 仕訳一覧取得 | Query |
|
|
2254
|
+
| GET | `/api/journals/{id}` | 仕訳詳細取得 | Query |
|
|
2255
|
+
| GET | `/api/journals/status/{status}` | ステータス別一覧 | Query |
|
|
2256
|
+
|
|
2257
|
+
---
|
|
2258
|
+
|
|
2259
|
+
### 30.3 Axon Test による単体テスト
|
|
2260
|
+
|
|
2261
|
+
<details>
|
|
2262
|
+
<summary>コード例: JournalEntryAggregateTest.java</summary>
|
|
2263
|
+
|
|
2264
|
+
```java
|
|
2265
|
+
package com.example.accounting.journal.application.aggregate;
|
|
2266
|
+
|
|
2267
|
+
import com.example.accounting.journal.api.events.*;
|
|
2268
|
+
import com.example.accounting.journal.api.events.JournalEntryCreatedEvent.JournalLineData;
|
|
2269
|
+
import com.example.accounting.journal.domain.model.aggregate.journal.JournalEntryCommands.*;
|
|
2270
|
+
import org.axonframework.test.aggregate.AggregateTestFixture;
|
|
2271
|
+
import org.axonframework.test.aggregate.FixtureConfiguration;
|
|
2272
|
+
import org.junit.jupiter.api.BeforeEach;
|
|
2273
|
+
import org.junit.jupiter.api.DisplayName;
|
|
2274
|
+
import org.junit.jupiter.api.Nested;
|
|
2275
|
+
import org.junit.jupiter.api.Test;
|
|
2276
|
+
|
|
2277
|
+
import java.math.BigDecimal;
|
|
2278
|
+
import java.time.LocalDate;
|
|
2279
|
+
import java.time.LocalDateTime;
|
|
2280
|
+
import java.util.List;
|
|
2281
|
+
|
|
2282
|
+
class JournalEntryAggregateTest {
|
|
2283
|
+
|
|
2284
|
+
private FixtureConfiguration<JournalEntryAggregateAdapter> fixture;
|
|
2285
|
+
|
|
2286
|
+
@BeforeEach
|
|
2287
|
+
void setUp() {
|
|
2288
|
+
fixture = new AggregateTestFixture<>(JournalEntryAggregateAdapter.class);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
@Nested
|
|
2292
|
+
@DisplayName("仕訳作成")
|
|
2293
|
+
class CreateJournalEntry {
|
|
2294
|
+
|
|
2295
|
+
@Test
|
|
2296
|
+
@DisplayName("借方・貸方が一致する仕訳を作成できる")
|
|
2297
|
+
void shouldCreateBalancedJournalEntry() {
|
|
2298
|
+
var lines = List.of(
|
|
2299
|
+
new JournalLineInput("1000", "現金", BigDecimal.valueOf(10000), BigDecimal.ZERO),
|
|
2300
|
+
new JournalLineInput("4000", "売上", BigDecimal.ZERO, BigDecimal.valueOf(10000))
|
|
2301
|
+
);
|
|
2302
|
+
|
|
2303
|
+
var command = new CreateJournalEntryCommand(
|
|
2304
|
+
"je-1",
|
|
2305
|
+
LocalDate.of(2024, 1, 15),
|
|
2306
|
+
"売上計上",
|
|
2307
|
+
lines,
|
|
2308
|
+
"user-1"
|
|
2309
|
+
);
|
|
2310
|
+
|
|
2311
|
+
fixture.givenNoPriorActivity()
|
|
2312
|
+
.when(command)
|
|
2313
|
+
.expectSuccessfulHandlerExecution()
|
|
2314
|
+
.expectEventsMatching(events ->
|
|
2315
|
+
events.getPayload() instanceof JournalEntryCreatedEvent
|
|
2316
|
+
);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
@Test
|
|
2320
|
+
@DisplayName("借方・貸方が一致しない場合はエラー")
|
|
2321
|
+
void shouldRejectUnbalancedJournalEntry() {
|
|
2322
|
+
var lines = List.of(
|
|
2323
|
+
new JournalLineInput("1000", "現金", BigDecimal.valueOf(10000), BigDecimal.ZERO),
|
|
2324
|
+
new JournalLineInput("4000", "売上", BigDecimal.ZERO, BigDecimal.valueOf(5000))
|
|
2325
|
+
);
|
|
2326
|
+
|
|
2327
|
+
var command = new CreateJournalEntryCommand(
|
|
2328
|
+
"je-1",
|
|
2329
|
+
LocalDate.of(2024, 1, 15),
|
|
2330
|
+
"不均衡な仕訳",
|
|
2331
|
+
lines,
|
|
2332
|
+
"user-1"
|
|
2333
|
+
);
|
|
2334
|
+
|
|
2335
|
+
fixture.givenNoPriorActivity()
|
|
2336
|
+
.when(command)
|
|
2337
|
+
.expectException(IllegalArgumentException.class);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
@Nested
|
|
2342
|
+
@DisplayName("仕訳承認")
|
|
2343
|
+
class ApproveJournalEntry {
|
|
2344
|
+
|
|
2345
|
+
@Test
|
|
2346
|
+
@DisplayName("承認待ちの仕訳を承認できる")
|
|
2347
|
+
void shouldApprovePendingJournalEntry() {
|
|
2348
|
+
var createdEvent = createSampleCreatedEvent();
|
|
2349
|
+
var submittedEvent = new JournalEntrySubmittedEvent("je-1", "user-1", LocalDateTime.now());
|
|
2350
|
+
|
|
2351
|
+
fixture.given(createdEvent, submittedEvent)
|
|
2352
|
+
.when(new ApproveJournalEntryCommand("je-1", "approver-1", "承認します"))
|
|
2353
|
+
.expectSuccessfulHandlerExecution()
|
|
2354
|
+
.expectEventsMatching(events ->
|
|
2355
|
+
events.getPayload() instanceof JournalEntryApprovedEvent
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
@Test
|
|
2360
|
+
@DisplayName("下書きの仕訳は承認できない")
|
|
2361
|
+
void shouldNotApproveDraftJournalEntry() {
|
|
2362
|
+
var createdEvent = createSampleCreatedEvent();
|
|
2363
|
+
|
|
2364
|
+
fixture.given(createdEvent)
|
|
2365
|
+
.when(new ApproveJournalEntryCommand("je-1", "approver-1", "承認します"))
|
|
2366
|
+
.expectException(IllegalStateException.class);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
@Nested
|
|
2371
|
+
@DisplayName("仕訳転記")
|
|
2372
|
+
class PostJournalEntry {
|
|
2373
|
+
|
|
2374
|
+
@Test
|
|
2375
|
+
@DisplayName("承認済みの仕訳を転記できる")
|
|
2376
|
+
void shouldPostApprovedJournalEntry() {
|
|
2377
|
+
var createdEvent = createSampleCreatedEvent();
|
|
2378
|
+
var submittedEvent = new JournalEntrySubmittedEvent("je-1", "user-1", LocalDateTime.now());
|
|
2379
|
+
var approvedEvent = new JournalEntryApprovedEvent("je-1", "approver-1", "OK", LocalDateTime.now());
|
|
2380
|
+
|
|
2381
|
+
fixture.given(createdEvent, submittedEvent, approvedEvent)
|
|
2382
|
+
.when(new PostJournalEntryCommand("je-1", "2024-01", "poster-1"))
|
|
2383
|
+
.expectSuccessfulHandlerExecution()
|
|
2384
|
+
.expectEventsMatching(events ->
|
|
2385
|
+
events.getPayload() instanceof JournalEntryPostedEvent
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
private JournalEntryCreatedEvent createSampleCreatedEvent() {
|
|
2391
|
+
var lines = List.of(
|
|
2392
|
+
new JournalLineData("1000", "現金", BigDecimal.valueOf(10000), BigDecimal.ZERO),
|
|
2393
|
+
new JournalLineData("4000", "売上", BigDecimal.ZERO, BigDecimal.valueOf(10000))
|
|
2394
|
+
);
|
|
2395
|
+
|
|
2396
|
+
return new JournalEntryCreatedEvent(
|
|
2397
|
+
"je-1",
|
|
2398
|
+
"JE-00000001",
|
|
2399
|
+
LocalDate.of(2024, 1, 15),
|
|
2400
|
+
"売上計上",
|
|
2401
|
+
lines,
|
|
2402
|
+
"user-1",
|
|
2403
|
+
LocalDateTime.now()
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
```
|
|
2408
|
+
|
|
2409
|
+
</details>
|
|
2410
|
+
|
|
2411
|
+
---
|
|
2412
|
+
|
|
2413
|
+
## Part 5-F のまとめ
|
|
2414
|
+
|
|
2415
|
+
### 実装した機能一覧
|
|
2416
|
+
|
|
2417
|
+
| 章 | 内容 | 主要技術 |
|
|
2418
|
+
|---|---|---|
|
|
2419
|
+
| **第26章: 基礎** | CQRS/ES アーキテクチャの基礎 | Axon Framework, Event Sourcing |
|
|
2420
|
+
| **第27章: ドメイン設計** | イベント、コマンド、ドメインモデル | sealed interface, record |
|
|
2421
|
+
| **第28章: Aggregate/Policy** | Aggregate Adapter, Choreography | @Aggregate, @EventHandler |
|
|
2422
|
+
| **第29章: Projection** | Read Model の更新、最小限フィールド | @EventHandler, MyBatis |
|
|
2423
|
+
| **第30章: REST API** | Inbound Adapter, テスト | CommandGateway, Axon Test |
|
|
2424
|
+
|
|
2425
|
+
### アーキテクチャの特徴
|
|
2426
|
+
|
|
2427
|
+
```plantuml
|
|
2428
|
+
@startuml cqrs_summary
|
|
2429
|
+
skinparam backgroundColor #FEFEFE
|
|
2430
|
+
|
|
2431
|
+
title CQRS/ES アーキテクチャ(財務会計システム)
|
|
2432
|
+
|
|
2433
|
+
package "Command Side" {
|
|
2434
|
+
[REST Controller] as RC1
|
|
2435
|
+
[CommandGateway] as CG
|
|
2436
|
+
[Aggregate Adapter] as AA
|
|
2437
|
+
[Domain Model] as DM
|
|
2438
|
+
[Event Store] as ES
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
package "Query Side" {
|
|
2442
|
+
[REST Controller] as RC2
|
|
2443
|
+
[MyBatis Mapper] as MB
|
|
2444
|
+
[Read Model DB] as DB
|
|
2445
|
+
[Projection] as P
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
RC1 --> CG : send(command)
|
|
2449
|
+
CG --> AA : @CommandHandler
|
|
2450
|
+
AA --> DM : ビジネスロジック
|
|
2451
|
+
DM --> AA : event
|
|
2452
|
+
AA --> ES : apply(event)
|
|
2453
|
+
ES --> P : @EventHandler
|
|
2454
|
+
P --> MB : insert/update
|
|
2455
|
+
MB --> DB : SQL
|
|
2456
|
+
|
|
2457
|
+
RC2 --> MB : query
|
|
2458
|
+
MB --> DB : SELECT
|
|
2459
|
+
|
|
2460
|
+
note right of DM
|
|
2461
|
+
純粋なドメインモデル
|
|
2462
|
+
Axon に依存しない
|
|
2463
|
+
end note
|
|
2464
|
+
|
|
2465
|
+
note right of P
|
|
2466
|
+
イベントを購読して
|
|
2467
|
+
MyBatis 経由で
|
|
2468
|
+
Read Model を更新
|
|
2469
|
+
end note
|
|
2470
|
+
|
|
2471
|
+
@enduml
|
|
2472
|
+
```
|
|
2473
|
+
|
|
2474
|
+
### 設計上の特徴
|
|
2475
|
+
|
|
2476
|
+
| 特徴 | 説明 |
|
|
2477
|
+
|------|------|
|
|
2478
|
+
| **ドメインモデルとフレームワークの分離** | `JournalEntry.java` は純粋なドメインモデル、`JournalEntryAggregateAdapter.java` は Axon 用アダプター |
|
|
2479
|
+
| **Choreography による疎結合** | 各 Context は独立して動作、イベントを介した非同期連携 |
|
|
2480
|
+
| **sealed interface による型安全性** | イベントの網羅性チェック、switch 式でコンパイル時検証 |
|
|
2481
|
+
| **record によるイミュータブル設計** | 状態変更は常に新しいインスタンスを生成 |
|
|
2482
|
+
| **最小限フィールドの原則** | ドメインモデルは状態遷移判定に必要な最小限のみ保持 |
|
|
2483
|
+
| **MyBatis による柔軟な SQL 制御** | Read Model の更新に MyBatis Mapper を使用 |
|
|
2484
|
+
| **ビジネスルールの集約** | 借方・貸方一致検証をドメインモデル内で実施 |
|
|
2485
|
+
|
|
2486
|
+
### 財務会計システムでの CQRS/ES の意義
|
|
2487
|
+
|
|
2488
|
+
| 観点 | 説明 |
|
|
2489
|
+
|------|------|
|
|
2490
|
+
| **監査証跡** | すべての仕訳変更履歴を完全に保持(SOX 法対応) |
|
|
2491
|
+
| **訂正仕訳** | 過去の仕訳を直接変更せず、訂正仕訳として追記 |
|
|
2492
|
+
| **期末決算** | 任意の時点での残高を再計算可能 |
|
|
2493
|
+
| **ワークフロー** | 仕訳の作成→承認→転記の状態管理 |
|
|
2494
|
+
| **内部統制** | 誰が・いつ・何を承認したかの記録 |
|
|
2495
|
+
|
|
2496
|
+
### 技術スタック
|
|
2497
|
+
|
|
2498
|
+
| カテゴリ | 技術 |
|
|
2499
|
+
|---------|------|
|
|
2500
|
+
| **言語** | Java 21 |
|
|
2501
|
+
| **フレームワーク** | Spring Boot 3.4.1 |
|
|
2502
|
+
| **CQRS/ES** | Axon Framework 4.10.3 |
|
|
2503
|
+
| **ORM** | MyBatis 3.0.4 |
|
|
2504
|
+
| **データベース** | H2 / PostgreSQL |
|
|
2505
|
+
| **テスト** | JUnit 5, Axon Test |
|
|
2506
|
+
|
|
2507
|
+
CQRS/ES は財務会計システムに特に適しています。すべての取引履歴を完全に保持し、監査要件に対応できるだけでなく、仕訳の承認ワークフローやビジネスルール(借方・貸方の一致)を明確に表現できます。MyBatis を使用することで、複雑な財務レポート(試算表、財務諸表など)のクエリにも柔軟に対応できます。
|