@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,1434 +1,1434 @@
|
|
|
1
|
-
# 第13章:API サービスの実装
|
|
2
|
-
|
|
3
|
-
本章では、販売管理システムのデータベース設計を外部から利用できるようにするため、RESTful API サービスを実装します。ヘキサゴナルアーキテクチャ(Ports and Adapters)を採用し、ドメインロジックを外部技術から分離した保守性の高い API を構築します。
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 13.1 ヘキサゴナルアーキテクチャの復習
|
|
8
|
-
|
|
9
|
-
### Ports and Adapters パターンの概要
|
|
10
|
-
|
|
11
|
-
ヘキサゴナルアーキテクチャ(Ports and Adapters パターン)は、Alistair Cockburn によって提唱された設計手法で、アプリケーションの中核となるビジネスロジック(ドメイン層)を外部の技術的詳細から完全に分離することを目的とします。
|
|
12
|
-
|
|
13
|
-
```plantuml
|
|
14
|
-
@startuml hexagonal_architecture_sales
|
|
15
|
-
!define RECTANGLE class
|
|
16
|
-
|
|
17
|
-
package "Hexagonal Architecture (販売管理API)" {
|
|
18
|
-
|
|
19
|
-
RECTANGLE "Application Core\n(Domain + Use Cases)" as core {
|
|
20
|
-
- Product (商品)
|
|
21
|
-
- Partner (取引先)
|
|
22
|
-
- Order (受注)
|
|
23
|
-
- Sales (売上)
|
|
24
|
-
- Invoice (請求)
|
|
25
|
-
- ProductUseCase
|
|
26
|
-
- OrderUseCase
|
|
27
|
-
- SalesUseCase
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
RECTANGLE "Input Adapters\n(Driving Side)" as input {
|
|
31
|
-
- Spring Controllers
|
|
32
|
-
- REST API Endpoints
|
|
33
|
-
- Request Validation
|
|
34
|
-
- Error Handling
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
RECTANGLE "Output Adapters\n(Driven Side)" as output {
|
|
38
|
-
- MyBatis Repository
|
|
39
|
-
- Database Access
|
|
40
|
-
- Entity Mapping
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
input --> core : "Input Ports\n(Use Cases)"
|
|
45
|
-
core --> output : "Output Ports\n(Repository Interfaces)"
|
|
46
|
-
|
|
47
|
-
note top of core
|
|
48
|
-
純粋なビジネスロジック
|
|
49
|
-
MyBatis に依存しない
|
|
50
|
-
高速でテスト可能
|
|
51
|
-
end note
|
|
52
|
-
|
|
53
|
-
note left of input
|
|
54
|
-
外部からアプリケーションを
|
|
55
|
-
駆動するアダプター
|
|
56
|
-
HTTP, REST等
|
|
57
|
-
end note
|
|
58
|
-
|
|
59
|
-
note right of output
|
|
60
|
-
アプリケーションが外部の
|
|
61
|
-
技術を使うためのアダプター
|
|
62
|
-
PostgreSQL, MyBatis等
|
|
63
|
-
end note
|
|
64
|
-
@enduml
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### ドメイン中心設計
|
|
68
|
-
|
|
69
|
-
ビジネスロジックを中心に据え、外部技術(DB、Web、UI など)を周辺に配置します。これにより、ビジネスルールが技術的な関心事から独立し、変更に強い設計が実現できます。
|
|
70
|
-
|
|
71
|
-
### 依存性の逆転
|
|
72
|
-
|
|
73
|
-
ドメイン層は外部に依存せず、外部がドメイン層に依存します。具体的には、リポジトリのインターフェース(Output Port)をドメイン層で定義し、その実装(Adapter)をインフラストラクチャ層に配置します。
|
|
74
|
-
|
|
75
|
-
### テスト容易性
|
|
76
|
-
|
|
77
|
-
モックやスタブを使った単体テストが容易になります。ドメインロジックを独立してテストでき、外部依存(データベースなど)なしで高速なテストが可能です。
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## 13.2 アーキテクチャ構造
|
|
82
|
-
|
|
83
|
-
### レイヤー構成
|
|
84
|
-
|
|
85
|
-
販売管理 API の実装では、以下のレイヤー構造を採用します。
|
|
86
|
-
|
|
87
|
-
```
|
|
88
|
-
src/main/java/com/example/sms/
|
|
89
|
-
├── domain/ # ドメイン層(純粋なビジネスロジック)
|
|
90
|
-
│ ├── model/ # ドメインモデル(エンティティ、値オブジェクト)
|
|
91
|
-
│ │ ├── department/ # 部門関連
|
|
92
|
-
│ │ ├── employee/ # 社員関連
|
|
93
|
-
│ │ ├── inventory/ # 在庫関連
|
|
94
|
-
│ │ ├── partner/ # 取引先関連
|
|
95
|
-
│ │ ├── product/ # 商品関連
|
|
96
|
-
│ │ ├── purchase/ # 仕入関連
|
|
97
|
-
│ │ └── sales/ # 販売関連
|
|
98
|
-
│ ├── exception/ # ドメイン例外
|
|
99
|
-
│ └── type/ # 値型定義
|
|
100
|
-
│
|
|
101
|
-
├── application/ # アプリケーション層
|
|
102
|
-
│ └── port/
|
|
103
|
-
│ └── out/ # Output Port(リポジトリインターフェース)
|
|
104
|
-
│
|
|
105
|
-
├── infrastructure/ # インフラストラクチャ層
|
|
106
|
-
│ ├── in/ # Input Adapter(受信アダプター)
|
|
107
|
-
│ │ ├── rest/ # REST API(Web実装)
|
|
108
|
-
│ │ │ ├── controller/ # REST Controller(Spring MVC)
|
|
109
|
-
│ │ │ ├── dto/ # Data Transfer Object
|
|
110
|
-
│ │ │ └── exception/ # Exception Handler
|
|
111
|
-
│ │ └── seed/ # Seed データ投入
|
|
112
|
-
│ └── out/ # Output Adapter(送信アダプター)
|
|
113
|
-
│ └── persistence/ # DB実装
|
|
114
|
-
│ ├── mapper/ # MyBatis Mapper
|
|
115
|
-
│ ├── repository/ # Repository実装
|
|
116
|
-
│ └── typehandler/ # 型ハンドラー
|
|
117
|
-
│
|
|
118
|
-
└── config/ # 設定クラス
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Domain 層
|
|
122
|
-
|
|
123
|
-
ビジネスルールとドメインモデルを定義します。外部技術に依存しない純粋な Java コードで構成されます。
|
|
124
|
-
|
|
125
|
-
<details>
|
|
126
|
-
<summary>Product.java(商品ドメインモデル)</summary>
|
|
127
|
-
|
|
128
|
-
```java
|
|
129
|
-
package com.example.sms.domain.model.product;
|
|
130
|
-
|
|
131
|
-
import lombok.AllArgsConstructor;
|
|
132
|
-
import lombok.Builder;
|
|
133
|
-
import lombok.Data;
|
|
134
|
-
import lombok.NoArgsConstructor;
|
|
135
|
-
|
|
136
|
-
import java.math.BigDecimal;
|
|
137
|
-
import java.time.LocalDateTime;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* 商品エンティティ.
|
|
141
|
-
*/
|
|
142
|
-
@Data
|
|
143
|
-
@Builder
|
|
144
|
-
@NoArgsConstructor
|
|
145
|
-
@AllArgsConstructor
|
|
146
|
-
public class Product {
|
|
147
|
-
private String productCode;
|
|
148
|
-
private String productFullName;
|
|
149
|
-
private String productName;
|
|
150
|
-
private String productNameKana;
|
|
151
|
-
private ProductCategory productCategory;
|
|
152
|
-
private String modelNumber;
|
|
153
|
-
@Builder.Default
|
|
154
|
-
private BigDecimal sellingPrice = BigDecimal.ZERO;
|
|
155
|
-
@Builder.Default
|
|
156
|
-
private BigDecimal purchasePrice = BigDecimal.ZERO;
|
|
157
|
-
private TaxCategory taxCategory;
|
|
158
|
-
private String classificationCode;
|
|
159
|
-
@Builder.Default
|
|
160
|
-
private boolean isMiscellaneous = false;
|
|
161
|
-
@Builder.Default
|
|
162
|
-
private boolean isInventoryManaged = true;
|
|
163
|
-
@Builder.Default
|
|
164
|
-
private boolean isInventoryAllocated = true;
|
|
165
|
-
private String supplierCode;
|
|
166
|
-
private String supplierBranchNumber;
|
|
167
|
-
private LocalDateTime createdAt;
|
|
168
|
-
private String createdBy;
|
|
169
|
-
private LocalDateTime updatedAt;
|
|
170
|
-
private String updatedBy;
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
</details>
|
|
175
|
-
|
|
176
|
-
### Application 層
|
|
177
|
-
|
|
178
|
-
ユースケースの実装とオーケストレーションを担当します。
|
|
179
|
-
|
|
180
|
-
<details>
|
|
181
|
-
<summary>ProductRepository.java(Output Port)</summary>
|
|
182
|
-
|
|
183
|
-
```java
|
|
184
|
-
package com.example.sms.application.port.out;
|
|
185
|
-
|
|
186
|
-
import com.example.sms.domain.model.product.Product;
|
|
187
|
-
import com.example.sms.domain.model.product.ProductCategory;
|
|
188
|
-
|
|
189
|
-
import java.util.List;
|
|
190
|
-
import java.util.Optional;
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* 商品リポジトリ(Output Port).
|
|
194
|
-
*/
|
|
195
|
-
public interface ProductRepository {
|
|
196
|
-
|
|
197
|
-
void save(Product product);
|
|
198
|
-
|
|
199
|
-
Optional<Product> findByCode(String productCode);
|
|
200
|
-
|
|
201
|
-
List<Product> findAll();
|
|
202
|
-
|
|
203
|
-
List<Product> findByCategory(ProductCategory category);
|
|
204
|
-
|
|
205
|
-
List<Product> findByClassificationCode(String classificationCode);
|
|
206
|
-
|
|
207
|
-
void update(Product product);
|
|
208
|
-
|
|
209
|
-
void deleteByCode(String productCode);
|
|
210
|
-
|
|
211
|
-
void deleteAll();
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
</details>
|
|
216
|
-
|
|
217
|
-
### Infrastructure 層
|
|
218
|
-
|
|
219
|
-
外部技術との接続を担当します。DB アクセス(MyBatis)や Web フレームワーク(Spring MVC)の実装を含みます。
|
|
220
|
-
|
|
221
|
-
### Input Port / Output Port の分離
|
|
222
|
-
|
|
223
|
-
| ポート | 役割 | 例 |
|
|
224
|
-
|--------|------|-----|
|
|
225
|
-
| Input Port | アプリケーションへの入力インターフェース | `ProductUseCase`, `OrderUseCase` |
|
|
226
|
-
| Output Port | アプリケーションからの出力インターフェース | `ProductRepository`, `OrderRepository` |
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## 13.3 マスタ API の実装
|
|
231
|
-
|
|
232
|
-
### 商品マスタ API(CRUD エンドポイント)
|
|
233
|
-
|
|
234
|
-
#### Output Port(リポジトリインターフェース)
|
|
235
|
-
|
|
236
|
-
<details>
|
|
237
|
-
<summary>ProductRepository.java</summary>
|
|
238
|
-
|
|
239
|
-
```java
|
|
240
|
-
package com.example.sms.application.port.out;
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* 商品リポジトリ(Output Port)
|
|
244
|
-
*/
|
|
245
|
-
public interface ProductRepository {
|
|
246
|
-
Product save(Product product);
|
|
247
|
-
List<Product> findAll();
|
|
248
|
-
Optional<Product> findByCode(String productCode, LocalDate effectiveDate);
|
|
249
|
-
List<Product> findByType(ProductType type);
|
|
250
|
-
List<Product> findByCategoryCode(String categoryCode);
|
|
251
|
-
void deleteByCode(String productCode, LocalDate effectiveDate);
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
</details>
|
|
256
|
-
|
|
257
|
-
#### Input Port(ユースケースインターフェース)
|
|
258
|
-
|
|
259
|
-
<details>
|
|
260
|
-
<summary>コマンドオブジェクト</summary>
|
|
261
|
-
|
|
262
|
-
```java
|
|
263
|
-
package com.example.sms.application.port.in;
|
|
264
|
-
|
|
265
|
-
public record CreateProductCommand(
|
|
266
|
-
String productCode,
|
|
267
|
-
String productName,
|
|
268
|
-
String categoryCode,
|
|
269
|
-
ProductType productType,
|
|
270
|
-
TaxType taxType,
|
|
271
|
-
BigDecimal sellingPrice,
|
|
272
|
-
BigDecimal purchasePrice
|
|
273
|
-
) {}
|
|
274
|
-
|
|
275
|
-
public record UpdateProductCommand(
|
|
276
|
-
String productCode,
|
|
277
|
-
String productName,
|
|
278
|
-
ProductType productType,
|
|
279
|
-
TaxType taxType,
|
|
280
|
-
BigDecimal sellingPrice,
|
|
281
|
-
BigDecimal purchasePrice
|
|
282
|
-
) {}
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
</details>
|
|
286
|
-
|
|
287
|
-
### TDD による実装(Red-Green-Refactor)
|
|
288
|
-
|
|
289
|
-
#### Red: 失敗するテストを書く
|
|
290
|
-
|
|
291
|
-
<details>
|
|
292
|
-
<summary>ProductControllerTest.java</summary>
|
|
293
|
-
|
|
294
|
-
```java
|
|
295
|
-
package com.example.sms.infrastructure.in.rest;
|
|
296
|
-
|
|
297
|
-
import com.example.sms.application.port.out.ProductClassificationRepository;
|
|
298
|
-
import com.example.sms.application.port.out.ProductRepository;
|
|
299
|
-
import com.example.sms.domain.model.product.Product;
|
|
300
|
-
import com.example.sms.domain.model.product.ProductCategory;
|
|
301
|
-
import com.example.sms.domain.model.product.ProductClassification;
|
|
302
|
-
import com.example.sms.domain.model.product.TaxCategory;
|
|
303
|
-
import com.example.sms.testsetup.BaseIntegrationTest;
|
|
304
|
-
import org.junit.jupiter.api.BeforeEach;
|
|
305
|
-
import org.junit.jupiter.api.DisplayName;
|
|
306
|
-
import org.junit.jupiter.api.Nested;
|
|
307
|
-
import org.junit.jupiter.api.Test;
|
|
308
|
-
import org.springframework.beans.factory.annotation.Autowired;
|
|
309
|
-
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
|
310
|
-
import org.springframework.http.MediaType;
|
|
311
|
-
import org.springframework.test.web.servlet.MockMvc;
|
|
312
|
-
|
|
313
|
-
import java.math.BigDecimal;
|
|
314
|
-
|
|
315
|
-
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
316
|
-
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
317
|
-
|
|
318
|
-
@AutoConfigureMockMvc
|
|
319
|
-
@DisplayName("商品マスタ API テスト")
|
|
320
|
-
class ProductControllerTest extends BaseIntegrationTest {
|
|
321
|
-
|
|
322
|
-
private static final String TEST_CLASSIFICATION_CODE = "CAT-TEST";
|
|
323
|
-
|
|
324
|
-
@Autowired
|
|
325
|
-
private MockMvc mockMvc;
|
|
326
|
-
|
|
327
|
-
@Autowired
|
|
328
|
-
private ProductRepository productRepository;
|
|
329
|
-
|
|
330
|
-
@Autowired
|
|
331
|
-
private ProductClassificationRepository productClassificationRepository;
|
|
332
|
-
|
|
333
|
-
@BeforeEach
|
|
334
|
-
void setUp() {
|
|
335
|
-
productRepository.deleteAll();
|
|
336
|
-
productClassificationRepository.deleteAll();
|
|
337
|
-
// テスト用の商品分類を作成
|
|
338
|
-
ProductClassification classification = ProductClassification.builder()
|
|
339
|
-
.classificationCode(TEST_CLASSIFICATION_CODE)
|
|
340
|
-
.classificationName("テスト分類")
|
|
341
|
-
.hierarchyLevel(1)
|
|
342
|
-
.classificationPath("/CAT-TEST")
|
|
343
|
-
.isLeaf(true)
|
|
344
|
-
.build();
|
|
345
|
-
productClassificationRepository.save(classification);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
@Nested
|
|
349
|
-
@DisplayName("POST /api/v1/products")
|
|
350
|
-
class CreateProduct {
|
|
351
|
-
|
|
352
|
-
@Test
|
|
353
|
-
@DisplayName("商品を登録できる")
|
|
354
|
-
void shouldCreateProduct() throws Exception {
|
|
355
|
-
var request = """
|
|
356
|
-
{
|
|
357
|
-
"productCode": "NEW-001",
|
|
358
|
-
"productFullName": "新規テスト商品 フルネーム",
|
|
359
|
-
"productName": "新規テスト商品",
|
|
360
|
-
"productNameKana": "シンキテストショウヒン",
|
|
361
|
-
"productCategory": "PRODUCT",
|
|
362
|
-
"modelNumber": "MODEL-001",
|
|
363
|
-
"sellingPrice": 5000,
|
|
364
|
-
"purchasePrice": 3000,
|
|
365
|
-
"taxCategory": "EXCLUSIVE",
|
|
366
|
-
"classificationCode": "CAT-TEST"
|
|
367
|
-
}
|
|
368
|
-
""";
|
|
369
|
-
|
|
370
|
-
mockMvc.perform(post("/api/v1/products")
|
|
371
|
-
.contentType(MediaType.APPLICATION_JSON)
|
|
372
|
-
.content(request))
|
|
373
|
-
.andExpect(status().isCreated())
|
|
374
|
-
.andExpect(jsonPath("$.productCode").value("NEW-001"))
|
|
375
|
-
.andExpect(jsonPath("$.productName").value("新規テスト商品"));
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
</details>
|
|
382
|
-
|
|
383
|
-
#### Green: テストを通す実装
|
|
384
|
-
|
|
385
|
-
<details>
|
|
386
|
-
<summary>ProductController.java</summary>
|
|
387
|
-
|
|
388
|
-
```java
|
|
389
|
-
package com.example.sms.infrastructure.in.rest.controller;
|
|
390
|
-
|
|
391
|
-
import com.example.sms.application.port.out.ProductRepository;
|
|
392
|
-
import com.example.sms.domain.exception.DuplicateProductException;
|
|
393
|
-
import com.example.sms.domain.exception.ProductNotFoundException;
|
|
394
|
-
import com.example.sms.domain.model.product.Product;
|
|
395
|
-
import com.example.sms.infrastructure.in.rest.dto.CreateProductRequest;
|
|
396
|
-
import com.example.sms.infrastructure.in.rest.dto.ProductResponse;
|
|
397
|
-
import com.example.sms.infrastructure.in.rest.dto.UpdateProductRequest;
|
|
398
|
-
import io.swagger.v3.oas.annotations.Operation;
|
|
399
|
-
import io.swagger.v3.oas.annotations.Parameter;
|
|
400
|
-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
401
|
-
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
402
|
-
import jakarta.validation.Valid;
|
|
403
|
-
import org.springframework.http.HttpStatus;
|
|
404
|
-
import org.springframework.http.ResponseEntity;
|
|
405
|
-
import org.springframework.web.bind.annotation.*;
|
|
406
|
-
|
|
407
|
-
import java.util.List;
|
|
408
|
-
|
|
409
|
-
@RestController
|
|
410
|
-
@RequestMapping("/api/v1/products")
|
|
411
|
-
@Tag(name = "products", description = "商品マスタ API")
|
|
412
|
-
public class ProductController {
|
|
413
|
-
|
|
414
|
-
private final ProductRepository productRepository;
|
|
415
|
-
|
|
416
|
-
public ProductController(ProductRepository productRepository) {
|
|
417
|
-
this.productRepository = productRepository;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
@GetMapping
|
|
421
|
-
@Operation(summary = "商品一覧の取得")
|
|
422
|
-
@ApiResponse(responseCode = "200", description = "商品一覧を返却")
|
|
423
|
-
public ResponseEntity<List<ProductResponse>> getAllProducts() {
|
|
424
|
-
List<Product> products = productRepository.findAll();
|
|
425
|
-
List<ProductResponse> responses = products.stream()
|
|
426
|
-
.map(ProductResponse::from)
|
|
427
|
-
.toList();
|
|
428
|
-
return ResponseEntity.ok(responses);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
@GetMapping("/{productCode}")
|
|
432
|
-
@Operation(summary = "商品の取得")
|
|
433
|
-
@ApiResponse(responseCode = "200", description = "商品を返却")
|
|
434
|
-
@ApiResponse(responseCode = "404", description = "商品が見つからない")
|
|
435
|
-
public ResponseEntity<ProductResponse> getProduct(
|
|
436
|
-
@Parameter(description = "商品コード")
|
|
437
|
-
@PathVariable String productCode) {
|
|
438
|
-
|
|
439
|
-
Product product = productRepository.findByCode(productCode)
|
|
440
|
-
.orElseThrow(() -> new ProductNotFoundException(productCode));
|
|
441
|
-
|
|
442
|
-
return ResponseEntity.ok(ProductResponse.from(product));
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
@PostMapping
|
|
446
|
-
@Operation(summary = "商品の登録")
|
|
447
|
-
@ApiResponse(responseCode = "201", description = "商品を登録")
|
|
448
|
-
@ApiResponse(responseCode = "409", description = "商品が既に存在する")
|
|
449
|
-
public ResponseEntity<ProductResponse> createProduct(
|
|
450
|
-
@Valid @RequestBody CreateProductRequest request) {
|
|
451
|
-
|
|
452
|
-
// 重複チェック
|
|
453
|
-
productRepository.findByCode(request.productCode())
|
|
454
|
-
.ifPresent(existing -> {
|
|
455
|
-
throw new DuplicateProductException(request.productCode());
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
Product product = Product.builder()
|
|
459
|
-
.productCode(request.productCode())
|
|
460
|
-
.productFullName(request.productFullName())
|
|
461
|
-
.productName(request.productName())
|
|
462
|
-
.productNameKana(request.productNameKana())
|
|
463
|
-
.productCategory(request.productCategory())
|
|
464
|
-
.modelNumber(request.modelNumber())
|
|
465
|
-
.sellingPrice(request.sellingPrice())
|
|
466
|
-
.purchasePrice(request.purchasePrice())
|
|
467
|
-
.taxCategory(request.taxCategory())
|
|
468
|
-
.classificationCode(request.classificationCode())
|
|
469
|
-
.build();
|
|
470
|
-
|
|
471
|
-
productRepository.save(product);
|
|
472
|
-
|
|
473
|
-
return ResponseEntity.status(HttpStatus.CREATED)
|
|
474
|
-
.body(ProductResponse.from(product));
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
</details>
|
|
480
|
-
|
|
481
|
-
### Controller・Service・Repository の実装
|
|
482
|
-
|
|
483
|
-
#### Application Service
|
|
484
|
-
|
|
485
|
-
<details>
|
|
486
|
-
<summary>ProductService.java</summary>
|
|
487
|
-
|
|
488
|
-
```java
|
|
489
|
-
@Service
|
|
490
|
-
@Transactional
|
|
491
|
-
public class ProductService implements ProductUseCase {
|
|
492
|
-
|
|
493
|
-
private final ProductRepository productRepository;
|
|
494
|
-
|
|
495
|
-
public ProductService(ProductRepository productRepository) {
|
|
496
|
-
this.productRepository = productRepository;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
@Override
|
|
500
|
-
public Product createProduct(CreateProductCommand command) {
|
|
501
|
-
LocalDate effectiveDate = LocalDate.now();
|
|
502
|
-
|
|
503
|
-
// 重複チェック
|
|
504
|
-
productRepository.findByCode(command.productCode(), effectiveDate)
|
|
505
|
-
.ifPresent(existing -> {
|
|
506
|
-
throw new DuplicateProductException(command.productCode());
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
Product product = new Product(
|
|
510
|
-
command.productCode(),
|
|
511
|
-
effectiveDate,
|
|
512
|
-
command.productName(),
|
|
513
|
-
command.categoryCode(),
|
|
514
|
-
effectiveDate,
|
|
515
|
-
command.productType(),
|
|
516
|
-
command.taxType(),
|
|
517
|
-
command.sellingPrice(),
|
|
518
|
-
command.purchasePrice()
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
return productRepository.save(product);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
@Override
|
|
525
|
-
@Transactional(readOnly = true)
|
|
526
|
-
public Product getProductByCode(String productCode) {
|
|
527
|
-
LocalDate effectiveDate = LocalDate.now();
|
|
528
|
-
return productRepository.findByCode(productCode, effectiveDate)
|
|
529
|
-
.orElseThrow(() -> new ProductNotFoundException(productCode));
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
</details>
|
|
535
|
-
|
|
536
|
-
### 顧客マスタ API(請求先・回収先の管理)
|
|
537
|
-
|
|
538
|
-
顧客マスタ API では、請求先と回収先の概念を正しく管理します。
|
|
539
|
-
|
|
540
|
-
<details>
|
|
541
|
-
<summary>CustomerController.java</summary>
|
|
542
|
-
|
|
543
|
-
```java
|
|
544
|
-
@RestController
|
|
545
|
-
@RequestMapping("/api/v1/customers")
|
|
546
|
-
@Tag(name = "customers", description = "顧客 API")
|
|
547
|
-
public class CustomerController {
|
|
548
|
-
|
|
549
|
-
private final CustomerUseCase customerUseCase;
|
|
550
|
-
|
|
551
|
-
@GetMapping("/{customerCode}")
|
|
552
|
-
@Operation(summary = "顧客の取得")
|
|
553
|
-
public ResponseEntity<CustomerResponse> getCustomer(
|
|
554
|
-
@PathVariable String customerCode) {
|
|
555
|
-
|
|
556
|
-
Customer customer = customerUseCase.getCustomerByCode(customerCode);
|
|
557
|
-
return ResponseEntity.ok(CustomerResponse.from(customer));
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
@GetMapping("/{customerCode}/billing-destinations")
|
|
561
|
-
@Operation(summary = "請求先一覧の取得")
|
|
562
|
-
public ResponseEntity<List<BillingDestinationResponse>> getBillingDestinations(
|
|
563
|
-
@PathVariable String customerCode) {
|
|
564
|
-
|
|
565
|
-
List<BillingDestination> destinations =
|
|
566
|
-
customerUseCase.getBillingDestinations(customerCode);
|
|
567
|
-
return ResponseEntity.ok(destinations.stream()
|
|
568
|
-
.map(BillingDestinationResponse::from)
|
|
569
|
-
.toList());
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
```
|
|
573
|
-
|
|
574
|
-
</details>
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
## 13.4 トランザクション API の実装
|
|
579
|
-
|
|
580
|
-
### 受注 API(受注登録・受注照会・受注明細照会)
|
|
581
|
-
|
|
582
|
-
<details>
|
|
583
|
-
<summary>OrderController.java</summary>
|
|
584
|
-
|
|
585
|
-
```java
|
|
586
|
-
@RestController
|
|
587
|
-
@RequestMapping("/api/v1/orders")
|
|
588
|
-
@Tag(name = "orders", description = "受注 API")
|
|
589
|
-
public class OrderController {
|
|
590
|
-
|
|
591
|
-
private final OrderUseCase orderUseCase;
|
|
592
|
-
|
|
593
|
-
@PostMapping
|
|
594
|
-
@Operation(summary = "受注の登録")
|
|
595
|
-
public ResponseEntity<OrderResponse> createOrder(
|
|
596
|
-
@Valid @RequestBody CreateOrderRequest request) {
|
|
597
|
-
|
|
598
|
-
Order order = orderUseCase.createOrder(request.toCommand());
|
|
599
|
-
return ResponseEntity.status(HttpStatus.CREATED)
|
|
600
|
-
.body(OrderResponse.from(order));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
@GetMapping("/{orderNumber}")
|
|
604
|
-
@Operation(summary = "受注の取得")
|
|
605
|
-
public ResponseEntity<OrderResponse> getOrder(
|
|
606
|
-
@PathVariable String orderNumber) {
|
|
607
|
-
|
|
608
|
-
Order order = orderUseCase.getOrderByNumber(orderNumber);
|
|
609
|
-
return ResponseEntity.ok(OrderResponse.from(order));
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
@GetMapping("/{orderNumber}/details")
|
|
613
|
-
@Operation(summary = "受注明細の取得")
|
|
614
|
-
public ResponseEntity<List<OrderDetailResponse>> getOrderDetails(
|
|
615
|
-
@PathVariable String orderNumber) {
|
|
616
|
-
|
|
617
|
-
List<OrderDetail> details = orderUseCase.getOrderDetails(orderNumber);
|
|
618
|
-
return ResponseEntity.ok(details.stream()
|
|
619
|
-
.map(OrderDetailResponse::from)
|
|
620
|
-
.toList());
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
```
|
|
624
|
-
|
|
625
|
-
</details>
|
|
626
|
-
|
|
627
|
-
### 出荷 API(出荷指示・出荷確定)
|
|
628
|
-
|
|
629
|
-
<details>
|
|
630
|
-
<summary>ShipmentController.java</summary>
|
|
631
|
-
|
|
632
|
-
```java
|
|
633
|
-
@RestController
|
|
634
|
-
@RequestMapping("/api/v1/shipments")
|
|
635
|
-
@Tag(name = "shipments", description = "出荷 API")
|
|
636
|
-
public class ShipmentController {
|
|
637
|
-
|
|
638
|
-
private final ShipmentUseCase shipmentUseCase;
|
|
639
|
-
|
|
640
|
-
@PostMapping("/instructions")
|
|
641
|
-
@Operation(summary = "出荷指示の登録")
|
|
642
|
-
public ResponseEntity<ShipmentInstructionResponse> createShipmentInstruction(
|
|
643
|
-
@Valid @RequestBody CreateShipmentInstructionRequest request) {
|
|
644
|
-
|
|
645
|
-
ShipmentInstruction instruction =
|
|
646
|
-
shipmentUseCase.createInstruction(request.toCommand());
|
|
647
|
-
return ResponseEntity.status(HttpStatus.CREATED)
|
|
648
|
-
.body(ShipmentInstructionResponse.from(instruction));
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
@PostMapping("/{shipmentNumber}/confirm")
|
|
652
|
-
@Operation(summary = "出荷確定")
|
|
653
|
-
public ResponseEntity<ShipmentResponse> confirmShipment(
|
|
654
|
-
@PathVariable String shipmentNumber) {
|
|
655
|
-
|
|
656
|
-
Shipment shipment = shipmentUseCase.confirmShipment(shipmentNumber);
|
|
657
|
-
return ResponseEntity.ok(ShipmentResponse.from(shipment));
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
```
|
|
661
|
-
|
|
662
|
-
</details>
|
|
663
|
-
|
|
664
|
-
### 売上 API(売上計上・売上照会)
|
|
665
|
-
|
|
666
|
-
<details>
|
|
667
|
-
<summary>SalesController.java</summary>
|
|
668
|
-
|
|
669
|
-
```java
|
|
670
|
-
@RestController
|
|
671
|
-
@RequestMapping("/api/v1/sales")
|
|
672
|
-
@Tag(name = "sales", description = "売上 API")
|
|
673
|
-
public class SalesController {
|
|
674
|
-
|
|
675
|
-
private final SalesUseCase salesUseCase;
|
|
676
|
-
|
|
677
|
-
@PostMapping
|
|
678
|
-
@Operation(summary = "売上計上")
|
|
679
|
-
public ResponseEntity<SalesResponse> recordSales(
|
|
680
|
-
@Valid @RequestBody RecordSalesRequest request) {
|
|
681
|
-
|
|
682
|
-
Sales sales = salesUseCase.recordSales(request.toCommand());
|
|
683
|
-
return ResponseEntity.status(HttpStatus.CREATED)
|
|
684
|
-
.body(SalesResponse.from(sales));
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
@GetMapping
|
|
688
|
-
@Operation(summary = "売上一覧の取得")
|
|
689
|
-
public ResponseEntity<List<SalesResponse>> getSalesList(
|
|
690
|
-
@RequestParam(required = false) LocalDate fromDate,
|
|
691
|
-
@RequestParam(required = false) LocalDate toDate,
|
|
692
|
-
@RequestParam(required = false) String customerCode) {
|
|
693
|
-
|
|
694
|
-
List<Sales> salesList = salesUseCase.getSalesList(fromDate, toDate, customerCode);
|
|
695
|
-
return ResponseEntity.ok(salesList.stream()
|
|
696
|
-
.map(SalesResponse::from)
|
|
697
|
-
.toList());
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
```
|
|
701
|
-
|
|
702
|
-
</details>
|
|
703
|
-
|
|
704
|
-
### 請求・入金 API(請求締め・入金消込)
|
|
705
|
-
|
|
706
|
-
<details>
|
|
707
|
-
<summary>InvoiceController.java / ReceiptController.java</summary>
|
|
708
|
-
|
|
709
|
-
```java
|
|
710
|
-
@RestController
|
|
711
|
-
@RequestMapping("/api/v1/invoices")
|
|
712
|
-
@Tag(name = "invoices", description = "請求 API")
|
|
713
|
-
public class InvoiceController {
|
|
714
|
-
|
|
715
|
-
private final InvoiceUseCase invoiceUseCase;
|
|
716
|
-
|
|
717
|
-
@PostMapping("/closing")
|
|
718
|
-
@Operation(summary = "請求締め処理")
|
|
719
|
-
public ResponseEntity<List<InvoiceResponse>> executeClosing(
|
|
720
|
-
@Valid @RequestBody InvoiceClosingRequest request) {
|
|
721
|
-
|
|
722
|
-
List<Invoice> invoices = invoiceUseCase.executeClosing(
|
|
723
|
-
request.closingDate(),
|
|
724
|
-
request.customerCode()
|
|
725
|
-
);
|
|
726
|
-
return ResponseEntity.ok(invoices.stream()
|
|
727
|
-
.map(InvoiceResponse::from)
|
|
728
|
-
.toList());
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
@RestController
|
|
733
|
-
@RequestMapping("/api/v1/receipts")
|
|
734
|
-
@Tag(name = "receipts", description = "入金 API")
|
|
735
|
-
public class ReceiptController {
|
|
736
|
-
|
|
737
|
-
private final ReceiptUseCase receiptUseCase;
|
|
738
|
-
|
|
739
|
-
@PostMapping
|
|
740
|
-
@Operation(summary = "入金登録")
|
|
741
|
-
public ResponseEntity<ReceiptResponse> recordReceipt(
|
|
742
|
-
@Valid @RequestBody RecordReceiptRequest request) {
|
|
743
|
-
|
|
744
|
-
Receipt receipt = receiptUseCase.recordReceipt(request.toCommand());
|
|
745
|
-
return ResponseEntity.status(HttpStatus.CREATED)
|
|
746
|
-
.body(ReceiptResponse.from(receipt));
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
@PostMapping("/{receiptNumber}/apply")
|
|
750
|
-
@Operation(summary = "入金消込")
|
|
751
|
-
public ResponseEntity<ReceiptResponse> applyReceipt(
|
|
752
|
-
@PathVariable String receiptNumber,
|
|
753
|
-
@Valid @RequestBody ApplyReceiptRequest request) {
|
|
754
|
-
|
|
755
|
-
Receipt receipt = receiptUseCase.applyToInvoices(
|
|
756
|
-
receiptNumber,
|
|
757
|
-
request.invoiceNumbers()
|
|
758
|
-
);
|
|
759
|
-
return ResponseEntity.ok(ReceiptResponse.from(receipt));
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
</details>
|
|
765
|
-
|
|
766
|
-
---
|
|
767
|
-
|
|
768
|
-
## 13.5 エラーハンドリング
|
|
769
|
-
|
|
770
|
-
### グローバル例外ハンドラー(@RestControllerAdvice)
|
|
771
|
-
|
|
772
|
-
<details>
|
|
773
|
-
<summary>GlobalExceptionHandler.java</summary>
|
|
774
|
-
|
|
775
|
-
```java
|
|
776
|
-
package com.example.sms.infrastructure.in.rest.exception;
|
|
777
|
-
|
|
778
|
-
import com.example.sms.domain.exception.BusinessRuleViolationException;
|
|
779
|
-
import com.example.sms.domain.exception.DuplicateResourceException;
|
|
780
|
-
import com.example.sms.domain.exception.OptimisticLockException;
|
|
781
|
-
import com.example.sms.domain.exception.ResourceNotFoundException;
|
|
782
|
-
import com.example.sms.infrastructure.in.rest.dto.ErrorResponse;
|
|
783
|
-
import com.example.sms.infrastructure.in.rest.dto.ValidationErrorResponse;
|
|
784
|
-
import org.slf4j.Logger;
|
|
785
|
-
import org.slf4j.LoggerFactory;
|
|
786
|
-
import org.springframework.http.HttpStatus;
|
|
787
|
-
import org.springframework.http.ResponseEntity;
|
|
788
|
-
import org.springframework.validation.FieldError;
|
|
789
|
-
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
790
|
-
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
791
|
-
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
792
|
-
|
|
793
|
-
import java.time.Instant;
|
|
794
|
-
import java.util.Map;
|
|
795
|
-
import java.util.concurrent.ConcurrentHashMap;
|
|
796
|
-
|
|
797
|
-
@RestControllerAdvice
|
|
798
|
-
public class GlobalExceptionHandler {
|
|
799
|
-
|
|
800
|
-
private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
801
|
-
|
|
802
|
-
@ExceptionHandler(ResourceNotFoundException.class)
|
|
803
|
-
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException e) {
|
|
804
|
-
LOG.warn("Resource not found: {}", e.getMessage());
|
|
805
|
-
|
|
806
|
-
ErrorResponse response = new ErrorResponse(
|
|
807
|
-
HttpStatus.NOT_FOUND.value(),
|
|
808
|
-
"NOT_FOUND",
|
|
809
|
-
e.getMessage(),
|
|
810
|
-
Instant.now()
|
|
811
|
-
);
|
|
812
|
-
|
|
813
|
-
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
@ExceptionHandler(DuplicateResourceException.class)
|
|
817
|
-
public ResponseEntity<ErrorResponse> handleDuplicateResource(DuplicateResourceException e) {
|
|
818
|
-
LOG.warn("Duplicate resource: {}", e.getMessage());
|
|
819
|
-
|
|
820
|
-
ErrorResponse response = new ErrorResponse(
|
|
821
|
-
HttpStatus.CONFLICT.value(),
|
|
822
|
-
"CONFLICT",
|
|
823
|
-
e.getMessage(),
|
|
824
|
-
Instant.now()
|
|
825
|
-
);
|
|
826
|
-
|
|
827
|
-
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
@ExceptionHandler(BusinessRuleViolationException.class)
|
|
831
|
-
public ResponseEntity<ErrorResponse> handleBusinessRuleViolation(BusinessRuleViolationException e) {
|
|
832
|
-
LOG.warn("Business rule violation: {}", e.getMessage());
|
|
833
|
-
|
|
834
|
-
ErrorResponse response = new ErrorResponse(
|
|
835
|
-
HttpStatus.UNPROCESSABLE_ENTITY.value(),
|
|
836
|
-
"BUSINESS_RULE_VIOLATION",
|
|
837
|
-
e.getMessage(),
|
|
838
|
-
Instant.now()
|
|
839
|
-
);
|
|
840
|
-
|
|
841
|
-
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response);
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
@ExceptionHandler(OptimisticLockException.class)
|
|
845
|
-
public ResponseEntity<ErrorResponse> handleOptimisticLock(OptimisticLockException e) {
|
|
846
|
-
LOG.warn("Optimistic lock exception: {}", e.getMessage());
|
|
847
|
-
|
|
848
|
-
ErrorResponse response = new ErrorResponse(
|
|
849
|
-
HttpStatus.CONFLICT.value(),
|
|
850
|
-
"OPTIMISTIC_LOCK",
|
|
851
|
-
e.getMessage(),
|
|
852
|
-
Instant.now()
|
|
853
|
-
);
|
|
854
|
-
|
|
855
|
-
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
859
|
-
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(
|
|
860
|
-
MethodArgumentNotValidException e) {
|
|
861
|
-
|
|
862
|
-
Map<String, String> errors = new ConcurrentHashMap<>();
|
|
863
|
-
e.getBindingResult().getAllErrors().forEach(error -> {
|
|
864
|
-
String fieldName = ((FieldError) error).getField();
|
|
865
|
-
String errorMessage = error.getDefaultMessage();
|
|
866
|
-
errors.put(fieldName, errorMessage);
|
|
867
|
-
});
|
|
868
|
-
|
|
869
|
-
ValidationErrorResponse response = new ValidationErrorResponse(
|
|
870
|
-
HttpStatus.BAD_REQUEST.value(),
|
|
871
|
-
"VALIDATION_ERROR",
|
|
872
|
-
"入力値が不正です",
|
|
873
|
-
errors,
|
|
874
|
-
Instant.now()
|
|
875
|
-
);
|
|
876
|
-
|
|
877
|
-
return ResponseEntity.badRequest().body(response);
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
@ExceptionHandler(Exception.class)
|
|
881
|
-
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
|
|
882
|
-
LOG.error("Unexpected error occurred", e);
|
|
883
|
-
|
|
884
|
-
ErrorResponse response = new ErrorResponse(
|
|
885
|
-
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
|
886
|
-
"INTERNAL_ERROR",
|
|
887
|
-
"システムエラーが発生しました",
|
|
888
|
-
Instant.now()
|
|
889
|
-
);
|
|
890
|
-
|
|
891
|
-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
```
|
|
895
|
-
|
|
896
|
-
</details>
|
|
897
|
-
|
|
898
|
-
### ドメイン例外の定義と変換
|
|
899
|
-
|
|
900
|
-
<details>
|
|
901
|
-
<summary>ドメイン例外クラス</summary>
|
|
902
|
-
|
|
903
|
-
```java
|
|
904
|
-
package com.example.sms.domain.exception;
|
|
905
|
-
|
|
906
|
-
public abstract class ResourceNotFoundException extends RuntimeException {
|
|
907
|
-
protected ResourceNotFoundException(String message) {
|
|
908
|
-
super(message);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
public class ProductNotFoundException extends ResourceNotFoundException {
|
|
913
|
-
public ProductNotFoundException(String productCode) {
|
|
914
|
-
super("商品が見つかりません: " + productCode);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
public class OrderNotFoundException extends ResourceNotFoundException {
|
|
919
|
-
public OrderNotFoundException(String orderNumber) {
|
|
920
|
-
super("受注が見つかりません: " + orderNumber);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
public abstract class BusinessRuleViolationException extends RuntimeException {
|
|
925
|
-
protected BusinessRuleViolationException(String message) {
|
|
926
|
-
super(message);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
public class CreditLimitExceededException extends BusinessRuleViolationException {
|
|
931
|
-
public CreditLimitExceededException(String customerCode, BigDecimal limit, BigDecimal amount) {
|
|
932
|
-
super(String.format(
|
|
933
|
-
"与信限度額を超過しています(顧客: %s, 限度額: %s, 申請額: %s)",
|
|
934
|
-
customerCode, limit, amount
|
|
935
|
-
));
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
public class InsufficientInventoryException extends BusinessRuleViolationException {
|
|
940
|
-
public InsufficientInventoryException(String productCode, BigDecimal available, BigDecimal requested) {
|
|
941
|
-
super(String.format(
|
|
942
|
-
"在庫が不足しています(商品: %s, 有効在庫: %s, 要求数量: %s)",
|
|
943
|
-
productCode, available, requested
|
|
944
|
-
));
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
```
|
|
948
|
-
|
|
949
|
-
</details>
|
|
950
|
-
|
|
951
|
-
### ProblemDetail によるエラーレスポンス
|
|
952
|
-
|
|
953
|
-
RFC 7807 に準拠した ProblemDetail 形式でエラーを返却することも可能です。
|
|
954
|
-
|
|
955
|
-
<details>
|
|
956
|
-
<summary>エラーレスポンス DTO</summary>
|
|
957
|
-
|
|
958
|
-
```java
|
|
959
|
-
public record ErrorResponse(
|
|
960
|
-
int status,
|
|
961
|
-
String code,
|
|
962
|
-
String message,
|
|
963
|
-
Instant timestamp
|
|
964
|
-
) {}
|
|
965
|
-
|
|
966
|
-
public record ValidationErrorResponse(
|
|
967
|
-
int status,
|
|
968
|
-
String code,
|
|
969
|
-
String message,
|
|
970
|
-
Map<String, String> errors,
|
|
971
|
-
Instant timestamp
|
|
972
|
-
) {}
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
</details>
|
|
976
|
-
|
|
977
|
-
### バリデーションエラーの処理
|
|
978
|
-
|
|
979
|
-
リクエスト DTO に Bean Validation アノテーションを付与し、バリデーションエラーを統一的に処理します。
|
|
980
|
-
|
|
981
|
-
<details>
|
|
982
|
-
<summary>CreateProductRequest.java</summary>
|
|
983
|
-
|
|
984
|
-
```java
|
|
985
|
-
public record CreateProductRequest(
|
|
986
|
-
@NotBlank(message = "商品コードは必須です")
|
|
987
|
-
String productCode,
|
|
988
|
-
|
|
989
|
-
@NotBlank(message = "商品名は必須です")
|
|
990
|
-
String productName,
|
|
991
|
-
|
|
992
|
-
@NotBlank(message = "商品分類コードは必須です")
|
|
993
|
-
String categoryCode,
|
|
994
|
-
|
|
995
|
-
@NotNull(message = "商品区分は必須です")
|
|
996
|
-
ProductType productType,
|
|
997
|
-
|
|
998
|
-
@NotNull(message = "税区分は必須です")
|
|
999
|
-
TaxType taxType,
|
|
1000
|
-
|
|
1001
|
-
@NotNull(message = "販売単価は必須です")
|
|
1002
|
-
@Positive(message = "販売単価は正の数である必要があります")
|
|
1003
|
-
BigDecimal sellingPrice,
|
|
1004
|
-
|
|
1005
|
-
@NotNull(message = "仕入単価は必須です")
|
|
1006
|
-
@Positive(message = "仕入単価は正の数である必要があります")
|
|
1007
|
-
BigDecimal purchasePrice
|
|
1008
|
-
) {}
|
|
1009
|
-
```
|
|
1010
|
-
|
|
1011
|
-
</details>
|
|
1012
|
-
|
|
1013
|
-
---
|
|
1014
|
-
|
|
1015
|
-
## 13.6 API ドキュメント
|
|
1016
|
-
|
|
1017
|
-
### OpenAPI / Swagger の設定
|
|
1018
|
-
|
|
1019
|
-
<details>
|
|
1020
|
-
<summary>OpenApiConfig.java</summary>
|
|
1021
|
-
|
|
1022
|
-
```java
|
|
1023
|
-
@Configuration
|
|
1024
|
-
public class OpenApiConfig {
|
|
1025
|
-
|
|
1026
|
-
@Bean
|
|
1027
|
-
public OpenAPI openAPI() {
|
|
1028
|
-
return new OpenAPI()
|
|
1029
|
-
.info(new Info()
|
|
1030
|
-
.title("販売管理システム API")
|
|
1031
|
-
.description("TDD で育てる販売管理システムの API ドキュメント")
|
|
1032
|
-
.version("1.0.0"))
|
|
1033
|
-
.servers(List.of(
|
|
1034
|
-
new Server()
|
|
1035
|
-
.url("http://localhost:8080")
|
|
1036
|
-
.description("開発サーバー")))
|
|
1037
|
-
.tags(List.of(
|
|
1038
|
-
new Tag().name("products").description("商品マスタ API"),
|
|
1039
|
-
new Tag().name("partners").description("取引先マスタ API"),
|
|
1040
|
-
new Tag().name("customers").description("顧客 API"),
|
|
1041
|
-
new Tag().name("orders").description("受注 API"),
|
|
1042
|
-
new Tag().name("shipments").description("出荷 API"),
|
|
1043
|
-
new Tag().name("sales").description("売上 API"),
|
|
1044
|
-
new Tag().name("invoices").description("請求 API"),
|
|
1045
|
-
new Tag().name("receipts").description("入金 API")));
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
```
|
|
1049
|
-
|
|
1050
|
-
</details>
|
|
1051
|
-
|
|
1052
|
-
### エンドポイントの文書化
|
|
1053
|
-
|
|
1054
|
-
<details>
|
|
1055
|
-
<summary>アノテーションによるドキュメント化</summary>
|
|
1056
|
-
|
|
1057
|
-
```java
|
|
1058
|
-
@GetMapping("/{productCode}")
|
|
1059
|
-
@Operation(
|
|
1060
|
-
summary = "商品の取得",
|
|
1061
|
-
description = "商品コードを指定して商品情報を取得します"
|
|
1062
|
-
)
|
|
1063
|
-
@ApiResponse(responseCode = "200", description = "商品を返却")
|
|
1064
|
-
@ApiResponse(responseCode = "404", description = "商品が見つからない")
|
|
1065
|
-
public ResponseEntity<ProductResponse> getProduct(
|
|
1066
|
-
@Parameter(description = "商品コード", example = "BEEF-001")
|
|
1067
|
-
@PathVariable String productCode) {
|
|
1068
|
-
// ...
|
|
1069
|
-
}
|
|
1070
|
-
```
|
|
1071
|
-
|
|
1072
|
-
</details>
|
|
1073
|
-
|
|
1074
|
-
### リクエスト・レスポンスのスキーマ定義
|
|
1075
|
-
|
|
1076
|
-
application.yml に Swagger UI の設定を追加します。
|
|
1077
|
-
|
|
1078
|
-
<details>
|
|
1079
|
-
<summary>application.yml(Swagger 設定)</summary>
|
|
1080
|
-
|
|
1081
|
-
```yaml
|
|
1082
|
-
springdoc:
|
|
1083
|
-
swagger-ui:
|
|
1084
|
-
path: /swagger-ui.html
|
|
1085
|
-
tags-sorter: alpha
|
|
1086
|
-
operations-sorter: alpha
|
|
1087
|
-
api-docs:
|
|
1088
|
-
path: /api-docs
|
|
1089
|
-
```
|
|
1090
|
-
|
|
1091
|
-
</details>
|
|
1092
|
-
|
|
1093
|
-
Swagger UI にアクセスすると、API ドキュメントが自動生成されます。
|
|
1094
|
-
|
|
1095
|
-
---
|
|
1096
|
-
|
|
1097
|
-
## 13.7 API インテグレーションテスト
|
|
1098
|
-
|
|
1099
|
-
### テストコンテナによる統合テスト環境
|
|
1100
|
-
|
|
1101
|
-
Testcontainers を使用して、実際のデータベース環境で API の統合テストを実行します。
|
|
1102
|
-
|
|
1103
|
-
<details>
|
|
1104
|
-
<summary>テスト基盤クラス</summary>
|
|
1105
|
-
|
|
1106
|
-
```java
|
|
1107
|
-
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1108
|
-
@Testcontainers
|
|
1109
|
-
@ActiveProfiles("test")
|
|
1110
|
-
public abstract class IntegrationTestBase {
|
|
1111
|
-
|
|
1112
|
-
@Container
|
|
1113
|
-
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
|
|
1114
|
-
.withDatabaseName("sales_test")
|
|
1115
|
-
.withUsername("test")
|
|
1116
|
-
.withPassword("test");
|
|
1117
|
-
|
|
1118
|
-
@DynamicPropertySource
|
|
1119
|
-
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
1120
|
-
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
1121
|
-
registry.add("spring.datasource.username", postgres::getUsername);
|
|
1122
|
-
registry.add("spring.datasource.password", postgres::getPassword);
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
@Autowired
|
|
1126
|
-
protected TestRestTemplate restTemplate;
|
|
1127
|
-
|
|
1128
|
-
@Autowired
|
|
1129
|
-
protected ObjectMapper objectMapper;
|
|
1130
|
-
}
|
|
1131
|
-
```
|
|
1132
|
-
|
|
1133
|
-
</details>
|
|
1134
|
-
|
|
1135
|
-
### REST API エンドポイントのテスト
|
|
1136
|
-
|
|
1137
|
-
<details>
|
|
1138
|
-
<summary>ProductApiIntegrationTest.java</summary>
|
|
1139
|
-
|
|
1140
|
-
```java
|
|
1141
|
-
@DisplayName("商品 API 統合テスト")
|
|
1142
|
-
class ProductApiIntegrationTest extends IntegrationTestBase {
|
|
1143
|
-
|
|
1144
|
-
@Nested
|
|
1145
|
-
@DisplayName("商品登録・取得フロー")
|
|
1146
|
-
class ProductCrudFlow {
|
|
1147
|
-
|
|
1148
|
-
@Test
|
|
1149
|
-
@DisplayName("商品を登録して取得できる")
|
|
1150
|
-
void shouldCreateAndRetrieveProduct() {
|
|
1151
|
-
// Given: 商品登録リクエスト
|
|
1152
|
-
var createRequest = new CreateProductRequest(
|
|
1153
|
-
"BEEF-INT-001",
|
|
1154
|
-
"統合テスト用商品",
|
|
1155
|
-
"CAT-BEEF",
|
|
1156
|
-
ProductType.PRODUCT,
|
|
1157
|
-
TaxType.STANDARD,
|
|
1158
|
-
new BigDecimal("5000"),
|
|
1159
|
-
new BigDecimal("3000")
|
|
1160
|
-
);
|
|
1161
|
-
|
|
1162
|
-
// When: 商品を登録
|
|
1163
|
-
ResponseEntity<ProductResponse> createResponse = restTemplate.postForEntity(
|
|
1164
|
-
"/api/v1/products",
|
|
1165
|
-
createRequest,
|
|
1166
|
-
ProductResponse.class
|
|
1167
|
-
);
|
|
1168
|
-
|
|
1169
|
-
// Then: 登録成功
|
|
1170
|
-
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1171
|
-
assertThat(createResponse.getBody()).isNotNull();
|
|
1172
|
-
assertThat(createResponse.getBody().productCode()).isEqualTo("BEEF-INT-001");
|
|
1173
|
-
|
|
1174
|
-
// When: 登録した商品を取得
|
|
1175
|
-
ResponseEntity<ProductResponse> getResponse = restTemplate.getForEntity(
|
|
1176
|
-
"/api/v1/products/BEEF-INT-001",
|
|
1177
|
-
ProductResponse.class
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
// Then: 取得成功
|
|
1181
|
-
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1182
|
-
assertThat(getResponse.getBody().productName()).isEqualTo("統合テスト用商品");
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
@Test
|
|
1186
|
-
@DisplayName("存在しない商品を取得すると404エラー")
|
|
1187
|
-
void shouldReturn404WhenProductNotFound() {
|
|
1188
|
-
// When
|
|
1189
|
-
ResponseEntity<ErrorResponse> response = restTemplate.getForEntity(
|
|
1190
|
-
"/api/v1/products/NOT-EXIST",
|
|
1191
|
-
ErrorResponse.class
|
|
1192
|
-
);
|
|
1193
|
-
|
|
1194
|
-
// Then
|
|
1195
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
|
1196
|
-
assertThat(response.getBody().code()).isEqualTo("NOT_FOUND");
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
```
|
|
1201
|
-
|
|
1202
|
-
</details>
|
|
1203
|
-
|
|
1204
|
-
### データベース状態の検証
|
|
1205
|
-
|
|
1206
|
-
<details>
|
|
1207
|
-
<summary>OrderApiIntegrationTest.java</summary>
|
|
1208
|
-
|
|
1209
|
-
```java
|
|
1210
|
-
@DisplayName("受注 API 統合テスト")
|
|
1211
|
-
class OrderApiIntegrationTest extends IntegrationTestBase {
|
|
1212
|
-
|
|
1213
|
-
@Autowired
|
|
1214
|
-
private JdbcTemplate jdbcTemplate;
|
|
1215
|
-
|
|
1216
|
-
@BeforeEach
|
|
1217
|
-
void setUp() {
|
|
1218
|
-
// テストデータのセットアップ
|
|
1219
|
-
jdbcTemplate.execute("""
|
|
1220
|
-
INSERT INTO 商品 (商品コード, 適用開始日, 商品名, 販売単価, 仕入単価)
|
|
1221
|
-
VALUES ('BEEF-001', CURRENT_DATE, 'テスト牛肉', 5000, 3000)
|
|
1222
|
-
ON CONFLICT DO NOTHING
|
|
1223
|
-
""");
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
@Test
|
|
1227
|
-
@DisplayName("受注登録時にデータベースに正しく保存される")
|
|
1228
|
-
void shouldPersistOrderToDatabase() {
|
|
1229
|
-
// Given
|
|
1230
|
-
var createRequest = new CreateOrderRequest(
|
|
1231
|
-
"CUS-001",
|
|
1232
|
-
LocalDate.now(),
|
|
1233
|
-
List.of(new OrderDetailRequest("BEEF-001", 10, new BigDecimal("5000")))
|
|
1234
|
-
);
|
|
1235
|
-
|
|
1236
|
-
// When
|
|
1237
|
-
ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
|
|
1238
|
-
"/api/v1/orders",
|
|
1239
|
-
createRequest,
|
|
1240
|
-
OrderResponse.class
|
|
1241
|
-
);
|
|
1242
|
-
|
|
1243
|
-
// Then: API レスポンスの検証
|
|
1244
|
-
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1245
|
-
String orderNumber = response.getBody().orderNumber();
|
|
1246
|
-
|
|
1247
|
-
// Then: データベース状態の検証
|
|
1248
|
-
Integer count = jdbcTemplate.queryForObject(
|
|
1249
|
-
"SELECT COUNT(*) FROM 受注 WHERE 受注番号 = ?",
|
|
1250
|
-
Integer.class,
|
|
1251
|
-
orderNumber
|
|
1252
|
-
);
|
|
1253
|
-
assertThat(count).isEqualTo(1);
|
|
1254
|
-
|
|
1255
|
-
// Then: 明細データの検証
|
|
1256
|
-
Integer detailCount = jdbcTemplate.queryForObject(
|
|
1257
|
-
"SELECT COUNT(*) FROM 受注明細 WHERE 受注番号 = ?",
|
|
1258
|
-
Integer.class,
|
|
1259
|
-
orderNumber
|
|
1260
|
-
);
|
|
1261
|
-
assertThat(detailCount).isEqualTo(1);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
```
|
|
1265
|
-
|
|
1266
|
-
</details>
|
|
1267
|
-
|
|
1268
|
-
### テストデータのセットアップとクリーンアップ
|
|
1269
|
-
|
|
1270
|
-
<details>
|
|
1271
|
-
<summary>TestDataManager.java</summary>
|
|
1272
|
-
|
|
1273
|
-
```java
|
|
1274
|
-
@Component
|
|
1275
|
-
@Profile("test")
|
|
1276
|
-
public class TestDataManager {
|
|
1277
|
-
|
|
1278
|
-
private final JdbcTemplate jdbcTemplate;
|
|
1279
|
-
|
|
1280
|
-
public TestDataManager(JdbcTemplate jdbcTemplate) {
|
|
1281
|
-
this.jdbcTemplate = jdbcTemplate;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
@Transactional
|
|
1285
|
-
public void setupMasterData() {
|
|
1286
|
-
// 商品分類マスタ
|
|
1287
|
-
jdbcTemplate.execute("""
|
|
1288
|
-
INSERT INTO 商品分類 (商品分類コード, 適用開始日, 商品分類名)
|
|
1289
|
-
VALUES ('CAT-BEEF', CURRENT_DATE, '牛肉'),
|
|
1290
|
-
('CAT-PORK', CURRENT_DATE, '豚肉')
|
|
1291
|
-
ON CONFLICT DO NOTHING
|
|
1292
|
-
""");
|
|
1293
|
-
|
|
1294
|
-
// 取引先マスタ
|
|
1295
|
-
jdbcTemplate.execute("""
|
|
1296
|
-
INSERT INTO 取引先 (取引先コード, 取引先名, 取引先区分コード)
|
|
1297
|
-
VALUES ('CUS-001', 'テスト顧客', '1'),
|
|
1298
|
-
('SUP-001', 'テスト仕入先', '2')
|
|
1299
|
-
ON CONFLICT DO NOTHING
|
|
1300
|
-
""");
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
@Transactional
|
|
1304
|
-
public void cleanupTransactionData() {
|
|
1305
|
-
// トランザクションデータのクリーンアップ(外部キー制約の順序に注意)
|
|
1306
|
-
jdbcTemplate.execute("DELETE FROM 入金明細");
|
|
1307
|
-
jdbcTemplate.execute("DELETE FROM 入金");
|
|
1308
|
-
jdbcTemplate.execute("DELETE FROM 請求明細");
|
|
1309
|
-
jdbcTemplate.execute("DELETE FROM 請求");
|
|
1310
|
-
jdbcTemplate.execute("DELETE FROM 売上明細");
|
|
1311
|
-
jdbcTemplate.execute("DELETE FROM 売上");
|
|
1312
|
-
jdbcTemplate.execute("DELETE FROM 出荷明細");
|
|
1313
|
-
jdbcTemplate.execute("DELETE FROM 出荷");
|
|
1314
|
-
jdbcTemplate.execute("DELETE FROM 受注明細");
|
|
1315
|
-
jdbcTemplate.execute("DELETE FROM 受注");
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
@Transactional
|
|
1319
|
-
public void cleanupAll() {
|
|
1320
|
-
cleanupTransactionData();
|
|
1321
|
-
jdbcTemplate.execute("DELETE FROM 商品");
|
|
1322
|
-
jdbcTemplate.execute("DELETE FROM 商品分類");
|
|
1323
|
-
jdbcTemplate.execute("DELETE FROM 取引先");
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
```
|
|
1327
|
-
|
|
1328
|
-
</details>
|
|
1329
|
-
|
|
1330
|
-
<details>
|
|
1331
|
-
<summary>統合テストでの使用例</summary>
|
|
1332
|
-
|
|
1333
|
-
```java
|
|
1334
|
-
@DisplayName("販売フロー統合テスト")
|
|
1335
|
-
class SalesFlowIntegrationTest extends IntegrationTestBase {
|
|
1336
|
-
|
|
1337
|
-
@Autowired
|
|
1338
|
-
private TestDataManager testDataManager;
|
|
1339
|
-
|
|
1340
|
-
@BeforeEach
|
|
1341
|
-
void setUp() {
|
|
1342
|
-
testDataManager.cleanupTransactionData();
|
|
1343
|
-
testDataManager.setupMasterData();
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
@AfterEach
|
|
1347
|
-
void tearDown() {
|
|
1348
|
-
testDataManager.cleanupTransactionData();
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
@Test
|
|
1352
|
-
@DisplayName("受注から売上までの一連のフローが正常に動作する")
|
|
1353
|
-
void shouldCompleteSalesFlow() {
|
|
1354
|
-
// 1. 受注登録
|
|
1355
|
-
var orderRequest = createOrderRequest();
|
|
1356
|
-
ResponseEntity<OrderResponse> orderResponse = restTemplate.postForEntity(
|
|
1357
|
-
"/api/v1/orders",
|
|
1358
|
-
orderRequest,
|
|
1359
|
-
OrderResponse.class
|
|
1360
|
-
);
|
|
1361
|
-
assertThat(orderResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1362
|
-
String orderNumber = orderResponse.getBody().orderNumber();
|
|
1363
|
-
|
|
1364
|
-
// 2. 出荷指示
|
|
1365
|
-
var shipmentRequest = new CreateShipmentInstructionRequest(orderNumber);
|
|
1366
|
-
ResponseEntity<ShipmentInstructionResponse> shipmentResponse = restTemplate.postForEntity(
|
|
1367
|
-
"/api/v1/shipments/instructions",
|
|
1368
|
-
shipmentRequest,
|
|
1369
|
-
ShipmentInstructionResponse.class
|
|
1370
|
-
);
|
|
1371
|
-
assertThat(shipmentResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1372
|
-
String shipmentNumber = shipmentResponse.getBody().shipmentNumber();
|
|
1373
|
-
|
|
1374
|
-
// 3. 出荷確定
|
|
1375
|
-
ResponseEntity<ShipmentResponse> confirmResponse = restTemplate.postForEntity(
|
|
1376
|
-
"/api/v1/shipments/" + shipmentNumber + "/confirm",
|
|
1377
|
-
null,
|
|
1378
|
-
ShipmentResponse.class
|
|
1379
|
-
);
|
|
1380
|
-
assertThat(confirmResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1381
|
-
|
|
1382
|
-
// 4. 売上計上の確認
|
|
1383
|
-
ResponseEntity<SalesResponse[]> salesResponse = restTemplate.getForEntity(
|
|
1384
|
-
"/api/v1/sales?orderNumber=" + orderNumber,
|
|
1385
|
-
SalesResponse[].class
|
|
1386
|
-
);
|
|
1387
|
-
assertThat(salesResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1388
|
-
assertThat(salesResponse.getBody()).hasSize(1);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
```
|
|
1392
|
-
|
|
1393
|
-
</details>
|
|
1394
|
-
|
|
1395
|
-
---
|
|
1396
|
-
|
|
1397
|
-
## 本章のまとめ
|
|
1398
|
-
|
|
1399
|
-
本章では、販売管理システムの API サービスを実装しました。
|
|
1400
|
-
|
|
1401
|
-
### 実装したコンポーネント
|
|
1402
|
-
|
|
1403
|
-
| カテゴリ | 内容 |
|
|
1404
|
-
|----------|------|
|
|
1405
|
-
| アーキテクチャ | ヘキサゴナルアーキテクチャ(Ports & Adapters) |
|
|
1406
|
-
| マスタ API | 商品マスタ、顧客マスタ |
|
|
1407
|
-
| トランザクション API | 受注、出荷、売上、請求・入金 |
|
|
1408
|
-
| エラーハンドリング | グローバル例外ハンドラー、ドメイン例外 |
|
|
1409
|
-
| API ドキュメント | OpenAPI / Swagger UI |
|
|
1410
|
-
|
|
1411
|
-
### アーキテクチャの利点
|
|
1412
|
-
|
|
1413
|
-
1. **テスト容易性**: ドメインロジックを独立してテスト可能
|
|
1414
|
-
2. **技術変更の容易さ**: アダプターを差し替えるだけで技術を変更可能
|
|
1415
|
-
3. **保守性**: 関心事の分離による高い保守性
|
|
1416
|
-
4. **チーム開発**: レイヤーごとに並行開発が可能
|
|
1417
|
-
|
|
1418
|
-
### エンドポイント一覧
|
|
1419
|
-
|
|
1420
|
-
| メソッド | パス | 説明 |
|
|
1421
|
-
|----------|------|------|
|
|
1422
|
-
| GET | `/api/v1/products` | 商品一覧の取得 |
|
|
1423
|
-
| GET | `/api/v1/products/{code}` | 商品の取得 |
|
|
1424
|
-
| POST | `/api/v1/products` | 商品の登録 |
|
|
1425
|
-
| PUT | `/api/v1/products/{code}` | 商品の更新 |
|
|
1426
|
-
| DELETE | `/api/v1/products/{code}` | 商品の削除 |
|
|
1427
|
-
| GET | `/api/v1/orders` | 受注一覧の取得 |
|
|
1428
|
-
| POST | `/api/v1/orders` | 受注の登録 |
|
|
1429
|
-
| POST | `/api/v1/shipments/instructions` | 出荷指示の登録 |
|
|
1430
|
-
| POST | `/api/v1/shipments/{number}/confirm` | 出荷確定 |
|
|
1431
|
-
| POST | `/api/v1/sales` | 売上計上 |
|
|
1432
|
-
| POST | `/api/v1/invoices/closing` | 請求締め処理 |
|
|
1433
|
-
| POST | `/api/v1/receipts` | 入金登録 |
|
|
1434
|
-
| POST | `/api/v1/receipts/{number}/apply` | 入金消込 |
|
|
1
|
+
# 第13章:API サービスの実装
|
|
2
|
+
|
|
3
|
+
本章では、販売管理システムのデータベース設計を外部から利用できるようにするため、RESTful API サービスを実装します。ヘキサゴナルアーキテクチャ(Ports and Adapters)を採用し、ドメインロジックを外部技術から分離した保守性の高い API を構築します。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 13.1 ヘキサゴナルアーキテクチャの復習
|
|
8
|
+
|
|
9
|
+
### Ports and Adapters パターンの概要
|
|
10
|
+
|
|
11
|
+
ヘキサゴナルアーキテクチャ(Ports and Adapters パターン)は、Alistair Cockburn によって提唱された設計手法で、アプリケーションの中核となるビジネスロジック(ドメイン層)を外部の技術的詳細から完全に分離することを目的とします。
|
|
12
|
+
|
|
13
|
+
```plantuml
|
|
14
|
+
@startuml hexagonal_architecture_sales
|
|
15
|
+
!define RECTANGLE class
|
|
16
|
+
|
|
17
|
+
package "Hexagonal Architecture (販売管理API)" {
|
|
18
|
+
|
|
19
|
+
RECTANGLE "Application Core\n(Domain + Use Cases)" as core {
|
|
20
|
+
- Product (商品)
|
|
21
|
+
- Partner (取引先)
|
|
22
|
+
- Order (受注)
|
|
23
|
+
- Sales (売上)
|
|
24
|
+
- Invoice (請求)
|
|
25
|
+
- ProductUseCase
|
|
26
|
+
- OrderUseCase
|
|
27
|
+
- SalesUseCase
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
RECTANGLE "Input Adapters\n(Driving Side)" as input {
|
|
31
|
+
- Spring Controllers
|
|
32
|
+
- REST API Endpoints
|
|
33
|
+
- Request Validation
|
|
34
|
+
- Error Handling
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
RECTANGLE "Output Adapters\n(Driven Side)" as output {
|
|
38
|
+
- MyBatis Repository
|
|
39
|
+
- Database Access
|
|
40
|
+
- Entity Mapping
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
input --> core : "Input Ports\n(Use Cases)"
|
|
45
|
+
core --> output : "Output Ports\n(Repository Interfaces)"
|
|
46
|
+
|
|
47
|
+
note top of core
|
|
48
|
+
純粋なビジネスロジック
|
|
49
|
+
MyBatis に依存しない
|
|
50
|
+
高速でテスト可能
|
|
51
|
+
end note
|
|
52
|
+
|
|
53
|
+
note left of input
|
|
54
|
+
外部からアプリケーションを
|
|
55
|
+
駆動するアダプター
|
|
56
|
+
HTTP, REST等
|
|
57
|
+
end note
|
|
58
|
+
|
|
59
|
+
note right of output
|
|
60
|
+
アプリケーションが外部の
|
|
61
|
+
技術を使うためのアダプター
|
|
62
|
+
PostgreSQL, MyBatis等
|
|
63
|
+
end note
|
|
64
|
+
@enduml
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### ドメイン中心設計
|
|
68
|
+
|
|
69
|
+
ビジネスロジックを中心に据え、外部技術(DB、Web、UI など)を周辺に配置します。これにより、ビジネスルールが技術的な関心事から独立し、変更に強い設計が実現できます。
|
|
70
|
+
|
|
71
|
+
### 依存性の逆転
|
|
72
|
+
|
|
73
|
+
ドメイン層は外部に依存せず、外部がドメイン層に依存します。具体的には、リポジトリのインターフェース(Output Port)をドメイン層で定義し、その実装(Adapter)をインフラストラクチャ層に配置します。
|
|
74
|
+
|
|
75
|
+
### テスト容易性
|
|
76
|
+
|
|
77
|
+
モックやスタブを使った単体テストが容易になります。ドメインロジックを独立してテストでき、外部依存(データベースなど)なしで高速なテストが可能です。
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 13.2 アーキテクチャ構造
|
|
82
|
+
|
|
83
|
+
### レイヤー構成
|
|
84
|
+
|
|
85
|
+
販売管理 API の実装では、以下のレイヤー構造を採用します。
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
src/main/java/com/example/sms/
|
|
89
|
+
├── domain/ # ドメイン層(純粋なビジネスロジック)
|
|
90
|
+
│ ├── model/ # ドメインモデル(エンティティ、値オブジェクト)
|
|
91
|
+
│ │ ├── department/ # 部門関連
|
|
92
|
+
│ │ ├── employee/ # 社員関連
|
|
93
|
+
│ │ ├── inventory/ # 在庫関連
|
|
94
|
+
│ │ ├── partner/ # 取引先関連
|
|
95
|
+
│ │ ├── product/ # 商品関連
|
|
96
|
+
│ │ ├── purchase/ # 仕入関連
|
|
97
|
+
│ │ └── sales/ # 販売関連
|
|
98
|
+
│ ├── exception/ # ドメイン例外
|
|
99
|
+
│ └── type/ # 値型定義
|
|
100
|
+
│
|
|
101
|
+
├── application/ # アプリケーション層
|
|
102
|
+
│ └── port/
|
|
103
|
+
│ └── out/ # Output Port(リポジトリインターフェース)
|
|
104
|
+
│
|
|
105
|
+
├── infrastructure/ # インフラストラクチャ層
|
|
106
|
+
│ ├── in/ # Input Adapter(受信アダプター)
|
|
107
|
+
│ │ ├── rest/ # REST API(Web実装)
|
|
108
|
+
│ │ │ ├── controller/ # REST Controller(Spring MVC)
|
|
109
|
+
│ │ │ ├── dto/ # Data Transfer Object
|
|
110
|
+
│ │ │ └── exception/ # Exception Handler
|
|
111
|
+
│ │ └── seed/ # Seed データ投入
|
|
112
|
+
│ └── out/ # Output Adapter(送信アダプター)
|
|
113
|
+
│ └── persistence/ # DB実装
|
|
114
|
+
│ ├── mapper/ # MyBatis Mapper
|
|
115
|
+
│ ├── repository/ # Repository実装
|
|
116
|
+
│ └── typehandler/ # 型ハンドラー
|
|
117
|
+
│
|
|
118
|
+
└── config/ # 設定クラス
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Domain 層
|
|
122
|
+
|
|
123
|
+
ビジネスルールとドメインモデルを定義します。外部技術に依存しない純粋な Java コードで構成されます。
|
|
124
|
+
|
|
125
|
+
<details>
|
|
126
|
+
<summary>Product.java(商品ドメインモデル)</summary>
|
|
127
|
+
|
|
128
|
+
```java
|
|
129
|
+
package com.example.sms.domain.model.product;
|
|
130
|
+
|
|
131
|
+
import lombok.AllArgsConstructor;
|
|
132
|
+
import lombok.Builder;
|
|
133
|
+
import lombok.Data;
|
|
134
|
+
import lombok.NoArgsConstructor;
|
|
135
|
+
|
|
136
|
+
import java.math.BigDecimal;
|
|
137
|
+
import java.time.LocalDateTime;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 商品エンティティ.
|
|
141
|
+
*/
|
|
142
|
+
@Data
|
|
143
|
+
@Builder
|
|
144
|
+
@NoArgsConstructor
|
|
145
|
+
@AllArgsConstructor
|
|
146
|
+
public class Product {
|
|
147
|
+
private String productCode;
|
|
148
|
+
private String productFullName;
|
|
149
|
+
private String productName;
|
|
150
|
+
private String productNameKana;
|
|
151
|
+
private ProductCategory productCategory;
|
|
152
|
+
private String modelNumber;
|
|
153
|
+
@Builder.Default
|
|
154
|
+
private BigDecimal sellingPrice = BigDecimal.ZERO;
|
|
155
|
+
@Builder.Default
|
|
156
|
+
private BigDecimal purchasePrice = BigDecimal.ZERO;
|
|
157
|
+
private TaxCategory taxCategory;
|
|
158
|
+
private String classificationCode;
|
|
159
|
+
@Builder.Default
|
|
160
|
+
private boolean isMiscellaneous = false;
|
|
161
|
+
@Builder.Default
|
|
162
|
+
private boolean isInventoryManaged = true;
|
|
163
|
+
@Builder.Default
|
|
164
|
+
private boolean isInventoryAllocated = true;
|
|
165
|
+
private String supplierCode;
|
|
166
|
+
private String supplierBranchNumber;
|
|
167
|
+
private LocalDateTime createdAt;
|
|
168
|
+
private String createdBy;
|
|
169
|
+
private LocalDateTime updatedAt;
|
|
170
|
+
private String updatedBy;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
</details>
|
|
175
|
+
|
|
176
|
+
### Application 層
|
|
177
|
+
|
|
178
|
+
ユースケースの実装とオーケストレーションを担当します。
|
|
179
|
+
|
|
180
|
+
<details>
|
|
181
|
+
<summary>ProductRepository.java(Output Port)</summary>
|
|
182
|
+
|
|
183
|
+
```java
|
|
184
|
+
package com.example.sms.application.port.out;
|
|
185
|
+
|
|
186
|
+
import com.example.sms.domain.model.product.Product;
|
|
187
|
+
import com.example.sms.domain.model.product.ProductCategory;
|
|
188
|
+
|
|
189
|
+
import java.util.List;
|
|
190
|
+
import java.util.Optional;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 商品リポジトリ(Output Port).
|
|
194
|
+
*/
|
|
195
|
+
public interface ProductRepository {
|
|
196
|
+
|
|
197
|
+
void save(Product product);
|
|
198
|
+
|
|
199
|
+
Optional<Product> findByCode(String productCode);
|
|
200
|
+
|
|
201
|
+
List<Product> findAll();
|
|
202
|
+
|
|
203
|
+
List<Product> findByCategory(ProductCategory category);
|
|
204
|
+
|
|
205
|
+
List<Product> findByClassificationCode(String classificationCode);
|
|
206
|
+
|
|
207
|
+
void update(Product product);
|
|
208
|
+
|
|
209
|
+
void deleteByCode(String productCode);
|
|
210
|
+
|
|
211
|
+
void deleteAll();
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
</details>
|
|
216
|
+
|
|
217
|
+
### Infrastructure 層
|
|
218
|
+
|
|
219
|
+
外部技術との接続を担当します。DB アクセス(MyBatis)や Web フレームワーク(Spring MVC)の実装を含みます。
|
|
220
|
+
|
|
221
|
+
### Input Port / Output Port の分離
|
|
222
|
+
|
|
223
|
+
| ポート | 役割 | 例 |
|
|
224
|
+
|--------|------|-----|
|
|
225
|
+
| Input Port | アプリケーションへの入力インターフェース | `ProductUseCase`, `OrderUseCase` |
|
|
226
|
+
| Output Port | アプリケーションからの出力インターフェース | `ProductRepository`, `OrderRepository` |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 13.3 マスタ API の実装
|
|
231
|
+
|
|
232
|
+
### 商品マスタ API(CRUD エンドポイント)
|
|
233
|
+
|
|
234
|
+
#### Output Port(リポジトリインターフェース)
|
|
235
|
+
|
|
236
|
+
<details>
|
|
237
|
+
<summary>ProductRepository.java</summary>
|
|
238
|
+
|
|
239
|
+
```java
|
|
240
|
+
package com.example.sms.application.port.out;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 商品リポジトリ(Output Port)
|
|
244
|
+
*/
|
|
245
|
+
public interface ProductRepository {
|
|
246
|
+
Product save(Product product);
|
|
247
|
+
List<Product> findAll();
|
|
248
|
+
Optional<Product> findByCode(String productCode, LocalDate effectiveDate);
|
|
249
|
+
List<Product> findByType(ProductType type);
|
|
250
|
+
List<Product> findByCategoryCode(String categoryCode);
|
|
251
|
+
void deleteByCode(String productCode, LocalDate effectiveDate);
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
</details>
|
|
256
|
+
|
|
257
|
+
#### Input Port(ユースケースインターフェース)
|
|
258
|
+
|
|
259
|
+
<details>
|
|
260
|
+
<summary>コマンドオブジェクト</summary>
|
|
261
|
+
|
|
262
|
+
```java
|
|
263
|
+
package com.example.sms.application.port.in;
|
|
264
|
+
|
|
265
|
+
public record CreateProductCommand(
|
|
266
|
+
String productCode,
|
|
267
|
+
String productName,
|
|
268
|
+
String categoryCode,
|
|
269
|
+
ProductType productType,
|
|
270
|
+
TaxType taxType,
|
|
271
|
+
BigDecimal sellingPrice,
|
|
272
|
+
BigDecimal purchasePrice
|
|
273
|
+
) {}
|
|
274
|
+
|
|
275
|
+
public record UpdateProductCommand(
|
|
276
|
+
String productCode,
|
|
277
|
+
String productName,
|
|
278
|
+
ProductType productType,
|
|
279
|
+
TaxType taxType,
|
|
280
|
+
BigDecimal sellingPrice,
|
|
281
|
+
BigDecimal purchasePrice
|
|
282
|
+
) {}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
</details>
|
|
286
|
+
|
|
287
|
+
### TDD による実装(Red-Green-Refactor)
|
|
288
|
+
|
|
289
|
+
#### Red: 失敗するテストを書く
|
|
290
|
+
|
|
291
|
+
<details>
|
|
292
|
+
<summary>ProductControllerTest.java</summary>
|
|
293
|
+
|
|
294
|
+
```java
|
|
295
|
+
package com.example.sms.infrastructure.in.rest;
|
|
296
|
+
|
|
297
|
+
import com.example.sms.application.port.out.ProductClassificationRepository;
|
|
298
|
+
import com.example.sms.application.port.out.ProductRepository;
|
|
299
|
+
import com.example.sms.domain.model.product.Product;
|
|
300
|
+
import com.example.sms.domain.model.product.ProductCategory;
|
|
301
|
+
import com.example.sms.domain.model.product.ProductClassification;
|
|
302
|
+
import com.example.sms.domain.model.product.TaxCategory;
|
|
303
|
+
import com.example.sms.testsetup.BaseIntegrationTest;
|
|
304
|
+
import org.junit.jupiter.api.BeforeEach;
|
|
305
|
+
import org.junit.jupiter.api.DisplayName;
|
|
306
|
+
import org.junit.jupiter.api.Nested;
|
|
307
|
+
import org.junit.jupiter.api.Test;
|
|
308
|
+
import org.springframework.beans.factory.annotation.Autowired;
|
|
309
|
+
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
|
310
|
+
import org.springframework.http.MediaType;
|
|
311
|
+
import org.springframework.test.web.servlet.MockMvc;
|
|
312
|
+
|
|
313
|
+
import java.math.BigDecimal;
|
|
314
|
+
|
|
315
|
+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
|
316
|
+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
|
317
|
+
|
|
318
|
+
@AutoConfigureMockMvc
|
|
319
|
+
@DisplayName("商品マスタ API テスト")
|
|
320
|
+
class ProductControllerTest extends BaseIntegrationTest {
|
|
321
|
+
|
|
322
|
+
private static final String TEST_CLASSIFICATION_CODE = "CAT-TEST";
|
|
323
|
+
|
|
324
|
+
@Autowired
|
|
325
|
+
private MockMvc mockMvc;
|
|
326
|
+
|
|
327
|
+
@Autowired
|
|
328
|
+
private ProductRepository productRepository;
|
|
329
|
+
|
|
330
|
+
@Autowired
|
|
331
|
+
private ProductClassificationRepository productClassificationRepository;
|
|
332
|
+
|
|
333
|
+
@BeforeEach
|
|
334
|
+
void setUp() {
|
|
335
|
+
productRepository.deleteAll();
|
|
336
|
+
productClassificationRepository.deleteAll();
|
|
337
|
+
// テスト用の商品分類を作成
|
|
338
|
+
ProductClassification classification = ProductClassification.builder()
|
|
339
|
+
.classificationCode(TEST_CLASSIFICATION_CODE)
|
|
340
|
+
.classificationName("テスト分類")
|
|
341
|
+
.hierarchyLevel(1)
|
|
342
|
+
.classificationPath("/CAT-TEST")
|
|
343
|
+
.isLeaf(true)
|
|
344
|
+
.build();
|
|
345
|
+
productClassificationRepository.save(classification);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
@Nested
|
|
349
|
+
@DisplayName("POST /api/v1/products")
|
|
350
|
+
class CreateProduct {
|
|
351
|
+
|
|
352
|
+
@Test
|
|
353
|
+
@DisplayName("商品を登録できる")
|
|
354
|
+
void shouldCreateProduct() throws Exception {
|
|
355
|
+
var request = """
|
|
356
|
+
{
|
|
357
|
+
"productCode": "NEW-001",
|
|
358
|
+
"productFullName": "新規テスト商品 フルネーム",
|
|
359
|
+
"productName": "新規テスト商品",
|
|
360
|
+
"productNameKana": "シンキテストショウヒン",
|
|
361
|
+
"productCategory": "PRODUCT",
|
|
362
|
+
"modelNumber": "MODEL-001",
|
|
363
|
+
"sellingPrice": 5000,
|
|
364
|
+
"purchasePrice": 3000,
|
|
365
|
+
"taxCategory": "EXCLUSIVE",
|
|
366
|
+
"classificationCode": "CAT-TEST"
|
|
367
|
+
}
|
|
368
|
+
""";
|
|
369
|
+
|
|
370
|
+
mockMvc.perform(post("/api/v1/products")
|
|
371
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
372
|
+
.content(request))
|
|
373
|
+
.andExpect(status().isCreated())
|
|
374
|
+
.andExpect(jsonPath("$.productCode").value("NEW-001"))
|
|
375
|
+
.andExpect(jsonPath("$.productName").value("新規テスト商品"));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
</details>
|
|
382
|
+
|
|
383
|
+
#### Green: テストを通す実装
|
|
384
|
+
|
|
385
|
+
<details>
|
|
386
|
+
<summary>ProductController.java</summary>
|
|
387
|
+
|
|
388
|
+
```java
|
|
389
|
+
package com.example.sms.infrastructure.in.rest.controller;
|
|
390
|
+
|
|
391
|
+
import com.example.sms.application.port.out.ProductRepository;
|
|
392
|
+
import com.example.sms.domain.exception.DuplicateProductException;
|
|
393
|
+
import com.example.sms.domain.exception.ProductNotFoundException;
|
|
394
|
+
import com.example.sms.domain.model.product.Product;
|
|
395
|
+
import com.example.sms.infrastructure.in.rest.dto.CreateProductRequest;
|
|
396
|
+
import com.example.sms.infrastructure.in.rest.dto.ProductResponse;
|
|
397
|
+
import com.example.sms.infrastructure.in.rest.dto.UpdateProductRequest;
|
|
398
|
+
import io.swagger.v3.oas.annotations.Operation;
|
|
399
|
+
import io.swagger.v3.oas.annotations.Parameter;
|
|
400
|
+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
|
401
|
+
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
402
|
+
import jakarta.validation.Valid;
|
|
403
|
+
import org.springframework.http.HttpStatus;
|
|
404
|
+
import org.springframework.http.ResponseEntity;
|
|
405
|
+
import org.springframework.web.bind.annotation.*;
|
|
406
|
+
|
|
407
|
+
import java.util.List;
|
|
408
|
+
|
|
409
|
+
@RestController
|
|
410
|
+
@RequestMapping("/api/v1/products")
|
|
411
|
+
@Tag(name = "products", description = "商品マスタ API")
|
|
412
|
+
public class ProductController {
|
|
413
|
+
|
|
414
|
+
private final ProductRepository productRepository;
|
|
415
|
+
|
|
416
|
+
public ProductController(ProductRepository productRepository) {
|
|
417
|
+
this.productRepository = productRepository;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@GetMapping
|
|
421
|
+
@Operation(summary = "商品一覧の取得")
|
|
422
|
+
@ApiResponse(responseCode = "200", description = "商品一覧を返却")
|
|
423
|
+
public ResponseEntity<List<ProductResponse>> getAllProducts() {
|
|
424
|
+
List<Product> products = productRepository.findAll();
|
|
425
|
+
List<ProductResponse> responses = products.stream()
|
|
426
|
+
.map(ProductResponse::from)
|
|
427
|
+
.toList();
|
|
428
|
+
return ResponseEntity.ok(responses);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
@GetMapping("/{productCode}")
|
|
432
|
+
@Operation(summary = "商品の取得")
|
|
433
|
+
@ApiResponse(responseCode = "200", description = "商品を返却")
|
|
434
|
+
@ApiResponse(responseCode = "404", description = "商品が見つからない")
|
|
435
|
+
public ResponseEntity<ProductResponse> getProduct(
|
|
436
|
+
@Parameter(description = "商品コード")
|
|
437
|
+
@PathVariable String productCode) {
|
|
438
|
+
|
|
439
|
+
Product product = productRepository.findByCode(productCode)
|
|
440
|
+
.orElseThrow(() -> new ProductNotFoundException(productCode));
|
|
441
|
+
|
|
442
|
+
return ResponseEntity.ok(ProductResponse.from(product));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
@PostMapping
|
|
446
|
+
@Operation(summary = "商品の登録")
|
|
447
|
+
@ApiResponse(responseCode = "201", description = "商品を登録")
|
|
448
|
+
@ApiResponse(responseCode = "409", description = "商品が既に存在する")
|
|
449
|
+
public ResponseEntity<ProductResponse> createProduct(
|
|
450
|
+
@Valid @RequestBody CreateProductRequest request) {
|
|
451
|
+
|
|
452
|
+
// 重複チェック
|
|
453
|
+
productRepository.findByCode(request.productCode())
|
|
454
|
+
.ifPresent(existing -> {
|
|
455
|
+
throw new DuplicateProductException(request.productCode());
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
Product product = Product.builder()
|
|
459
|
+
.productCode(request.productCode())
|
|
460
|
+
.productFullName(request.productFullName())
|
|
461
|
+
.productName(request.productName())
|
|
462
|
+
.productNameKana(request.productNameKana())
|
|
463
|
+
.productCategory(request.productCategory())
|
|
464
|
+
.modelNumber(request.modelNumber())
|
|
465
|
+
.sellingPrice(request.sellingPrice())
|
|
466
|
+
.purchasePrice(request.purchasePrice())
|
|
467
|
+
.taxCategory(request.taxCategory())
|
|
468
|
+
.classificationCode(request.classificationCode())
|
|
469
|
+
.build();
|
|
470
|
+
|
|
471
|
+
productRepository.save(product);
|
|
472
|
+
|
|
473
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
474
|
+
.body(ProductResponse.from(product));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
</details>
|
|
480
|
+
|
|
481
|
+
### Controller・Service・Repository の実装
|
|
482
|
+
|
|
483
|
+
#### Application Service
|
|
484
|
+
|
|
485
|
+
<details>
|
|
486
|
+
<summary>ProductService.java</summary>
|
|
487
|
+
|
|
488
|
+
```java
|
|
489
|
+
@Service
|
|
490
|
+
@Transactional
|
|
491
|
+
public class ProductService implements ProductUseCase {
|
|
492
|
+
|
|
493
|
+
private final ProductRepository productRepository;
|
|
494
|
+
|
|
495
|
+
public ProductService(ProductRepository productRepository) {
|
|
496
|
+
this.productRepository = productRepository;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
@Override
|
|
500
|
+
public Product createProduct(CreateProductCommand command) {
|
|
501
|
+
LocalDate effectiveDate = LocalDate.now();
|
|
502
|
+
|
|
503
|
+
// 重複チェック
|
|
504
|
+
productRepository.findByCode(command.productCode(), effectiveDate)
|
|
505
|
+
.ifPresent(existing -> {
|
|
506
|
+
throw new DuplicateProductException(command.productCode());
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
Product product = new Product(
|
|
510
|
+
command.productCode(),
|
|
511
|
+
effectiveDate,
|
|
512
|
+
command.productName(),
|
|
513
|
+
command.categoryCode(),
|
|
514
|
+
effectiveDate,
|
|
515
|
+
command.productType(),
|
|
516
|
+
command.taxType(),
|
|
517
|
+
command.sellingPrice(),
|
|
518
|
+
command.purchasePrice()
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
return productRepository.save(product);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
@Override
|
|
525
|
+
@Transactional(readOnly = true)
|
|
526
|
+
public Product getProductByCode(String productCode) {
|
|
527
|
+
LocalDate effectiveDate = LocalDate.now();
|
|
528
|
+
return productRepository.findByCode(productCode, effectiveDate)
|
|
529
|
+
.orElseThrow(() -> new ProductNotFoundException(productCode));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
</details>
|
|
535
|
+
|
|
536
|
+
### 顧客マスタ API(請求先・回収先の管理)
|
|
537
|
+
|
|
538
|
+
顧客マスタ API では、請求先と回収先の概念を正しく管理します。
|
|
539
|
+
|
|
540
|
+
<details>
|
|
541
|
+
<summary>CustomerController.java</summary>
|
|
542
|
+
|
|
543
|
+
```java
|
|
544
|
+
@RestController
|
|
545
|
+
@RequestMapping("/api/v1/customers")
|
|
546
|
+
@Tag(name = "customers", description = "顧客 API")
|
|
547
|
+
public class CustomerController {
|
|
548
|
+
|
|
549
|
+
private final CustomerUseCase customerUseCase;
|
|
550
|
+
|
|
551
|
+
@GetMapping("/{customerCode}")
|
|
552
|
+
@Operation(summary = "顧客の取得")
|
|
553
|
+
public ResponseEntity<CustomerResponse> getCustomer(
|
|
554
|
+
@PathVariable String customerCode) {
|
|
555
|
+
|
|
556
|
+
Customer customer = customerUseCase.getCustomerByCode(customerCode);
|
|
557
|
+
return ResponseEntity.ok(CustomerResponse.from(customer));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
@GetMapping("/{customerCode}/billing-destinations")
|
|
561
|
+
@Operation(summary = "請求先一覧の取得")
|
|
562
|
+
public ResponseEntity<List<BillingDestinationResponse>> getBillingDestinations(
|
|
563
|
+
@PathVariable String customerCode) {
|
|
564
|
+
|
|
565
|
+
List<BillingDestination> destinations =
|
|
566
|
+
customerUseCase.getBillingDestinations(customerCode);
|
|
567
|
+
return ResponseEntity.ok(destinations.stream()
|
|
568
|
+
.map(BillingDestinationResponse::from)
|
|
569
|
+
.toList());
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
</details>
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## 13.4 トランザクション API の実装
|
|
579
|
+
|
|
580
|
+
### 受注 API(受注登録・受注照会・受注明細照会)
|
|
581
|
+
|
|
582
|
+
<details>
|
|
583
|
+
<summary>OrderController.java</summary>
|
|
584
|
+
|
|
585
|
+
```java
|
|
586
|
+
@RestController
|
|
587
|
+
@RequestMapping("/api/v1/orders")
|
|
588
|
+
@Tag(name = "orders", description = "受注 API")
|
|
589
|
+
public class OrderController {
|
|
590
|
+
|
|
591
|
+
private final OrderUseCase orderUseCase;
|
|
592
|
+
|
|
593
|
+
@PostMapping
|
|
594
|
+
@Operation(summary = "受注の登録")
|
|
595
|
+
public ResponseEntity<OrderResponse> createOrder(
|
|
596
|
+
@Valid @RequestBody CreateOrderRequest request) {
|
|
597
|
+
|
|
598
|
+
Order order = orderUseCase.createOrder(request.toCommand());
|
|
599
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
600
|
+
.body(OrderResponse.from(order));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
@GetMapping("/{orderNumber}")
|
|
604
|
+
@Operation(summary = "受注の取得")
|
|
605
|
+
public ResponseEntity<OrderResponse> getOrder(
|
|
606
|
+
@PathVariable String orderNumber) {
|
|
607
|
+
|
|
608
|
+
Order order = orderUseCase.getOrderByNumber(orderNumber);
|
|
609
|
+
return ResponseEntity.ok(OrderResponse.from(order));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
@GetMapping("/{orderNumber}/details")
|
|
613
|
+
@Operation(summary = "受注明細の取得")
|
|
614
|
+
public ResponseEntity<List<OrderDetailResponse>> getOrderDetails(
|
|
615
|
+
@PathVariable String orderNumber) {
|
|
616
|
+
|
|
617
|
+
List<OrderDetail> details = orderUseCase.getOrderDetails(orderNumber);
|
|
618
|
+
return ResponseEntity.ok(details.stream()
|
|
619
|
+
.map(OrderDetailResponse::from)
|
|
620
|
+
.toList());
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
</details>
|
|
626
|
+
|
|
627
|
+
### 出荷 API(出荷指示・出荷確定)
|
|
628
|
+
|
|
629
|
+
<details>
|
|
630
|
+
<summary>ShipmentController.java</summary>
|
|
631
|
+
|
|
632
|
+
```java
|
|
633
|
+
@RestController
|
|
634
|
+
@RequestMapping("/api/v1/shipments")
|
|
635
|
+
@Tag(name = "shipments", description = "出荷 API")
|
|
636
|
+
public class ShipmentController {
|
|
637
|
+
|
|
638
|
+
private final ShipmentUseCase shipmentUseCase;
|
|
639
|
+
|
|
640
|
+
@PostMapping("/instructions")
|
|
641
|
+
@Operation(summary = "出荷指示の登録")
|
|
642
|
+
public ResponseEntity<ShipmentInstructionResponse> createShipmentInstruction(
|
|
643
|
+
@Valid @RequestBody CreateShipmentInstructionRequest request) {
|
|
644
|
+
|
|
645
|
+
ShipmentInstruction instruction =
|
|
646
|
+
shipmentUseCase.createInstruction(request.toCommand());
|
|
647
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
648
|
+
.body(ShipmentInstructionResponse.from(instruction));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
@PostMapping("/{shipmentNumber}/confirm")
|
|
652
|
+
@Operation(summary = "出荷確定")
|
|
653
|
+
public ResponseEntity<ShipmentResponse> confirmShipment(
|
|
654
|
+
@PathVariable String shipmentNumber) {
|
|
655
|
+
|
|
656
|
+
Shipment shipment = shipmentUseCase.confirmShipment(shipmentNumber);
|
|
657
|
+
return ResponseEntity.ok(ShipmentResponse.from(shipment));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
</details>
|
|
663
|
+
|
|
664
|
+
### 売上 API(売上計上・売上照会)
|
|
665
|
+
|
|
666
|
+
<details>
|
|
667
|
+
<summary>SalesController.java</summary>
|
|
668
|
+
|
|
669
|
+
```java
|
|
670
|
+
@RestController
|
|
671
|
+
@RequestMapping("/api/v1/sales")
|
|
672
|
+
@Tag(name = "sales", description = "売上 API")
|
|
673
|
+
public class SalesController {
|
|
674
|
+
|
|
675
|
+
private final SalesUseCase salesUseCase;
|
|
676
|
+
|
|
677
|
+
@PostMapping
|
|
678
|
+
@Operation(summary = "売上計上")
|
|
679
|
+
public ResponseEntity<SalesResponse> recordSales(
|
|
680
|
+
@Valid @RequestBody RecordSalesRequest request) {
|
|
681
|
+
|
|
682
|
+
Sales sales = salesUseCase.recordSales(request.toCommand());
|
|
683
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
684
|
+
.body(SalesResponse.from(sales));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
@GetMapping
|
|
688
|
+
@Operation(summary = "売上一覧の取得")
|
|
689
|
+
public ResponseEntity<List<SalesResponse>> getSalesList(
|
|
690
|
+
@RequestParam(required = false) LocalDate fromDate,
|
|
691
|
+
@RequestParam(required = false) LocalDate toDate,
|
|
692
|
+
@RequestParam(required = false) String customerCode) {
|
|
693
|
+
|
|
694
|
+
List<Sales> salesList = salesUseCase.getSalesList(fromDate, toDate, customerCode);
|
|
695
|
+
return ResponseEntity.ok(salesList.stream()
|
|
696
|
+
.map(SalesResponse::from)
|
|
697
|
+
.toList());
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
</details>
|
|
703
|
+
|
|
704
|
+
### 請求・入金 API(請求締め・入金消込)
|
|
705
|
+
|
|
706
|
+
<details>
|
|
707
|
+
<summary>InvoiceController.java / ReceiptController.java</summary>
|
|
708
|
+
|
|
709
|
+
```java
|
|
710
|
+
@RestController
|
|
711
|
+
@RequestMapping("/api/v1/invoices")
|
|
712
|
+
@Tag(name = "invoices", description = "請求 API")
|
|
713
|
+
public class InvoiceController {
|
|
714
|
+
|
|
715
|
+
private final InvoiceUseCase invoiceUseCase;
|
|
716
|
+
|
|
717
|
+
@PostMapping("/closing")
|
|
718
|
+
@Operation(summary = "請求締め処理")
|
|
719
|
+
public ResponseEntity<List<InvoiceResponse>> executeClosing(
|
|
720
|
+
@Valid @RequestBody InvoiceClosingRequest request) {
|
|
721
|
+
|
|
722
|
+
List<Invoice> invoices = invoiceUseCase.executeClosing(
|
|
723
|
+
request.closingDate(),
|
|
724
|
+
request.customerCode()
|
|
725
|
+
);
|
|
726
|
+
return ResponseEntity.ok(invoices.stream()
|
|
727
|
+
.map(InvoiceResponse::from)
|
|
728
|
+
.toList());
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
@RestController
|
|
733
|
+
@RequestMapping("/api/v1/receipts")
|
|
734
|
+
@Tag(name = "receipts", description = "入金 API")
|
|
735
|
+
public class ReceiptController {
|
|
736
|
+
|
|
737
|
+
private final ReceiptUseCase receiptUseCase;
|
|
738
|
+
|
|
739
|
+
@PostMapping
|
|
740
|
+
@Operation(summary = "入金登録")
|
|
741
|
+
public ResponseEntity<ReceiptResponse> recordReceipt(
|
|
742
|
+
@Valid @RequestBody RecordReceiptRequest request) {
|
|
743
|
+
|
|
744
|
+
Receipt receipt = receiptUseCase.recordReceipt(request.toCommand());
|
|
745
|
+
return ResponseEntity.status(HttpStatus.CREATED)
|
|
746
|
+
.body(ReceiptResponse.from(receipt));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
@PostMapping("/{receiptNumber}/apply")
|
|
750
|
+
@Operation(summary = "入金消込")
|
|
751
|
+
public ResponseEntity<ReceiptResponse> applyReceipt(
|
|
752
|
+
@PathVariable String receiptNumber,
|
|
753
|
+
@Valid @RequestBody ApplyReceiptRequest request) {
|
|
754
|
+
|
|
755
|
+
Receipt receipt = receiptUseCase.applyToInvoices(
|
|
756
|
+
receiptNumber,
|
|
757
|
+
request.invoiceNumbers()
|
|
758
|
+
);
|
|
759
|
+
return ResponseEntity.ok(ReceiptResponse.from(receipt));
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
</details>
|
|
765
|
+
|
|
766
|
+
---
|
|
767
|
+
|
|
768
|
+
## 13.5 エラーハンドリング
|
|
769
|
+
|
|
770
|
+
### グローバル例外ハンドラー(@RestControllerAdvice)
|
|
771
|
+
|
|
772
|
+
<details>
|
|
773
|
+
<summary>GlobalExceptionHandler.java</summary>
|
|
774
|
+
|
|
775
|
+
```java
|
|
776
|
+
package com.example.sms.infrastructure.in.rest.exception;
|
|
777
|
+
|
|
778
|
+
import com.example.sms.domain.exception.BusinessRuleViolationException;
|
|
779
|
+
import com.example.sms.domain.exception.DuplicateResourceException;
|
|
780
|
+
import com.example.sms.domain.exception.OptimisticLockException;
|
|
781
|
+
import com.example.sms.domain.exception.ResourceNotFoundException;
|
|
782
|
+
import com.example.sms.infrastructure.in.rest.dto.ErrorResponse;
|
|
783
|
+
import com.example.sms.infrastructure.in.rest.dto.ValidationErrorResponse;
|
|
784
|
+
import org.slf4j.Logger;
|
|
785
|
+
import org.slf4j.LoggerFactory;
|
|
786
|
+
import org.springframework.http.HttpStatus;
|
|
787
|
+
import org.springframework.http.ResponseEntity;
|
|
788
|
+
import org.springframework.validation.FieldError;
|
|
789
|
+
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
790
|
+
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
791
|
+
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
792
|
+
|
|
793
|
+
import java.time.Instant;
|
|
794
|
+
import java.util.Map;
|
|
795
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
796
|
+
|
|
797
|
+
@RestControllerAdvice
|
|
798
|
+
public class GlobalExceptionHandler {
|
|
799
|
+
|
|
800
|
+
private static final Logger LOG = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
|
801
|
+
|
|
802
|
+
@ExceptionHandler(ResourceNotFoundException.class)
|
|
803
|
+
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException e) {
|
|
804
|
+
LOG.warn("Resource not found: {}", e.getMessage());
|
|
805
|
+
|
|
806
|
+
ErrorResponse response = new ErrorResponse(
|
|
807
|
+
HttpStatus.NOT_FOUND.value(),
|
|
808
|
+
"NOT_FOUND",
|
|
809
|
+
e.getMessage(),
|
|
810
|
+
Instant.now()
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
@ExceptionHandler(DuplicateResourceException.class)
|
|
817
|
+
public ResponseEntity<ErrorResponse> handleDuplicateResource(DuplicateResourceException e) {
|
|
818
|
+
LOG.warn("Duplicate resource: {}", e.getMessage());
|
|
819
|
+
|
|
820
|
+
ErrorResponse response = new ErrorResponse(
|
|
821
|
+
HttpStatus.CONFLICT.value(),
|
|
822
|
+
"CONFLICT",
|
|
823
|
+
e.getMessage(),
|
|
824
|
+
Instant.now()
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
@ExceptionHandler(BusinessRuleViolationException.class)
|
|
831
|
+
public ResponseEntity<ErrorResponse> handleBusinessRuleViolation(BusinessRuleViolationException e) {
|
|
832
|
+
LOG.warn("Business rule violation: {}", e.getMessage());
|
|
833
|
+
|
|
834
|
+
ErrorResponse response = new ErrorResponse(
|
|
835
|
+
HttpStatus.UNPROCESSABLE_ENTITY.value(),
|
|
836
|
+
"BUSINESS_RULE_VIOLATION",
|
|
837
|
+
e.getMessage(),
|
|
838
|
+
Instant.now()
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
@ExceptionHandler(OptimisticLockException.class)
|
|
845
|
+
public ResponseEntity<ErrorResponse> handleOptimisticLock(OptimisticLockException e) {
|
|
846
|
+
LOG.warn("Optimistic lock exception: {}", e.getMessage());
|
|
847
|
+
|
|
848
|
+
ErrorResponse response = new ErrorResponse(
|
|
849
|
+
HttpStatus.CONFLICT.value(),
|
|
850
|
+
"OPTIMISTIC_LOCK",
|
|
851
|
+
e.getMessage(),
|
|
852
|
+
Instant.now()
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
859
|
+
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(
|
|
860
|
+
MethodArgumentNotValidException e) {
|
|
861
|
+
|
|
862
|
+
Map<String, String> errors = new ConcurrentHashMap<>();
|
|
863
|
+
e.getBindingResult().getAllErrors().forEach(error -> {
|
|
864
|
+
String fieldName = ((FieldError) error).getField();
|
|
865
|
+
String errorMessage = error.getDefaultMessage();
|
|
866
|
+
errors.put(fieldName, errorMessage);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
ValidationErrorResponse response = new ValidationErrorResponse(
|
|
870
|
+
HttpStatus.BAD_REQUEST.value(),
|
|
871
|
+
"VALIDATION_ERROR",
|
|
872
|
+
"入力値が不正です",
|
|
873
|
+
errors,
|
|
874
|
+
Instant.now()
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
return ResponseEntity.badRequest().body(response);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
@ExceptionHandler(Exception.class)
|
|
881
|
+
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
|
|
882
|
+
LOG.error("Unexpected error occurred", e);
|
|
883
|
+
|
|
884
|
+
ErrorResponse response = new ErrorResponse(
|
|
885
|
+
HttpStatus.INTERNAL_SERVER_ERROR.value(),
|
|
886
|
+
"INTERNAL_ERROR",
|
|
887
|
+
"システムエラーが発生しました",
|
|
888
|
+
Instant.now()
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
</details>
|
|
897
|
+
|
|
898
|
+
### ドメイン例外の定義と変換
|
|
899
|
+
|
|
900
|
+
<details>
|
|
901
|
+
<summary>ドメイン例外クラス</summary>
|
|
902
|
+
|
|
903
|
+
```java
|
|
904
|
+
package com.example.sms.domain.exception;
|
|
905
|
+
|
|
906
|
+
public abstract class ResourceNotFoundException extends RuntimeException {
|
|
907
|
+
protected ResourceNotFoundException(String message) {
|
|
908
|
+
super(message);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
public class ProductNotFoundException extends ResourceNotFoundException {
|
|
913
|
+
public ProductNotFoundException(String productCode) {
|
|
914
|
+
super("商品が見つかりません: " + productCode);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
public class OrderNotFoundException extends ResourceNotFoundException {
|
|
919
|
+
public OrderNotFoundException(String orderNumber) {
|
|
920
|
+
super("受注が見つかりません: " + orderNumber);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
public abstract class BusinessRuleViolationException extends RuntimeException {
|
|
925
|
+
protected BusinessRuleViolationException(String message) {
|
|
926
|
+
super(message);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
public class CreditLimitExceededException extends BusinessRuleViolationException {
|
|
931
|
+
public CreditLimitExceededException(String customerCode, BigDecimal limit, BigDecimal amount) {
|
|
932
|
+
super(String.format(
|
|
933
|
+
"与信限度額を超過しています(顧客: %s, 限度額: %s, 申請額: %s)",
|
|
934
|
+
customerCode, limit, amount
|
|
935
|
+
));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
public class InsufficientInventoryException extends BusinessRuleViolationException {
|
|
940
|
+
public InsufficientInventoryException(String productCode, BigDecimal available, BigDecimal requested) {
|
|
941
|
+
super(String.format(
|
|
942
|
+
"在庫が不足しています(商品: %s, 有効在庫: %s, 要求数量: %s)",
|
|
943
|
+
productCode, available, requested
|
|
944
|
+
));
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
</details>
|
|
950
|
+
|
|
951
|
+
### ProblemDetail によるエラーレスポンス
|
|
952
|
+
|
|
953
|
+
RFC 7807 に準拠した ProblemDetail 形式でエラーを返却することも可能です。
|
|
954
|
+
|
|
955
|
+
<details>
|
|
956
|
+
<summary>エラーレスポンス DTO</summary>
|
|
957
|
+
|
|
958
|
+
```java
|
|
959
|
+
public record ErrorResponse(
|
|
960
|
+
int status,
|
|
961
|
+
String code,
|
|
962
|
+
String message,
|
|
963
|
+
Instant timestamp
|
|
964
|
+
) {}
|
|
965
|
+
|
|
966
|
+
public record ValidationErrorResponse(
|
|
967
|
+
int status,
|
|
968
|
+
String code,
|
|
969
|
+
String message,
|
|
970
|
+
Map<String, String> errors,
|
|
971
|
+
Instant timestamp
|
|
972
|
+
) {}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
</details>
|
|
976
|
+
|
|
977
|
+
### バリデーションエラーの処理
|
|
978
|
+
|
|
979
|
+
リクエスト DTO に Bean Validation アノテーションを付与し、バリデーションエラーを統一的に処理します。
|
|
980
|
+
|
|
981
|
+
<details>
|
|
982
|
+
<summary>CreateProductRequest.java</summary>
|
|
983
|
+
|
|
984
|
+
```java
|
|
985
|
+
public record CreateProductRequest(
|
|
986
|
+
@NotBlank(message = "商品コードは必須です")
|
|
987
|
+
String productCode,
|
|
988
|
+
|
|
989
|
+
@NotBlank(message = "商品名は必須です")
|
|
990
|
+
String productName,
|
|
991
|
+
|
|
992
|
+
@NotBlank(message = "商品分類コードは必須です")
|
|
993
|
+
String categoryCode,
|
|
994
|
+
|
|
995
|
+
@NotNull(message = "商品区分は必須です")
|
|
996
|
+
ProductType productType,
|
|
997
|
+
|
|
998
|
+
@NotNull(message = "税区分は必須です")
|
|
999
|
+
TaxType taxType,
|
|
1000
|
+
|
|
1001
|
+
@NotNull(message = "販売単価は必須です")
|
|
1002
|
+
@Positive(message = "販売単価は正の数である必要があります")
|
|
1003
|
+
BigDecimal sellingPrice,
|
|
1004
|
+
|
|
1005
|
+
@NotNull(message = "仕入単価は必須です")
|
|
1006
|
+
@Positive(message = "仕入単価は正の数である必要があります")
|
|
1007
|
+
BigDecimal purchasePrice
|
|
1008
|
+
) {}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
</details>
|
|
1012
|
+
|
|
1013
|
+
---
|
|
1014
|
+
|
|
1015
|
+
## 13.6 API ドキュメント
|
|
1016
|
+
|
|
1017
|
+
### OpenAPI / Swagger の設定
|
|
1018
|
+
|
|
1019
|
+
<details>
|
|
1020
|
+
<summary>OpenApiConfig.java</summary>
|
|
1021
|
+
|
|
1022
|
+
```java
|
|
1023
|
+
@Configuration
|
|
1024
|
+
public class OpenApiConfig {
|
|
1025
|
+
|
|
1026
|
+
@Bean
|
|
1027
|
+
public OpenAPI openAPI() {
|
|
1028
|
+
return new OpenAPI()
|
|
1029
|
+
.info(new Info()
|
|
1030
|
+
.title("販売管理システム API")
|
|
1031
|
+
.description("TDD で育てる販売管理システムの API ドキュメント")
|
|
1032
|
+
.version("1.0.0"))
|
|
1033
|
+
.servers(List.of(
|
|
1034
|
+
new Server()
|
|
1035
|
+
.url("http://localhost:8080")
|
|
1036
|
+
.description("開発サーバー")))
|
|
1037
|
+
.tags(List.of(
|
|
1038
|
+
new Tag().name("products").description("商品マスタ API"),
|
|
1039
|
+
new Tag().name("partners").description("取引先マスタ API"),
|
|
1040
|
+
new Tag().name("customers").description("顧客 API"),
|
|
1041
|
+
new Tag().name("orders").description("受注 API"),
|
|
1042
|
+
new Tag().name("shipments").description("出荷 API"),
|
|
1043
|
+
new Tag().name("sales").description("売上 API"),
|
|
1044
|
+
new Tag().name("invoices").description("請求 API"),
|
|
1045
|
+
new Tag().name("receipts").description("入金 API")));
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
```
|
|
1049
|
+
|
|
1050
|
+
</details>
|
|
1051
|
+
|
|
1052
|
+
### エンドポイントの文書化
|
|
1053
|
+
|
|
1054
|
+
<details>
|
|
1055
|
+
<summary>アノテーションによるドキュメント化</summary>
|
|
1056
|
+
|
|
1057
|
+
```java
|
|
1058
|
+
@GetMapping("/{productCode}")
|
|
1059
|
+
@Operation(
|
|
1060
|
+
summary = "商品の取得",
|
|
1061
|
+
description = "商品コードを指定して商品情報を取得します"
|
|
1062
|
+
)
|
|
1063
|
+
@ApiResponse(responseCode = "200", description = "商品を返却")
|
|
1064
|
+
@ApiResponse(responseCode = "404", description = "商品が見つからない")
|
|
1065
|
+
public ResponseEntity<ProductResponse> getProduct(
|
|
1066
|
+
@Parameter(description = "商品コード", example = "BEEF-001")
|
|
1067
|
+
@PathVariable String productCode) {
|
|
1068
|
+
// ...
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
</details>
|
|
1073
|
+
|
|
1074
|
+
### リクエスト・レスポンスのスキーマ定義
|
|
1075
|
+
|
|
1076
|
+
application.yml に Swagger UI の設定を追加します。
|
|
1077
|
+
|
|
1078
|
+
<details>
|
|
1079
|
+
<summary>application.yml(Swagger 設定)</summary>
|
|
1080
|
+
|
|
1081
|
+
```yaml
|
|
1082
|
+
springdoc:
|
|
1083
|
+
swagger-ui:
|
|
1084
|
+
path: /swagger-ui.html
|
|
1085
|
+
tags-sorter: alpha
|
|
1086
|
+
operations-sorter: alpha
|
|
1087
|
+
api-docs:
|
|
1088
|
+
path: /api-docs
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
</details>
|
|
1092
|
+
|
|
1093
|
+
Swagger UI にアクセスすると、API ドキュメントが自動生成されます。
|
|
1094
|
+
|
|
1095
|
+
---
|
|
1096
|
+
|
|
1097
|
+
## 13.7 API インテグレーションテスト
|
|
1098
|
+
|
|
1099
|
+
### テストコンテナによる統合テスト環境
|
|
1100
|
+
|
|
1101
|
+
Testcontainers を使用して、実際のデータベース環境で API の統合テストを実行します。
|
|
1102
|
+
|
|
1103
|
+
<details>
|
|
1104
|
+
<summary>テスト基盤クラス</summary>
|
|
1105
|
+
|
|
1106
|
+
```java
|
|
1107
|
+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
|
1108
|
+
@Testcontainers
|
|
1109
|
+
@ActiveProfiles("test")
|
|
1110
|
+
public abstract class IntegrationTestBase {
|
|
1111
|
+
|
|
1112
|
+
@Container
|
|
1113
|
+
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
|
|
1114
|
+
.withDatabaseName("sales_test")
|
|
1115
|
+
.withUsername("test")
|
|
1116
|
+
.withPassword("test");
|
|
1117
|
+
|
|
1118
|
+
@DynamicPropertySource
|
|
1119
|
+
static void configureProperties(DynamicPropertyRegistry registry) {
|
|
1120
|
+
registry.add("spring.datasource.url", postgres::getJdbcUrl);
|
|
1121
|
+
registry.add("spring.datasource.username", postgres::getUsername);
|
|
1122
|
+
registry.add("spring.datasource.password", postgres::getPassword);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
@Autowired
|
|
1126
|
+
protected TestRestTemplate restTemplate;
|
|
1127
|
+
|
|
1128
|
+
@Autowired
|
|
1129
|
+
protected ObjectMapper objectMapper;
|
|
1130
|
+
}
|
|
1131
|
+
```
|
|
1132
|
+
|
|
1133
|
+
</details>
|
|
1134
|
+
|
|
1135
|
+
### REST API エンドポイントのテスト
|
|
1136
|
+
|
|
1137
|
+
<details>
|
|
1138
|
+
<summary>ProductApiIntegrationTest.java</summary>
|
|
1139
|
+
|
|
1140
|
+
```java
|
|
1141
|
+
@DisplayName("商品 API 統合テスト")
|
|
1142
|
+
class ProductApiIntegrationTest extends IntegrationTestBase {
|
|
1143
|
+
|
|
1144
|
+
@Nested
|
|
1145
|
+
@DisplayName("商品登録・取得フロー")
|
|
1146
|
+
class ProductCrudFlow {
|
|
1147
|
+
|
|
1148
|
+
@Test
|
|
1149
|
+
@DisplayName("商品を登録して取得できる")
|
|
1150
|
+
void shouldCreateAndRetrieveProduct() {
|
|
1151
|
+
// Given: 商品登録リクエスト
|
|
1152
|
+
var createRequest = new CreateProductRequest(
|
|
1153
|
+
"BEEF-INT-001",
|
|
1154
|
+
"統合テスト用商品",
|
|
1155
|
+
"CAT-BEEF",
|
|
1156
|
+
ProductType.PRODUCT,
|
|
1157
|
+
TaxType.STANDARD,
|
|
1158
|
+
new BigDecimal("5000"),
|
|
1159
|
+
new BigDecimal("3000")
|
|
1160
|
+
);
|
|
1161
|
+
|
|
1162
|
+
// When: 商品を登録
|
|
1163
|
+
ResponseEntity<ProductResponse> createResponse = restTemplate.postForEntity(
|
|
1164
|
+
"/api/v1/products",
|
|
1165
|
+
createRequest,
|
|
1166
|
+
ProductResponse.class
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
// Then: 登録成功
|
|
1170
|
+
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1171
|
+
assertThat(createResponse.getBody()).isNotNull();
|
|
1172
|
+
assertThat(createResponse.getBody().productCode()).isEqualTo("BEEF-INT-001");
|
|
1173
|
+
|
|
1174
|
+
// When: 登録した商品を取得
|
|
1175
|
+
ResponseEntity<ProductResponse> getResponse = restTemplate.getForEntity(
|
|
1176
|
+
"/api/v1/products/BEEF-INT-001",
|
|
1177
|
+
ProductResponse.class
|
|
1178
|
+
);
|
|
1179
|
+
|
|
1180
|
+
// Then: 取得成功
|
|
1181
|
+
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1182
|
+
assertThat(getResponse.getBody().productName()).isEqualTo("統合テスト用商品");
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
@Test
|
|
1186
|
+
@DisplayName("存在しない商品を取得すると404エラー")
|
|
1187
|
+
void shouldReturn404WhenProductNotFound() {
|
|
1188
|
+
// When
|
|
1189
|
+
ResponseEntity<ErrorResponse> response = restTemplate.getForEntity(
|
|
1190
|
+
"/api/v1/products/NOT-EXIST",
|
|
1191
|
+
ErrorResponse.class
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
// Then
|
|
1195
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
|
1196
|
+
assertThat(response.getBody().code()).isEqualTo("NOT_FOUND");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
```
|
|
1201
|
+
|
|
1202
|
+
</details>
|
|
1203
|
+
|
|
1204
|
+
### データベース状態の検証
|
|
1205
|
+
|
|
1206
|
+
<details>
|
|
1207
|
+
<summary>OrderApiIntegrationTest.java</summary>
|
|
1208
|
+
|
|
1209
|
+
```java
|
|
1210
|
+
@DisplayName("受注 API 統合テスト")
|
|
1211
|
+
class OrderApiIntegrationTest extends IntegrationTestBase {
|
|
1212
|
+
|
|
1213
|
+
@Autowired
|
|
1214
|
+
private JdbcTemplate jdbcTemplate;
|
|
1215
|
+
|
|
1216
|
+
@BeforeEach
|
|
1217
|
+
void setUp() {
|
|
1218
|
+
// テストデータのセットアップ
|
|
1219
|
+
jdbcTemplate.execute("""
|
|
1220
|
+
INSERT INTO 商品 (商品コード, 適用開始日, 商品名, 販売単価, 仕入単価)
|
|
1221
|
+
VALUES ('BEEF-001', CURRENT_DATE, 'テスト牛肉', 5000, 3000)
|
|
1222
|
+
ON CONFLICT DO NOTHING
|
|
1223
|
+
""");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
@Test
|
|
1227
|
+
@DisplayName("受注登録時にデータベースに正しく保存される")
|
|
1228
|
+
void shouldPersistOrderToDatabase() {
|
|
1229
|
+
// Given
|
|
1230
|
+
var createRequest = new CreateOrderRequest(
|
|
1231
|
+
"CUS-001",
|
|
1232
|
+
LocalDate.now(),
|
|
1233
|
+
List.of(new OrderDetailRequest("BEEF-001", 10, new BigDecimal("5000")))
|
|
1234
|
+
);
|
|
1235
|
+
|
|
1236
|
+
// When
|
|
1237
|
+
ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
|
|
1238
|
+
"/api/v1/orders",
|
|
1239
|
+
createRequest,
|
|
1240
|
+
OrderResponse.class
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
// Then: API レスポンスの検証
|
|
1244
|
+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1245
|
+
String orderNumber = response.getBody().orderNumber();
|
|
1246
|
+
|
|
1247
|
+
// Then: データベース状態の検証
|
|
1248
|
+
Integer count = jdbcTemplate.queryForObject(
|
|
1249
|
+
"SELECT COUNT(*) FROM 受注 WHERE 受注番号 = ?",
|
|
1250
|
+
Integer.class,
|
|
1251
|
+
orderNumber
|
|
1252
|
+
);
|
|
1253
|
+
assertThat(count).isEqualTo(1);
|
|
1254
|
+
|
|
1255
|
+
// Then: 明細データの検証
|
|
1256
|
+
Integer detailCount = jdbcTemplate.queryForObject(
|
|
1257
|
+
"SELECT COUNT(*) FROM 受注明細 WHERE 受注番号 = ?",
|
|
1258
|
+
Integer.class,
|
|
1259
|
+
orderNumber
|
|
1260
|
+
);
|
|
1261
|
+
assertThat(detailCount).isEqualTo(1);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
</details>
|
|
1267
|
+
|
|
1268
|
+
### テストデータのセットアップとクリーンアップ
|
|
1269
|
+
|
|
1270
|
+
<details>
|
|
1271
|
+
<summary>TestDataManager.java</summary>
|
|
1272
|
+
|
|
1273
|
+
```java
|
|
1274
|
+
@Component
|
|
1275
|
+
@Profile("test")
|
|
1276
|
+
public class TestDataManager {
|
|
1277
|
+
|
|
1278
|
+
private final JdbcTemplate jdbcTemplate;
|
|
1279
|
+
|
|
1280
|
+
public TestDataManager(JdbcTemplate jdbcTemplate) {
|
|
1281
|
+
this.jdbcTemplate = jdbcTemplate;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
@Transactional
|
|
1285
|
+
public void setupMasterData() {
|
|
1286
|
+
// 商品分類マスタ
|
|
1287
|
+
jdbcTemplate.execute("""
|
|
1288
|
+
INSERT INTO 商品分類 (商品分類コード, 適用開始日, 商品分類名)
|
|
1289
|
+
VALUES ('CAT-BEEF', CURRENT_DATE, '牛肉'),
|
|
1290
|
+
('CAT-PORK', CURRENT_DATE, '豚肉')
|
|
1291
|
+
ON CONFLICT DO NOTHING
|
|
1292
|
+
""");
|
|
1293
|
+
|
|
1294
|
+
// 取引先マスタ
|
|
1295
|
+
jdbcTemplate.execute("""
|
|
1296
|
+
INSERT INTO 取引先 (取引先コード, 取引先名, 取引先区分コード)
|
|
1297
|
+
VALUES ('CUS-001', 'テスト顧客', '1'),
|
|
1298
|
+
('SUP-001', 'テスト仕入先', '2')
|
|
1299
|
+
ON CONFLICT DO NOTHING
|
|
1300
|
+
""");
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
@Transactional
|
|
1304
|
+
public void cleanupTransactionData() {
|
|
1305
|
+
// トランザクションデータのクリーンアップ(外部キー制約の順序に注意)
|
|
1306
|
+
jdbcTemplate.execute("DELETE FROM 入金明細");
|
|
1307
|
+
jdbcTemplate.execute("DELETE FROM 入金");
|
|
1308
|
+
jdbcTemplate.execute("DELETE FROM 請求明細");
|
|
1309
|
+
jdbcTemplate.execute("DELETE FROM 請求");
|
|
1310
|
+
jdbcTemplate.execute("DELETE FROM 売上明細");
|
|
1311
|
+
jdbcTemplate.execute("DELETE FROM 売上");
|
|
1312
|
+
jdbcTemplate.execute("DELETE FROM 出荷明細");
|
|
1313
|
+
jdbcTemplate.execute("DELETE FROM 出荷");
|
|
1314
|
+
jdbcTemplate.execute("DELETE FROM 受注明細");
|
|
1315
|
+
jdbcTemplate.execute("DELETE FROM 受注");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
@Transactional
|
|
1319
|
+
public void cleanupAll() {
|
|
1320
|
+
cleanupTransactionData();
|
|
1321
|
+
jdbcTemplate.execute("DELETE FROM 商品");
|
|
1322
|
+
jdbcTemplate.execute("DELETE FROM 商品分類");
|
|
1323
|
+
jdbcTemplate.execute("DELETE FROM 取引先");
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
</details>
|
|
1329
|
+
|
|
1330
|
+
<details>
|
|
1331
|
+
<summary>統合テストでの使用例</summary>
|
|
1332
|
+
|
|
1333
|
+
```java
|
|
1334
|
+
@DisplayName("販売フロー統合テスト")
|
|
1335
|
+
class SalesFlowIntegrationTest extends IntegrationTestBase {
|
|
1336
|
+
|
|
1337
|
+
@Autowired
|
|
1338
|
+
private TestDataManager testDataManager;
|
|
1339
|
+
|
|
1340
|
+
@BeforeEach
|
|
1341
|
+
void setUp() {
|
|
1342
|
+
testDataManager.cleanupTransactionData();
|
|
1343
|
+
testDataManager.setupMasterData();
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
@AfterEach
|
|
1347
|
+
void tearDown() {
|
|
1348
|
+
testDataManager.cleanupTransactionData();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
@Test
|
|
1352
|
+
@DisplayName("受注から売上までの一連のフローが正常に動作する")
|
|
1353
|
+
void shouldCompleteSalesFlow() {
|
|
1354
|
+
// 1. 受注登録
|
|
1355
|
+
var orderRequest = createOrderRequest();
|
|
1356
|
+
ResponseEntity<OrderResponse> orderResponse = restTemplate.postForEntity(
|
|
1357
|
+
"/api/v1/orders",
|
|
1358
|
+
orderRequest,
|
|
1359
|
+
OrderResponse.class
|
|
1360
|
+
);
|
|
1361
|
+
assertThat(orderResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1362
|
+
String orderNumber = orderResponse.getBody().orderNumber();
|
|
1363
|
+
|
|
1364
|
+
// 2. 出荷指示
|
|
1365
|
+
var shipmentRequest = new CreateShipmentInstructionRequest(orderNumber);
|
|
1366
|
+
ResponseEntity<ShipmentInstructionResponse> shipmentResponse = restTemplate.postForEntity(
|
|
1367
|
+
"/api/v1/shipments/instructions",
|
|
1368
|
+
shipmentRequest,
|
|
1369
|
+
ShipmentInstructionResponse.class
|
|
1370
|
+
);
|
|
1371
|
+
assertThat(shipmentResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
|
1372
|
+
String shipmentNumber = shipmentResponse.getBody().shipmentNumber();
|
|
1373
|
+
|
|
1374
|
+
// 3. 出荷確定
|
|
1375
|
+
ResponseEntity<ShipmentResponse> confirmResponse = restTemplate.postForEntity(
|
|
1376
|
+
"/api/v1/shipments/" + shipmentNumber + "/confirm",
|
|
1377
|
+
null,
|
|
1378
|
+
ShipmentResponse.class
|
|
1379
|
+
);
|
|
1380
|
+
assertThat(confirmResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1381
|
+
|
|
1382
|
+
// 4. 売上計上の確認
|
|
1383
|
+
ResponseEntity<SalesResponse[]> salesResponse = restTemplate.getForEntity(
|
|
1384
|
+
"/api/v1/sales?orderNumber=" + orderNumber,
|
|
1385
|
+
SalesResponse[].class
|
|
1386
|
+
);
|
|
1387
|
+
assertThat(salesResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
|
|
1388
|
+
assertThat(salesResponse.getBody()).hasSize(1);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
```
|
|
1392
|
+
|
|
1393
|
+
</details>
|
|
1394
|
+
|
|
1395
|
+
---
|
|
1396
|
+
|
|
1397
|
+
## 本章のまとめ
|
|
1398
|
+
|
|
1399
|
+
本章では、販売管理システムの API サービスを実装しました。
|
|
1400
|
+
|
|
1401
|
+
### 実装したコンポーネント
|
|
1402
|
+
|
|
1403
|
+
| カテゴリ | 内容 |
|
|
1404
|
+
|----------|------|
|
|
1405
|
+
| アーキテクチャ | ヘキサゴナルアーキテクチャ(Ports & Adapters) |
|
|
1406
|
+
| マスタ API | 商品マスタ、顧客マスタ |
|
|
1407
|
+
| トランザクション API | 受注、出荷、売上、請求・入金 |
|
|
1408
|
+
| エラーハンドリング | グローバル例外ハンドラー、ドメイン例外 |
|
|
1409
|
+
| API ドキュメント | OpenAPI / Swagger UI |
|
|
1410
|
+
|
|
1411
|
+
### アーキテクチャの利点
|
|
1412
|
+
|
|
1413
|
+
1. **テスト容易性**: ドメインロジックを独立してテスト可能
|
|
1414
|
+
2. **技術変更の容易さ**: アダプターを差し替えるだけで技術を変更可能
|
|
1415
|
+
3. **保守性**: 関心事の分離による高い保守性
|
|
1416
|
+
4. **チーム開発**: レイヤーごとに並行開発が可能
|
|
1417
|
+
|
|
1418
|
+
### エンドポイント一覧
|
|
1419
|
+
|
|
1420
|
+
| メソッド | パス | 説明 |
|
|
1421
|
+
|----------|------|------|
|
|
1422
|
+
| GET | `/api/v1/products` | 商品一覧の取得 |
|
|
1423
|
+
| GET | `/api/v1/products/{code}` | 商品の取得 |
|
|
1424
|
+
| POST | `/api/v1/products` | 商品の登録 |
|
|
1425
|
+
| PUT | `/api/v1/products/{code}` | 商品の更新 |
|
|
1426
|
+
| DELETE | `/api/v1/products/{code}` | 商品の削除 |
|
|
1427
|
+
| GET | `/api/v1/orders` | 受注一覧の取得 |
|
|
1428
|
+
| POST | `/api/v1/orders` | 受注の登録 |
|
|
1429
|
+
| POST | `/api/v1/shipments/instructions` | 出荷指示の登録 |
|
|
1430
|
+
| POST | `/api/v1/shipments/{number}/confirm` | 出荷確定 |
|
|
1431
|
+
| POST | `/api/v1/sales` | 売上計上 |
|
|
1432
|
+
| POST | `/api/v1/invoices/closing` | 請求締め処理 |
|
|
1433
|
+
| POST | `/api/v1/receipts` | 入金登録 |
|
|
1434
|
+
| POST | `/api/v1/receipts/{number}/apply` | 入金消込 |
|