TypeScriptでDDDを実装する(Step2:ログイン機能)

## はじめに
Step1の続きとなります。ATMの利用前にユーザのID・Passwordで認証する機能を実装します。
そのため、Userと認証機能を実装します。

https://qiita.com/koji0705/items/4b48a834c65271cec176

## 成果物
```ディレクトリ構成
~/develop/HITOTSU/vending_machine_ts$ tree -I node_modules 
.
├── domain
│   ├── AuthService.ts
│   ├── BankAccount.ts
│   ├── BankAccountRepository.ts
│   ├── BankAccountService.ts
│   ├── Cash.ts
│   ├── User.ts
│   └── UserRepository.ts
├── index.ts
├── infra
│   ├── InMemoryBankAccountRepository.ts
│   └── InMemoryUserRepository.ts
├── package.json
├── tsconfig.json
└── yarn.lock

3 directories, 13 files
```

https://github.com/kouji0705/vending_machine_ts/pull/1

## ソースコード
```ts:domain/AuthService.ts
import { UserRepository } from './UserRepository';

class AuthService {
    private repository: UserRepository;

    constructor(repository: UserRepository) {
        this.repository = repository;
    }

    authenticate(username: string, password: string): boolean {
        const user = this.repository.find(username);
        return user !== null && user.verifyPassword(password);
    }
}

export { AuthService };
```

```ts:domain/BankAccount.ts
import { Cash } from './Cash';

class BankAccount {
    private balance: number;

    constructor() {
        this.balance = 0;
    }

    deposit(cash: Cash): void {
        console.log(`Depositing cash: ${cash}`);
        this.balance += cash;
    }

    withdraw(cash: Cash): void {
        console.log(`Withdrawing cash: ${cash}`);
        if (this.balance < cash) {
            throw new Error("Insufficient balance for withdrawal");
        }
        this.balance -= cash;
    }

    getBalance(): number {
        return this.balance;
    }
}

export { BankAccount };

```

```ts:domain/BankAccountRepository.ts
import { BankAccount } from './BankAccount';

interface BankAccountRepository {
    save(bankAccount: BankAccount): void;
    find(id: string): BankAccount | null;
}

export { BankAccountRepository };
```

```ts:domain/BankAccountService.ts
import { Cash } from './Cash';
import { BankAccountRepository } from './BankAccountRepository';
import { AuthService } from './AuthService';

class BankAccountService {
    private repository: BankAccountRepository;
    private authService: AuthService;

    constructor(repository: BankAccountRepository, authService: AuthService) {
        this.repository = repository;
        this.authService = authService;
    }

    private authenticate(username: string, password: string): void {
        if (!this.authService.authenticate(username, password)) {
            throw new Error("Authentication failed");
        }
    }

    deposit(accountId: string, cash: Cash, username: string, password: string): void {
        this.authenticate(username, password);
        const account = this.repository.find(accountId);
        if (account) {
            account.deposit(cash);
            this.repository.save(account);
        } else {
            throw new Error("Bank account not found");
        }
    }

    withdraw(accountId: string, cash: Cash, username: string, password: string): void {
        this.authenticate(username, password);
        const account = this.repository.find(accountId);
        if (account) {
            account.withdraw(cash);
            this.repository.save(account);
        } else {
            throw new Error("Bank account not found");
        }
    }

    getBalance(accountId: string, username: string, password: string): number {
        this.authenticate(username, password);
        const account = this.repository.find(accountId);
        if (account) {
            return account.getBalance();
        } else {
            throw new Error("Bank account not found");
        }
    }
}

export { BankAccountService };
```

```ts:domain/Cash.ts
import z from "zod";

// 値オブジェクト
export const Cash = z.union([
    z.literal(10),
    z.literal(50),
    z.literal(100),
    z.literal(500),
    z.literal(1000),
]).brand<"Cash">();
export type Cash = z.infer<typeof Cash>;

```

```ts:domain/Cash.ts
class User {
    private username: string;
    private password: string;

    constructor(username: string, password: string) {
        this.username = username;
        this.password = password;
    }

    getUsername(): string {
        return this.username;
    }

    verifyPassword(password: string): boolean {
        return this.password === password;
    }
}

export { User };
```

```ts:domain/UserRepository.ts
import { User } from './User';

interface UserRepository {
    save(user: User): void;
    find(username: string): User | null;
}

export { UserRepository };
```

```ts:infra/InMemoryBankAccountRepository.ts
import { BankAccount } from '../domain/BankAccount';
import { BankAccountRepository } from '../domain/BankAccountRepository';

class InMemoryBankAccountRepository implements BankAccountRepository {
    private storage: Map<string, BankAccount>;

    constructor() {
        this.storage = new Map();
    }

    save(bankAccount: BankAccount): void {
        // サンプル用に固定のIDを使用
        this.storage.set("default", bankAccount);
    }

    find(id: string): BankAccount | null {
        return this.storage.get(id) || null;
    }
}

export { InMemoryBankAccountRepository };
```

```ts:infra/InMemoryUserRepository.ts
import { User } from '../domain/User';
import { UserRepository } from '../domain/UserRepository';

class InMemoryUserRepository implements UserRepository {
    private storage: Map<string, User>;

    constructor() {
        this.storage = new Map();
    }

    save(user: User): void {
        this.storage.set(user.getUsername(), user);
    }

    find(username: string): User | null {
        return this.storage.get(username) || null;
    }
}

export { InMemoryUserRepository };
```